libevent -简单的异步IO介绍
為什么80%的碼農都做不了架構師?>>> ??
A Tiny Introduction Asynchronous IO
大部初學程序員都是從阻塞IO調用開始的。如果一個IO調用是同步的,當你調用它,它不會返回,直到這個操作完成或者過去足夠多的時間使你的網絡棧自動放棄。 當你在一個TCP連接上調用connect(),例如,你的操作系統隊列一個SYN包到達主機上TCP連接的另一邊。它不會把控制權交給你,直到你接收到對面的一個SYN ACK包或者直到過去了足夠多的時間以至于它決定放棄。
這有一個非常簡單的例子使用阻塞網絡調用。它去打開一個www.google.com的連接,發送一個簡單的HTTP請求,然后打印響應到標準輸出.(google大陸被墻,主機可以換成www.baidu.com)
例子:一個簡單的阻塞HTTP 客戶端
/* For sockaddr_in */ #include <netinet/in.h> /* For socket functions */ #include <sys/socket.h> /* For gethostbyname */ #include <netdb.h>#include <unistd.h> #include <string.h> #include <stdio.h>int main(int c, char **v) {const char query[] ="GET / HTTP/1.0\r\n""Host: www.google.com\r\n""\r\n";const char hostname[] = "www.google.com";struct sockaddr_in sin;struct hostent *h;const char *cp;int fd;ssize_t n_written, remaining;char buf[1024];/* Look up the IP address for the hostname. Watch out; this isn'tthreadsafe on most platforms. */h = gethostbyname(hostname);if (!h) {fprintf(stderr, "Couldn't lookup %s: %s", hostname, hstrerror(h_errno));return 1;}if (h->h_addrtype != AF_INET) {fprintf(stderr, "No ipv6 support, sorry.");return 1;}/* Allocate a new socket */fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("socket");return 1;}/* Connect to the remote host. */sin.sin_family = AF_INET;sin.sin_port = htons(80);sin.sin_addr = *(struct in_addr*)h->h_addr;if (connect(fd, (struct sockaddr*) &sin, sizeof(sin))) {perror("connect");close(fd);return 1;}/* Write the query. *//* XXX Can send succeed partially? */cp = query;remaining = strlen(query);while (remaining) {n_written = send(fd, cp, remaining, 0);if (n_written <= 0) {perror("send");return 1;}remaining -= n_written;cp += n_written;}/* Get an answer back. */while (1) {ssize_t result = recv(fd, buf, sizeof(buf), 0);if (result == 0) {break;} else if (result < 0) {perror("recv");close(fd);return 1;}fwrite(buf, 1, result, stdout);}close(fd);return 0; }上述的代碼所有的網絡調用都是阻塞的:gethostbyname函數直到www.google.com解析成功或者失敗后才會返回;connect函數直到連接成功才返回;recv函數直到接收到數據或者一個關閉才會返回;send函數直到最后刷新它的輸出到內核寫緩沖區。
現在,IO阻塞并不是不幸的。在此期間如果你的程序不去做其他事情,那么對你來說阻塞IO將工作的很好。 但是,假設你需要寫一個程序去處理同時處理多個連接。讓我們來具體的舉一個例子:加入你想從兩個連接中讀取輸入,但是你不知道那個連接將第一個輸入。你不能說
壞例子
/*這些代碼不能工作*/ char buf[1024]; int i, n; while (i_still_want_to_read()) {for (i=0; i<n_sockets; ++i) {n = recv(fd[i], buf, sizeof(buf), 0);if (n==0)handle_close(fd[i]);else if (n<0)handle_error(fd[i], errno);elsehandle_input(fd[i], buf, n);} }當有數據在fd[2]上到來時,你的程序不能讀取fd[2]上的數據,在fd[0]和fd[1]上的數據讀完之前。
有時候人們為了解決這個問題,采用多線程,或者多進程服務。其中一個最簡單的方法就是用多線程,每一個線程去處理一個連接。這樣每一個連接都有一個自己的進程,一個連接的IO阻塞調用等待不會影響其他連接的進程阻塞。
這還有另一個例子程序。這是一個微不足道的服務程序,監聽TCP連接端口為40713,從輸入一行,讀取數據,經過ROT13處理后的數據寫出。這里為每一個到來的連接調用Unix的fork()來創建一個新的進程。
例子:ROT13分支出來的server
/* For sockaddr_in */ #include <netinet/in.h> /* For socket functions */ #include <sys/socket.h>#include <unistd.h> #include <string.h> #include <stdio.h> #include <stdlib.h>#define MAX_LINE 16384char rot13_char(char c) {/* We don't want to use isalpha here; setting the locale would change* which characters are considered alphabetical. */if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))return c + 13;else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))return c - 13;elsereturn c; }void child(int fd) {char outbuf[MAX_LINE+1];size_t outbuf_used = 0;ssize_t result;while (1) {char ch;result = recv(fd, &ch, 1, 0);if (result == 0) {break;} else if (result == -1) {perror("read");break;}/* We do this test to keep the user from overflowing the buffer. */if (outbuf_used < sizeof(outbuf)) {outbuf[outbuf_used++] = rot13_char(ch);}if (ch == '\n') {send(fd, outbuf, outbuf_used, 0);outbuf_used = 0;continue;}} }void run(void) {int listener;struct sockaddr_in sin;sin.sin_family = AF_INET;sin.sin_addr.s_addr = 0;sin.sin_port = htons(40713);listener = socket(AF_INET, SOCK_STREAM, 0);#ifndef WIN32{int one = 1;setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));} #endifif (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {perror("bind");return;}if (listen(listener, 16)<0) {perror("listen");return;}while (1) {struct sockaddr_storage ss;socklen_t slen = sizeof(ss);int fd = accept(listener, (struct sockaddr*)&ss, &slen);if (fd < 0) {perror("accept");} else {if (fork() == 0) {child(fd);exit(0);}}} }int main(int c, char **v) {run();return 0; }所以,我們有一個完美的解決方案去同時處理多連接?那我們現在可以停止寫這本書,然后去干其他事情了嗎?其實并不是.首先,進程的創建(或者線程的創建)在某些平臺上是相當昂貴的。在現實生活中,你想用一個線程池,取代去創建新進程。但從根本上來說,線程不會像你想象的那么多。如果你的程序同時需要處理成千上萬個連接,處理成千上萬的線程是不會高效的,因為CPU處理器只能處理幾個線程。
但是線程沒有解決多個連接,怎么辦? 在Unix套接字中,設置你的sockets非阻塞。在Unix中通過下面函數設置。
fcntl(fd, F_SETFL, O_NONBLOCK)
文件描述符fd就是socket函數創建的。一旦你設置了socket 描述符fd為非阻塞,當你讓網絡去調用fd,調用操作將立即完成或者返回一個錯誤標明"我不能現在無法取得任何進展,請重試"。所以我們兩個socket例子可以寫成這樣:
壞例子:忙輪詢所有套接字
/* This will work, but the performance will be unforgivably bad. */ int i, n; char buf[1024]; for (i=0; i < n_sockets; ++i)fcntl(fd[i], F_SETFL, O_NONBLOCK);while (i_still_want_to_read()) {for (i=0; i < n_sockets; ++i) {n = recv(fd[i], buf, sizeof(buf), 0);if (n == 0) {handle_close(fd[i]);} else if (n < 0) {if (errno == EAGAIN); /* The kernel didn't have any data for us to read. */elsehandle_error(fd[i], errno);} else {handle_input(fd[i], buf, n);}} }現在,我們使用非阻塞的套接字,上面的代碼將會工作,但是那只是勉強的工作。性能將會很糟糕,主要有兩個原因。第一,當連接上沒有數據去讀的,將會一直輪詢下去,你的CPU將整個被占用。第二,如果使用這種方法試著處理一個或者兩個連接時,你將為每一個做一個內核調用,不管它是不是有數據給你。所以我們需要一種告訴內核"等待那些套接字有數據給我,并告訴我那些已經準備好了"。
舊的解決方案是人們一直使用select()函數解決這個問題.select()函數調用三套fds(以位數組方式實現):一個讀,一個寫,另一個異常處理。它等待,直到一個套接字從其中一個集合準備好,并且設置了集合包含準備使用的套接字。
這我們還有一個例子,使用select實現:
例子:使用select
/* If you only have a couple dozen fds, this version won't be awful */ fd_set readset; int i, n; char buf[1024];while (i_still_want_to_read()) {int maxfd = -1;FD_ZERO(&readset);/* Add all of the interesting fds to readset */for (i=0; i < n_sockets; ++i) {if (fd[i]>maxfd) maxfd = fd[i];FD_SET(fd[i], &readset);}/* Wait until one or more fds are ready to read */select(maxfd+1, &readset, NULL, NULL, NULL);/* Process all of the fds that are still set in readset */for (i=0; i < n_sockets; ++i) {if (FD_ISSET(fd[i], &readset)) {n = recv(fd[i], buf, sizeof(buf), 0);if (n == 0) {handle_close(fd[i]);} else if (n < 0) {if (errno == EAGAIN); /* The kernel didn't have any data for us to read. */elsehandle_error(fd[i], errno);} else {handle_input(fd[i], buf, n);}}} }這有一個用select實現的POT13 服務端
例子:select()實現的POT13服務器
/* For sockaddr_in */ #include <netinet/in.h> /* For socket functions */ #include <sys/socket.h> /* For fcntl */ #include <fcntl.h> /* for select */ #include <sys/select.h>#include <assert.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <errno.h>#define MAX_LINE 16384char rot13_char(char c) {/* We don't want to use isalpha here; setting the locale would change* which characters are considered alphabetical. */if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))return c + 13;else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))return c - 13;elsereturn c; }struct fd_state {char buffer[MAX_LINE];size_t buffer_used;int writing;size_t n_written;size_t write_upto; };struct fd_state * alloc_fd_state(void) {struct fd_state *state = malloc(sizeof(struct fd_state));if (!state)return NULL;state->buffer_used = state->n_written = state->writing =state->write_upto = 0;return state; }void free_fd_state(struct fd_state *state) {free(state); }void make_nonblocking(int fd) {fcntl(fd, F_SETFL, O_NONBLOCK); }int do_read(int fd, struct fd_state *state) {char buf[1024];int i;ssize_t result;while (1) {result = recv(fd, buf, sizeof(buf), 0);if (result <= 0)break;for (i=0; i < result; ++i) {if (state->buffer_used < sizeof(state->buffer))state->buffer[state->buffer_used++] = rot13_char(buf[i]);if (buf[i] == '\n') {state->writing = 1;state->write_upto = state->buffer_used;}}}if (result == 0) {return 1;} else if (result < 0) {if (errno == EAGAIN)return 0;return -1;}return 0; }int do_write(int fd, struct fd_state *state) {while (state->n_written < state->write_upto) {ssize_t result = send(fd, state->buffer + state->n_written,state->write_upto - state->n_written, 0);if (result < 0) {if (errno == EAGAIN)return 0;return -1;}assert(result != 0);state->n_written += result;}if (state->n_written == state->buffer_used)state->n_written = state->write_upto = state->buffer_used = 0;state->writing = 0;return 0; }void run(void) {int listener;struct fd_state *state[FD_SETSIZE];struct sockaddr_in sin;int i, maxfd;fd_set readset, writeset, exset;sin.sin_family = AF_INET;sin.sin_addr.s_addr = 0;sin.sin_port = htons(40713);for (i = 0; i < FD_SETSIZE; ++i)state[i] = NULL;listener = socket(AF_INET, SOCK_STREAM, 0);make_nonblocking(listener);#ifndef WIN32{int one = 1;setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));} #endifif (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {perror("bind");return;}if (listen(listener, 16)<0) {perror("listen");return;}FD_ZERO(&readset);FD_ZERO(&writeset);FD_ZERO(&exset);while (1) {maxfd = listener;FD_ZERO(&readset);FD_ZERO(&writeset);FD_ZERO(&exset);FD_SET(listener, &readset);for (i=0; i < FD_SETSIZE; ++i) {if (state[i]) {if (i > maxfd)maxfd = i;FD_SET(i, &readset);if (state[i]->writing) {FD_SET(i, &writeset);}}}if (select(maxfd+1, &readset, &writeset, &exset, NULL) < 0) {perror("select");return;}if (FD_ISSET(listener, &readset)) {struct sockaddr_storage ss;socklen_t slen = sizeof(ss);int fd = accept(listener, (struct sockaddr*)&ss, &slen);if (fd < 0) {perror("accept");} else if (fd > FD_SETSIZE) {close(fd);} else {make_nonblocking(fd);state[fd] = alloc_fd_state();assert(state[fd]);/*XXX*/}}for (i=0; i < maxfd+1; ++i) {int r = 0;if (i == listener)continue;if (FD_ISSET(i, &readset)) {r = do_read(i, state[i]);}if (r == 0 && FD_ISSET(i, &writeset)) {r = do_write(i, state[i]);}if (r) {free_fd_state(state[i]);state[i] = NULL;close(i);}}} }int main(int c, char **v) {setvbuf(stdout, NULL, _IONBF, 0);run();return 0; }但是我們并沒有做完。因為生成和讀select()位數組所消耗的事件將與select提供的最大的fd成正比。當有大量的套接字的時候調用'select()'是很糟糕的。
不同的操作系統提供不同替換函數讓你選擇,這些包括poll(),epoll(),kqueue(),evports和/dev/poll.這些的比select的性能更佳,除poll()之外,其他的在添加一個套接字、刪除一個套接字和通知一個套接字IO已經準備就緒的時間復雜度均為O(1)。
不幸的是,沒有一個有效的接口作為一個標準。Linux 有epoll(),BSD系統中有kqueue(),Solaris系統中有evports 和 /dev/poll等,不同的系統有不同的實現。所以,如果你想寫一個便捷的性能高的異步應用程序,你需要有一個包含這些抽象接口的統一接口,來根據不同的平臺提供有效的解決。
這里有一個底層的Libevent API可以為你提供這個統一接口。它提供一個統一的接口來為各種select()替代,使用最有效的版本在任何計算機上運行。
這里還有另一個版本的異步POT13服務器實現.現在我們用libevent 2 來取代select().請注意,fd_sets 結構現在已經消失了:相反的,我們連接和分離事件通過一個event_base結構,那些可能根據select(),poll(),epoll(),kqueue()等 實現的。
例子: 一個底層的libevent 實現的POT13 服務器
/* For sockaddr_in */ #include <netinet/in.h> /* For socket functions */ #include <sys/socket.h> /* For fcntl */ #include <fcntl.h>#include <event2/event.h>#include <assert.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <errno.h>#define MAX_LINE 16384void do_read(evutil_socket_t fd, short events, void *arg); void do_write(evutil_socket_t fd, short events, void *arg);char rot13_char(char c) {/* We don't want to use isalpha here; setting the locale would change* which characters are considered alphabetical. */if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))return c + 13;else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))return c - 13;elsereturn c; }struct fd_state {char buffer[MAX_LINE];size_t buffer_used;size_t n_written;size_t write_upto;struct event *read_event;struct event *write_event; };struct fd_state * alloc_fd_state(struct event_base *base, evutil_socket_t fd) {struct fd_state *state = malloc(sizeof(struct fd_state));if (!state)return NULL;state->read_event = event_new(base, fd, EV_READ|EV_PERSIST, do_read, state);if (!state->read_event) {free(state);return NULL;}state->write_event =event_new(base, fd, EV_WRITE|EV_PERSIST, do_write, state);if (!state->write_event) {event_free(state->read_event);free(state);return NULL;}state->buffer_used = state->n_written = state->write_upto = 0;assert(state->write_event);return state; }void free_fd_state(struct fd_state *state) {event_free(state->read_event);event_free(state->write_event);free(state); }void do_read(evutil_socket_t fd, short events, void *arg) {struct fd_state *state = arg;char buf[1024];int i;ssize_t result;while (1) {assert(state->write_event);result = recv(fd, buf, sizeof(buf), 0);if (result <= 0)break;for (i=0; i < result; ++i) {if (state->buffer_used < sizeof(state->buffer))state->buffer[state->buffer_used++] = rot13_char(buf[i]);if (buf[i] == '\n') {assert(state->write_event);event_add(state->write_event, NULL);state->write_upto = state->buffer_used;}}}if (result == 0) {free_fd_state(state);} else if (result < 0) {if (errno == EAGAIN) // XXXX use evutil macroreturn;perror("recv");free_fd_state(state);} }void do_write(evutil_socket_t fd, short events, void *arg) {struct fd_state *state = arg;while (state->n_written < state->write_upto) {ssize_t result = send(fd, state->buffer + state->n_written,state->write_upto - state->n_written, 0);if (result < 0) {if (errno == EAGAIN) // XXX use evutil macroreturn;free_fd_state(state);return;}assert(result != 0);state->n_written += result;}if (state->n_written == state->buffer_used)state->n_written = state->write_upto = state->buffer_used = 1;event_del(state->write_event); }void do_accept(evutil_socket_t listener, short event, void *arg) {struct event_base *base = arg;struct sockaddr_storage ss;socklen_t slen = sizeof(ss);int fd = accept(listener, (struct sockaddr*)&ss, &slen);if (fd < 0) { // XXXX eagain??perror("accept");} else if (fd > FD_SETSIZE) {close(fd); // XXX replace all closes with EVUTIL_CLOSESOCKET */} else {struct fd_state *state;evutil_make_socket_nonblocking(fd);state = alloc_fd_state(base, fd);assert(state); /*XXX err*/assert(state->write_event);event_add(state->read_event, NULL);} }void run(void) {evutil_socket_t listener;struct sockaddr_in sin;struct event_base *base;struct event *listener_event;base = event_base_new();if (!base)return; /*XXXerr*/sin.sin_family = AF_INET;sin.sin_addr.s_addr = 0;sin.sin_port = htons(40713);listener = socket(AF_INET, SOCK_STREAM, 0);evutil_make_socket_nonblocking(listener);#ifndef WIN32{int one = 1;setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));} #endifif (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {perror("bind");return;}if (listen(listener, 16)<0) {perror("listen");return;}listener_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);/*XXX check it */event_add(listener_event, NULL);event_base_dispatch(base); }int main(int c, char **v) {setvbuf(stdout, NULL, _IONBF, 0);run();return 0; }(代碼中有些地方需要注意: sockets的類型'int', 我們使用evutil_socket_t類型來替代。 調用evutil_make_socket_nonblocking 來替代fcntl(O_NONBLOCK) 設置socket非阻塞. 這些變化是我們的代碼兼容Win32 的網絡API)
怎么樣,方便吧?(在windows 上會怎么樣呢?)
你可能已經注意到了,我們的代碼開始變的高效,也變得比較復雜了。我們不需要為每個連接管理緩沖區,每一個進程會單獨分配一個堆棧。我們不需要明確的跟蹤哪一個套接字是正在讀還是正在寫:這些隱含在我們代碼里。我們不需要一個設計去跟蹤多少操作已經完成:我們僅僅使用循環和棧變量。
此外,如果你在Windows上有豐富的網絡編程經驗,你會發現使用上面的例子不會達到很好的性能。在Windows 上,最快的異步IO方式不是使用select()這樣的接口:它是使用IOCP(IO Completion Ports[IO完成端口])API.不同其他的最快的網絡API,IOCP不會通知你的程序,當一個套接字已經準備去操作,而是當你的操作執行完成以后才通知你。相反的,程序告訴了Windows 網絡棧,去開始一個網絡操作,IOCP在程序操作完成后會通知。
幸運是 Libevent 2 的bufferevents接口決絕了這些缺陷:它讓程序寫起來非常的簡單,提供一個接口可以高效的運行在Windows 和 Unix上。
這最后一次展示POT13服務器,通過bufferevents API
例子:一個用libevent實現的很簡單的POT13服務端
/* For sockaddr_in */ #include <netinet/in.h> /* For socket functions */ #include <sys/socket.h> /* For fcntl */ #include <fcntl.h>#include <event2/event.h> #include <event2/buffer.h> #include <event2/bufferevent.h>#include <assert.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <errno.h>#define MAX_LINE 16384void do_read(evutil_socket_t fd, short events, void *arg); void do_write(evutil_socket_t fd, short events, void *arg);char rot13_char(char c) {/* We don't want to use isalpha here; setting the locale would change* which characters are considered alphabetical. */if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))return c + 13;else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))return c - 13;elsereturn c; }void readcb(struct bufferevent *bev, void *ctx) {struct evbuffer *input, *output;char *line;size_t n;int i;input = bufferevent_get_input(bev);output = bufferevent_get_output(bev);while ((line = evbuffer_readln(input, &n, EVBUFFER_EOL_LF))) {for (i = 0; i < n; ++i)line[i] = rot13_char(line[i]);evbuffer_add(output, line, n);evbuffer_add(output, "\n", 1);free(line);}if (evbuffer_get_length(input) >= MAX_LINE) {/* Too long; just process what there is and go on so that the buffer* doesn't grow infinitely long. */char buf[1024];while (evbuffer_get_length(input)) {int n = evbuffer_remove(input, buf, sizeof(buf));for (i = 0; i < n; ++i)buf[i] = rot13_char(buf[i]);evbuffer_add(output, buf, n);}evbuffer_add(output, "\n", 1);} }void errorcb(struct bufferevent *bev, short error, void *ctx) {if (error & BEV_EVENT_EOF) {/* connection has been closed, do any clean up here *//* ... */} else if (error & BEV_EVENT_ERROR) {/* check errno to see what error occurred *//* ... */} else if (error & BEV_EVENT_TIMEOUT) {/* must be a timeout event handle, handle it *//* ... */}bufferevent_free(bev); }void do_accept(evutil_socket_t listener, short event, void *arg) {struct event_base *base = arg;struct sockaddr_storage ss;socklen_t slen = sizeof(ss);int fd = accept(listener, (struct sockaddr*)&ss, &slen);if (fd < 0) {perror("accept");} else if (fd > FD_SETSIZE) {close(fd);} else {struct bufferevent *bev;evutil_make_socket_nonblocking(fd);bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);bufferevent_setcb(bev, readcb, NULL, errorcb, NULL);bufferevent_setwatermark(bev, EV_READ, 0, MAX_LINE);bufferevent_enable(bev, EV_READ|EV_WRITE);} }void run(void) {evutil_socket_t listener;struct sockaddr_in sin;struct event_base *base;struct event *listener_event;base = event_base_new();if (!base)return; /*XXXerr*/sin.sin_family = AF_INET;sin.sin_addr.s_addr = 0;sin.sin_port = htons(40713);listener = socket(AF_INET, SOCK_STREAM, 0);evutil_make_socket_nonblocking(listener);#ifndef WIN32{int one = 1;setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));} #endifif (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {perror("bind");return;}if (listen(listener, 16)<0) {perror("listen");return;}listener_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);/*XXX check it */event_add(listener_event, NULL);event_base_dispatch(base); }int main(int c, char **v) {setvbuf(stdout, NULL, _IONBF, 0);run();return 0; }這一切真的很有效嗎?
在這里寫一段XXX的效率,對于libevnet來說真的已經過時了。
英文原文鏈接,出于學習的目的翻譯所以翻譯此文。在翻譯過程中限于個人水平有限,有些地方有些缺陷,還請發現后及時與我聯系(mjrao@foxmail.com)或者fork 提交您的pull request。 謝謝!
轉載于:https://my.oschina.net/mjRao/blog/666724
總結
以上是生活随笔為你收集整理的libevent -简单的异步IO介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于css3的自定义字体
- 下一篇: 【Unity】第8章 GUI开发