前言
學網絡I/O的時候難免會碰到這樣或那樣的異步IO庫,比如libevent、libev、libuv,看完UNP之后動手寫過幾個簡單的小玩意,總感覺網絡底層的那些函數使用起來好麻煩,一個接一個地man起來也挺費勁,於是學習這些成熟網絡I/O庫的想法應運而生。
初看這些庫的簡介感覺都差不多,原理和poll/select/epoll等都大同小異,無非是在不同平台上面封裝了一層API,不過真想把他們用起來還是沒那么容易的,下面就記錄一下我學習libuv的一些過程。
最開始看的是libevent,順便把前面的I/O阻塞非阻塞同步非同步等知識復習了一遍,當我看到bufferevent的時候就看不進去了。。。(菜才是原罪),正好看到這三個庫的區別的一個文章,順便摘錄一點下來:
以下對比部分來自:https://blog.csdn.net/lijinqi1987/article/details/71214974
Libevent、libev、libuv三個網絡庫,都是c語言實現的異步事件庫Asynchronousevent library)。
異步事件庫本質上是提供異步事件通知(Asynchronous Event Notification,AEN)的。異步事件通知機制就是根據發生的事件,調用相應的回調函數進行處理。
事件(Event):事件是異步事件通知機制的核心,比如fd事件、超時事件、信號事件、定時器事件。有時候也稱事件為事件處理器(EventHandler),這個名稱更形象,因為Handler本身表示了包含處理所需數據(或數據的地址)和處理的方法(回調函數),更像是面向對象思想中的稱謂。
事件循環(EventLoop):等待並分發事件。事件循環用於管理事件。
對於應用程序來說,這些只是異步事件庫提供的API,封裝了異步事件庫跟操作系統的交互,異步事件庫會選擇一種操作系統提供的機制來實現某一種事件,比如利用Unix/Linux平台的epoll機制實現網絡IO事件,在同時存在多種機制可以利用時,異步事件庫會采用最優機制。
對比下三個庫:
libevent :名氣最大,應用最廣泛,歷史悠久的跨平台事件庫;
libev :較libevent而言,設計更簡練,性能更好,但對Windows支持不夠好;
libuv :開發node的過程中需要一個跨平台的事件庫,他們首選了libev,但又要支持Windows,故重新封裝了一套,linux下用libev實現,Windows下用IOCP實現;
優先級、事件循環、線程安全維度的對比
特性 |
libevent |
libev |
libuv |
優先級 |
激活的事件組織在優先級隊列中,各類事 件默認的優先級是相同的,可以通過設置 事件的優先級使其優先被處理 |
也是通過優先級隊列來管理激活的時間, 也可以設置事件優先級 |
沒有優先級概念,按照固定的順序訪 問各類事件 |
事件循環 |
event_base用於管理事件 |
激活的事件組織在優先級隊列中,各類事件默認的優先級是相同的, 可以通 過設置事件的優先級 使其優先被處理 |
|
線程安全 |
event_base和loop都不是線程安全的,一個event_base或loop實例只能在用戶的一個線程內訪問(一般是主線程),注冊到event_base或者loop的event都是串行訪問的,即每個執行過程中,會按照優先級順序訪問已經激活的事件,執行其回調函數。所以在僅使用一個event_base或loop的情況下,回調函數的執行不存在並行關系 |
代碼學習過程(代碼注釋):
看到好像還是libuv用的人比較多,而且速度比較好,因此打算學習libuv。
大致把libuv的API和User Guide看了一遍之后感覺還是稀里糊塗,感覺還是直接看源碼比較好,先把官方文檔里面的例子好好熟悉一下:
這是一個簡單的tcp-echo-server回射服務器:
tcp-echo-server.c:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <uv.h> 5 6 #define DEFAULT_PORT 9877//默認端口 7 #define DEFAULT_BACKLOG 128//TCP等待連接隊列最大值 8 9 uv_loop_t *loop;//loop結構指針 10 struct sockaddr_in addr;//ipv4地址結構 11 12 typedef struct { 13 uv_write_t req; 14 uv_buf_t buf; 15 } write_req_t; 16 17 void free_write_req(uv_write_t *req) {//釋放資源 18 write_req_t *wr = (write_req_t*) req; 19 free(wr->buf.base); 20 free(wr); 21 } 22 23 void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {//內存分配回調函數,buff指針用於返回相應緩沖地址!!! 24 buf->base = (char*) malloc(suggested_size);//堆上創建buff 25 buf->len = suggested_size; 26 } 27 28 void echo_write(uv_write_t *req, int status) {//status返回write的結果 29 if (status) { 30 fprintf(stderr, "Write error %s\n", uv_strerror(status)); 31 } 32 free_write_req(req); 33 } 34 35 void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {//這些參數都是libuv庫為回調函數傳遞的,其中nread表示當前讀到的字節數,buff指向緩沖區 36 if (nread > 0) { 37 write_req_t *req = (write_req_t*) malloc(sizeof(write_req_t));//write_req_t這結構有點多余吧。。直接write_req_t不行么? 38 req->buf = uv_buf_init(buf->base, nread);//復制緩沖區數據(從一個到另一個) 39 uv_write((uv_write_t*) req, client, &req->buf, 1, echo_write);//當該連接能寫的時候異步調用 40 //其中&req->buf指明了緩沖區的地址,1表示如果存在uv_buf_t數組,數組的元素個數 41 //echo_write是uv_write_cb類型回調指針,當write完成后調用。void (*uv_write_cb)(uv_write_t* req, int status) 42 return; 43 } 44 if (nread < 0) {//出錯 45 if (nread != UV_EOF)//UV_EOF不一定是0,具體見文檔 46 fprintf(stderr, "Read error %s\n", uv_err_name(nread)); 47 uv_close((uv_handle_t*) client, NULL); 48 } 49 50 free(buf->base);//釋放緩沖區 51 } 52 53 void on_new_connection(uv_stream_t *server, int status) {//這里的status參數就是庫傳給我們的狀態參數,指示當前connect的狀態(是否能連接,是否出錯等) 54 if (status < 0) {//<0表示出錯 55 fprintf(stderr, "New connection error %s\n", uv_strerror(status)); 56 // error! 57 return; 58 } 59 60 uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));//新建uv_tcp_t進行連接 61 uv_tcp_init(loop, client);//簡介之后也把這個tcp流綁定在loop上 62 if (uv_accept(server, (uv_stream_t*) client) == 0) {//連接 63 uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);//連接完成后在event loop准備讀取,這也是個異步回調 64 //等到這個事件發生(能讀),異步回調才會開始。 65 //alloc_buffer參數這里第一次碰到,其回調函數格式為void (*uv_alloc_cb)(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) 66 //該函數負責為當前行為分配緩沖區,在當前事件發生之后運行,先進行緩沖區分配工作,在這個回調函數中libuv會向你提供一個suggested_size作為緩沖區大小的建議值 67 //我們需要做的是在堆上分配uv_buf_t這樣的數據結構,並把buf指針指向該地址!(這個理解很關鍵。。。應該是這樣吧?:)) 68 //echo_read是另一個uv_read_cb類型的回調函數,會在libuv完成read之后調用(時間點重要)。 69 } 70 else { 71 uv_close((uv_handle_t*) client, NULL);//出現錯誤,關閉 72 } 73 } 74 75 int main() { 76 loop = uv_default_loop();//使用默認loop 77 78 uv_tcp_t server;//tcp_t結構,這是在棧上分配 79 uv_tcp_init(loop, &server);//算是把tcp綁定在了loop上面 80 81 uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);//ip和端口直接獲得sockaddr_in結構。。要是自己寫要好幾個函數 82 83 uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);//綁定tcp連接和地址 84 int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);//開始監聽,loop上面對於server的event監聽正式開始 85 //這里的幾個參數才是關鍵:這里可以把uv_tcp_t當成uv_stream_t的子類(進行了結構體的擴展),所以這里可以使用強制類型轉換綁定的流 86 //on_new_connection作為一個回調函數(有固定格式,庫會有參數傳遞),當有連接可以connect的時候進行調用,從參數來看調用時connect還沒完成! 87 if (r) { 88 fprintf(stderr, "Listen error %s\n", uv_strerror(r));//libuv錯誤處理函數 89 return 1; 90 } 91 return uv_run(loop, UV_RUN_DEFAULT);//開始event loop 92 }
感覺把每個函數的調用時間搞清楚,把所有參數傳遞的過程搞清楚libuv庫也就搞清楚了。
第一次看這種這么多回調函數的代碼時感覺真的是不舒服,但一步一步想通之后就感覺好多了,關鍵要搞清楚libuv庫到底為我們提供了什么。