libuv學習筆記(一)


前言

學網絡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庫到底為我們提供了什么。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM