libevent入門
花了兩天的時間在libevent上,想總結下,就以寫簡單tutorial的方式吧,貌似沒有一篇簡單的說明,讓人馬上就能上手用的。
首先給出官方文件吧: http://libevent.org ,首頁有個Programming with Libevent,裡面是一節一節的介紹libevent,但是感覺資訊量太大了,而且還是英文的-。-(當然,如果想好好用libevent,看看還是很有必要的),還有個Reference,大致就是對各個版本的libevent使用doxgen生成的文件,用來查函式原型和基本用法什麼的。
下面假定已經學習過基本的socket程式設計(socket,bind,listen,accept,connect,recv,send,close),並且對非同步/callback有基本認識。
基本的socket程式設計是阻塞/同步的,每個操作除非已經完成或者出錯才會返回,這樣對於每一個請求,要使用一個執行緒或者單獨的程式去處理,系統資源沒法支撐大量的請求(所謂c10k problem?),例如記憶體:預設情況下每個執行緒需要佔用2~8M的棧空間。posix定義了可以使用非同步的select系統呼叫,但是因為其採用了輪詢的方式來判斷某個fd是否變成active,效率不高[O(n)],連線數一多,也還是撐不住。於是各系統分別提出了基於非同步/callback的系統呼叫,例如Linux的epoll,BSD的kqueue,Windows的IOCP。由於在核心層面做了支援,所以可以用O(1)的效率查詢到active的fd。基本上,libevent就是對這些高效IO的封裝,提供統一的API,簡化開發。
libevent大概是這樣的:
預設情況下是單執行緒的(可以配置成多執行緒,如果有需要的話),每個執行緒有且只有一個event_base,對應一個struct event_base結構體(以及附於其上的事件管理器),用來schedule託管給它的一系列event,可以和作業系統的程式管理類比,當然,要更簡單一點。當一個事件發生後,event_base會在合適的時間(不一定是立即)去呼叫繫結在這個事件上的函式(傳入一些預定義的引數,以及在繫結時指定的一個引數),直到這個函式執行完,再返回schedule其他事件。
event_base內部有一個迴圈,迴圈阻塞在epoll/kqueue等系統呼叫上,直到有一個/一些事件發生,然後去處理這些事件。當然,這些事件要被繫結在這個event_base上。每個事件對應一個struct event,可以是監聽一個fd或者POSIX訊號量之類(這裡只講fd了,其他的看manual吧)。struct event使用event_new來建立和繫結,使用event_add來啟用:
注:libevent支援的事件及屬性包括(使用bitfield實現,所以要用 | 來讓它們合體)
(a) EV_TIMEOUT: 超時
(b) EV_READ: 只要網路緩衝中還有資料,回撥函式就會被觸發
(c) EV_WRITE: 只要塞給網路緩衝的資料被寫完,回撥函式就會被觸發
(d) EV_SIGNAL: POSIX訊號量,參考manual吧
(e) EV_PERSIST: 不指定這個屬性的話,回撥函式被觸發後事件會被刪除
(f) EV_ET: Edge-Trigger邊緣觸發,參考EPOLL_ET
然後需要啟動event_base的迴圈,這樣才能開始處理髮生的事件。迴圈的啟動使用event_base_dispatch,迴圈將一直持續,直到不再有需要關注的事件,或者是遇到event_loopbreak()/event_loopexit()函式。
接下來關注下繫結到event的回撥函式callback_func:傳遞給它的是一個socket fd、一個event型別及屬性bit_field、以及傳遞給event_new的最後一個引數(去上面幾行回顧一下,把event_base給傳進來了,實際上更多地是分配一個結構體,把相關的資料都撂進去,然後丟給event_new,在這裡就能取得到了)。其原型是:
對於一個伺服器而言,上面的流程大概是這樣組合的:
1. listener = socket(),bind(),listen(),設定nonblocking(POSIX系統中可使用fcntl設定,windows不需要設定,實際上libevent提供了統一的包裝evutil_make_socket_nonblocking)
2. 建立一個event_base
3. 建立一個event,將該socket託管給event_base,指定要監聽的事件型別,並繫結上相應的回撥函式(及需要給它的引數)。對於listener socket來說,只需要監聽EV_READ|EV_PERSIST
4. 啟用該事件
5. 進入事件迴圈
---------------
6. (非同步) 當有client發起請求的時候,呼叫該回撥函式,進行處理。
問題:為什麼不在listen完馬上呼叫accept,獲得客戶端連線以後再丟給event_base呢?這個問題先想想噢。
回撥函式要做什麼事情呢?當然是處理client的請求了。首先要accept,獲得一個可以與client通訊的sockfd,然後……呼叫recv/send嗎?錯!大錯特錯!如果直接呼叫recv/send的話,這個執行緒就阻塞在這個地方了,如果這個客戶端非常的陰險(比如一直不發訊息,或者網路不好,老是丟包),libevent就只能等它,沒法處理其他的請求了——所以應該建立一個新的event來託管這個sockfd。
在老版本libevent上的實現,比較羅嗦[如果不想詳細瞭解的話,看下一部分]。
對於伺服器希望先從client獲取資料的情況,大致流程是這樣的:
1. 將這個sockfd設定為nonblocking
2. 建立2個event:
event_read,綁上sockfd的EV_READ|EV_PERSIST,設定回撥函式和引數(後面提到的struct)
event_write,綁上sockfd的EV_WRITE|EV_PERSIST,設定回撥函式和引數(後面提到的struct)
3. 啟用event_read事件
------
4. (非同步) 等待event_read事件的發生, 呼叫相應的回撥函式。這裡麻煩來了:回撥函式用recv讀入的資料,不能直接用send丟給sockfd了事——因為sockfd是nonblocking的,丟給它的話,不能保證正確(為什麼呢?)。所以需要一個自己管理的快取用來儲存讀入的資料中(在accept以後就建立一個struct,作為第2步回撥函式的arg傳進來),在合適的時間(比如遇到換行符)啟用event_write事件【event_add(event_write, NULL)】,等待EV_WRITE事件的觸發
------
5. (非同步) 當event_write事件的回撥函式被呼叫的時候,往sockfd寫入資料,然後刪除event_write事件【event_del(event_write)】,等待event_read事件的下一次執行。
以上步驟比較晦澀,具體程式碼可參考官方文件裡面的【Example: A low-level ROT13 server with Libevent】
由於需要自己管理緩衝區,且過程晦澀難懂,並且不相容於Windows的IOCP,所以libevent2開始,提供了bufferevent這個神器,用來提供更加優雅、易用的API。struct bufferevent內建了兩個event(read/write)和對應的緩衝區【struct evbuffer *input, *output】,並提供相應的函式用來操作緩衝區(或者直接操作bufferevent)。每當有資料被讀入input的時候,read_cb函式被呼叫;每當output被輸出完的時候,write_cb被呼叫;在網路IO操作出現錯誤的情況(連線中斷、超時、其他錯誤),error_cb被呼叫。於是上一部分的步驟被簡化為:
1. 設定sockfd為nonblocking
2. 使用bufferevent_socket_new建立一個struct bufferevent *bev,關聯該sockfd,託管給event_base
3. 使用bufferevent_setcb(bev, read_cb, write_cb, error_cb, (void *)arg)將EV_READ/EV_WRITE對應的函式
4. 使用bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST)來啟用read/write事件
------
5. (非同步)
在read_cb裡面從input讀取資料,處理完畢後塞到output裡(會被自動寫入到sockfd)
在write_cb裡面(需要做什麼嗎?對於一個echo server來說,read_cb就足夠了)
在error_cb裡面處理遇到的錯誤
*. 可以使用bufferevent_set_timeouts(bev, struct timeval *READ, struct timeval *WRITE)來設定讀寫超時, 在error_cb裡面處理超時。
*. read_cb和write_cb的原型是
void read_or_write_callback(struct bufferevent *bev, void *arg)
error_cb的原型是
void error_cb(struct bufferevent *bev, short error, void *arg) //這個是event的標準回撥函式原型
可以從bev中用libevent的API提取出event_base、sockfd、input/output等相關資料,詳情RTFM~
於是程式碼簡化到只需要幾行的read_cb和error_cb函式即可:
於是一個支援大併發量的echo server就成型了!下面附上無註釋的echo server原始碼,110行,多抄幾遍,就能完全弄懂啦!更復雜的例子參見官方文件裡面的【Example: A simpler ROT13 server with Libevent】
首先給出官方文件吧: http://libevent.org ,首頁有個Programming with Libevent,裡面是一節一節的介紹libevent,但是感覺資訊量太大了,而且還是英文的-。-(當然,如果想好好用libevent,看看還是很有必要的),還有個Reference,大致就是對各個版本的libevent使用doxgen生成的文件,用來查函式原型和基本用法什麼的。
下面假定已經學習過基本的socket程式設計(socket,bind,listen,accept,connect,recv,send,close),並且對非同步/callback有基本認識。
基本的socket程式設計是阻塞/同步的,每個操作除非已經完成或者出錯才會返回,這樣對於每一個請求,要使用一個執行緒或者單獨的程式去處理,系統資源沒法支撐大量的請求(所謂c10k problem?),例如記憶體:預設情況下每個執行緒需要佔用2~8M的棧空間。posix定義了可以使用非同步的select系統呼叫,但是因為其採用了輪詢的方式來判斷某個fd是否變成active,效率不高[O(n)],連線數一多,也還是撐不住。於是各系統分別提出了基於非同步/callback的系統呼叫,例如Linux的epoll,BSD的kqueue,Windows的IOCP。由於在核心層面做了支援,所以可以用O(1)的效率查詢到active的fd。基本上,libevent就是對這些高效IO的封裝,提供統一的API,簡化開發。
libevent大概是這樣的:
預設情況下是單執行緒的(可以配置成多執行緒,如果有需要的話),每個執行緒有且只有一個event_base,對應一個struct event_base結構體(以及附於其上的事件管理器),用來schedule託管給它的一系列event,可以和作業系統的程式管理類比,當然,要更簡單一點。當一個事件發生後,event_base會在合適的時間(不一定是立即)去呼叫繫結在這個事件上的函式(傳入一些預定義的引數,以及在繫結時指定的一個引數),直到這個函式執行完,再返回schedule其他事件。
//建立一個event_base
struct event_base *base = event_base_new();
assert(base != NULL);
struct event_base *base = event_base_new();
assert(base != NULL);
event_base內部有一個迴圈,迴圈阻塞在epoll/kqueue等系統呼叫上,直到有一個/一些事件發生,然後去處理這些事件。當然,這些事件要被繫結在這個event_base上。每個事件對應一個struct event,可以是監聽一個fd或者POSIX訊號量之類(這裡只講fd了,其他的看manual吧)。struct event使用event_new來建立和繫結,使用event_add來啟用:
//建立並繫結一個event
struct event *listen_event;
//引數:event_base, 監聽的fd,事件型別及屬性,繫結的回撥函式,給回撥函式的引數
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, (void*)base);
//引數:event,超時時間(struct timeval *型別的,NULL表示無超時設定)
event_add(listen_event, NULL);
struct event *listen_event;
//引數:event_base, 監聽的fd,事件型別及屬性,繫結的回撥函式,給回撥函式的引數
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, (void*)base);
//引數:event,超時時間(struct timeval *型別的,NULL表示無超時設定)
event_add(listen_event, NULL);
注:libevent支援的事件及屬性包括(使用bitfield實現,所以要用 | 來讓它們合體)
(a) EV_TIMEOUT: 超時
(b) EV_READ: 只要網路緩衝中還有資料,回撥函式就會被觸發
(c) EV_WRITE: 只要塞給網路緩衝的資料被寫完,回撥函式就會被觸發
(d) EV_SIGNAL: POSIX訊號量,參考manual吧
(e) EV_PERSIST: 不指定這個屬性的話,回撥函式被觸發後事件會被刪除
(f) EV_ET: Edge-Trigger邊緣觸發,參考EPOLL_ET
然後需要啟動event_base的迴圈,這樣才能開始處理髮生的事件。迴圈的啟動使用event_base_dispatch,迴圈將一直持續,直到不再有需要關注的事件,或者是遇到event_loopbreak()/event_loopexit()函式。
//啟動事件迴圈
event_base_dispatch(base); //#add,內部直接呼叫event_base_loop(base, 0);
event_base_dispatch(base); //#add,內部直接呼叫event_base_loop(base, 0);
接下來關注下繫結到event的回撥函式callback_func:傳遞給它的是一個socket fd、一個event型別及屬性bit_field、以及傳遞給event_new的最後一個引數(去上面幾行回顧一下,把event_base給傳進來了,實際上更多地是分配一個結構體,把相關的資料都撂進去,然後丟給event_new,在這裡就能取得到了)。其原型是:
typedef void(* event_callback_fn)(evutil_socket_t sockfd, short event_type, void *arg)
對於一個伺服器而言,上面的流程大概是這樣組合的:
1. listener = socket(),bind(),listen(),設定nonblocking(POSIX系統中可使用fcntl設定,windows不需要設定,實際上libevent提供了統一的包裝evutil_make_socket_nonblocking)
2. 建立一個event_base
3. 建立一個event,將該socket託管給event_base,指定要監聽的事件型別,並繫結上相應的回撥函式(及需要給它的引數)。對於listener socket來說,只需要監聽EV_READ|EV_PERSIST
4. 啟用該事件
5. 進入事件迴圈
---------------
6. (非同步) 當有client發起請求的時候,呼叫該回撥函式,進行處理。
問題:為什麼不在listen完馬上呼叫accept,獲得客戶端連線以後再丟給event_base呢?這個問題先想想噢。
回撥函式要做什麼事情呢?當然是處理client的請求了。首先要accept,獲得一個可以與client通訊的sockfd,然後……呼叫recv/send嗎?錯!大錯特錯!如果直接呼叫recv/send的話,這個執行緒就阻塞在這個地方了,如果這個客戶端非常的陰險(比如一直不發訊息,或者網路不好,老是丟包),libevent就只能等它,沒法處理其他的請求了——所以應該建立一個新的event來託管這個sockfd。
在老版本libevent上的實現,比較羅嗦[如果不想詳細瞭解的話,看下一部分]。
對於伺服器希望先從client獲取資料的情況,大致流程是這樣的:
1. 將這個sockfd設定為nonblocking
2. 建立2個event:
event_read,綁上sockfd的EV_READ|EV_PERSIST,設定回撥函式和引數(後面提到的struct)
event_write,綁上sockfd的EV_WRITE|EV_PERSIST,設定回撥函式和引數(後面提到的struct)
3. 啟用event_read事件
------
4. (非同步) 等待event_read事件的發生, 呼叫相應的回撥函式。這裡麻煩來了:回撥函式用recv讀入的資料,不能直接用send丟給sockfd了事——因為sockfd是nonblocking的,丟給它的話,不能保證正確(為什麼呢?)。所以需要一個自己管理的快取用來儲存讀入的資料中(在accept以後就建立一個struct,作為第2步回撥函式的arg傳進來),在合適的時間(比如遇到換行符)啟用event_write事件【event_add(event_write, NULL)】,等待EV_WRITE事件的觸發
------
5. (非同步) 當event_write事件的回撥函式被呼叫的時候,往sockfd寫入資料,然後刪除event_write事件【event_del(event_write)】,等待event_read事件的下一次執行。
以上步驟比較晦澀,具體程式碼可參考官方文件裡面的【Example: A low-level ROT13 server with Libevent】
由於需要自己管理緩衝區,且過程晦澀難懂,並且不相容於Windows的IOCP,所以libevent2開始,提供了bufferevent這個神器,用來提供更加優雅、易用的API。struct bufferevent內建了兩個event(read/write)和對應的緩衝區【struct evbuffer *input, *output】,並提供相應的函式用來操作緩衝區(或者直接操作bufferevent)。每當有資料被讀入input的時候,read_cb函式被呼叫;每當output被輸出完的時候,write_cb被呼叫;在網路IO操作出現錯誤的情況(連線中斷、超時、其他錯誤),error_cb被呼叫。於是上一部分的步驟被簡化為:
1. 設定sockfd為nonblocking
2. 使用bufferevent_socket_new建立一個struct bufferevent *bev,關聯該sockfd,託管給event_base
3. 使用bufferevent_setcb(bev, read_cb, write_cb, error_cb, (void *)arg)將EV_READ/EV_WRITE對應的函式
4. 使用bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST)來啟用read/write事件
------
5. (非同步)
在read_cb裡面從input讀取資料,處理完畢後塞到output裡(會被自動寫入到sockfd)
在write_cb裡面(需要做什麼嗎?對於一個echo server來說,read_cb就足夠了)
在error_cb裡面處理遇到的錯誤
*. 可以使用bufferevent_set_timeouts(bev, struct timeval *READ, struct timeval *WRITE)來設定讀寫超時, 在error_cb裡面處理超時。
*. read_cb和write_cb的原型是
void read_or_write_callback(struct bufferevent *bev, void *arg)
error_cb的原型是
void error_cb(struct bufferevent *bev, short error, void *arg) //這個是event的標準回撥函式原型
可以從bev中用libevent的API提取出event_base、sockfd、input/output等相關資料,詳情RTFM~
於是程式碼簡化到只需要幾行的read_cb和error_cb函式即可:
void read_cb(struct bufferevent *bev, void *arg) {
char line[256];
int n;
evutil_socket_t fd = bufferevent_getfd(bev);
while (n = bufferevent_read(bev, line, 256), n > 0)
bufferevent_write(bev, line, n);
}
void error_cb(struct bufferevent *bev, short event, void *arg) {
bufferevent_free(bev);
}
char line[256];
int n;
evutil_socket_t fd = bufferevent_getfd(bev);
while (n = bufferevent_read(bev, line, 256), n > 0)
bufferevent_write(bev, line, n);
}
void error_cb(struct bufferevent *bev, short event, void *arg) {
bufferevent_free(bev);
}
於是一個支援大併發量的echo server就成型了!下面附上無註釋的echo server原始碼,110行,多抄幾遍,就能完全弄懂啦!更復雜的例子參見官方文件裡面的【Example: A simpler ROT13 server with Libevent】
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#define LISTEN_PORT 9999
#define LISTEN_BACKLOG 32
void do_accept(evutil_socket_t listener, short event, void *arg);
void read_cb(struct bufferevent *bev, void *arg);
void error_cb(struct bufferevent *bev, short event, void *arg);
void write_cb(struct bufferevent *bev, void *arg);
int main(int argc, char *argv[])
{
int ret;
evutil_socket_t listener;
listener = socket(AF_INET, SOCK_STREAM, 0);
assert(listener > 0);
evutil_make_listen_socket_reuseable(listener);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(LISTEN_PORT);
if (bind(listener, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
perror("bind");
return 1;
}
if (listen(listener, LISTEN_BACKLOG) < 0) {
perror("listen");
return 1;
}
printf ("Listening...\n");
evutil_make_socket_nonblocking(listener);
struct event_base *base = event_base_new();
assert(base != NULL);
struct event *listen_event;
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
event_add(listen_event, NULL);
event_base_dispatch(base);
printf("The End.");
return 0;
}
void do_accept(evutil_socket_t listener, short event, void *arg)
{
struct event_base *base = (struct event_base *)arg;
evutil_socket_t fd;
struct sockaddr_in sin;
socklen_t slen;
fd = accept(listener, (struct sockaddr *)&sin, &slen);
if (fd < 0) {
perror("accept");
return;
}
if (fd > FD_SETSIZE) {
perror("fd > FD_SETSIZE\n");
return;
}
printf("ACCEPT: fd = %u\n", fd);
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, read_cb, NULL, error_cb, arg);
bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST);
}
void read_cb(struct bufferevent *bev, void *arg)
{
#define MAX_LINE 256
char line[MAX_LINE+1];
int n;
evutil_socket_t fd = bufferevent_getfd(bev);
while (n = bufferevent_read(bev, line, MAX_LINE), n > 0) {
line[n] = '\0';
printf("fd=%u, read line: %s\n", fd, line);
bufferevent_write(bev, line, n);
}
}
void write_cb(struct bufferevent *bev, void *arg) {}
void error_cb(struct bufferevent *bev, short event, void *arg)
{
evutil_socket_t fd = bufferevent_getfd(bev);
printf("fd = %u, ", fd);
if (event & BEV_EVENT_TIMEOUT) {
printf("Timed out\n"); //if bufferevent_set_timeouts() called
}
else if (event & BEV_EVENT_EOF) {
printf("connection closed\n");
}
else if (event & BEV_EVENT_ERROR) {
printf("some other error\n");
}
bufferevent_free(bev);
}
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#define LISTEN_PORT 9999
#define LISTEN_BACKLOG 32
void do_accept(evutil_socket_t listener, short event, void *arg);
void read_cb(struct bufferevent *bev, void *arg);
void error_cb(struct bufferevent *bev, short event, void *arg);
void write_cb(struct bufferevent *bev, void *arg);
int main(int argc, char *argv[])
{
int ret;
evutil_socket_t listener;
listener = socket(AF_INET, SOCK_STREAM, 0);
assert(listener > 0);
evutil_make_listen_socket_reuseable(listener);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(LISTEN_PORT);
if (bind(listener, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
perror("bind");
return 1;
}
if (listen(listener, LISTEN_BACKLOG) < 0) {
perror("listen");
return 1;
}
printf ("Listening...\n");
evutil_make_socket_nonblocking(listener);
struct event_base *base = event_base_new();
assert(base != NULL);
struct event *listen_event;
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
event_add(listen_event, NULL);
event_base_dispatch(base);
printf("The End.");
return 0;
}
void do_accept(evutil_socket_t listener, short event, void *arg)
{
struct event_base *base = (struct event_base *)arg;
evutil_socket_t fd;
struct sockaddr_in sin;
socklen_t slen;
fd = accept(listener, (struct sockaddr *)&sin, &slen);
if (fd < 0) {
perror("accept");
return;
}
if (fd > FD_SETSIZE) {
perror("fd > FD_SETSIZE\n");
return;
}
printf("ACCEPT: fd = %u\n", fd);
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, read_cb, NULL, error_cb, arg);
bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST);
}
void read_cb(struct bufferevent *bev, void *arg)
{
#define MAX_LINE 256
char line[MAX_LINE+1];
int n;
evutil_socket_t fd = bufferevent_getfd(bev);
while (n = bufferevent_read(bev, line, MAX_LINE), n > 0) {
line[n] = '\0';
printf("fd=%u, read line: %s\n", fd, line);
bufferevent_write(bev, line, n);
}
}
void write_cb(struct bufferevent *bev, void *arg) {}
void error_cb(struct bufferevent *bev, short event, void *arg)
{
evutil_socket_t fd = bufferevent_getfd(bev);
printf("fd = %u, ", fd);
if (event & BEV_EVENT_TIMEOUT) {
printf("Timed out\n"); //if bufferevent_set_timeouts() called
}
else if (event & BEV_EVENT_EOF) {
printf("connection closed\n");
}
else if (event & BEV_EVENT_ERROR) {
printf("some other error\n");
}
bufferevent_free(bev);
}
--
相關文章
- libevent入門介紹
- libevent入門和使用
- libevent使用<一> libevent匯入專案
- Libevent APIAPI
- libevent之event
- libevent之bufferevents
- libevent之evbuffer
- Libevent應用 (零) Libevent簡單介紹與安裝
- libevent之evconnlistener
- 入門入門入門 MySQL命名行MySql
- libevent學習資料
- libevent之event_base
- 如何入CTF的“門”?——所謂入門就是入門
- 何入CTF的“門”?——所謂入門就是入門
- scala 從入門到入門+
- makefile從入門到入門
- ACM入門之新手入門ACM
- 【小入門】react極簡入門React
- gRPC(二)入門:Protobuf入門RPC
- 《Flutter 入門經典》之“Flutter 入門 ”Flutter
- 新手入門,webpack入門詳細教程Web
- Android入門教程 | RecyclerView使用入門AndroidView
- linux新手入門――shell入門(轉)Linux
- Libevent應用 (一) 建立event_base
- Libevent應用 (三) 資料緩衝
- libevent原始碼分析:hello-world例子原始碼
- Libevent教程001: 簡介與配置
- libevent C 事件通知介面函式庫事件函式
- MyBatis從入門到精通(一):MyBatis入門MyBatis
- SqlSugar ORM 入門到精通【一】入門篇SqlSugarORM
- Storm入門指南第二章 入門ORM
- VUE入門Vue
- MyBatis 入門MyBatis
- CSS 入門CSS
- JavaScript 入門JavaScript
- Nginx 入門Nginx
- RabbitMQ入門MQ
- GitHub入門Github