本文同步發(fā)表在豆米的博客
學(xué)習(xí)完nodejs基石之一的v8基礎(chǔ)篇(還沒看過的童鞋請?zhí)D(zhuǎn)到這里:nodejs深入學(xué)習(xí)系列之v8基礎(chǔ)篇)礁遵,我們這次將要繼續(xù)學(xué)習(xí)另外一塊基石:libuv。關(guān)于libuv的設(shè)計思想贪庙,我已經(jīng)翻譯成中文,還沒看過的童鞋還是請?zhí)D(zhuǎn)到這里: [譯文]libuv設(shè)計思想概述被芳,如果還沒看完這篇文章的童鞋烤送,下面的內(nèi)容也不建議細(xì)看了,因為會有”代溝“的問題~
本文的所有示例代碼都可以在這個倉庫中找到: libuv-demo
1爹土、libuv入門介紹
libuv是一個跨平臺聚焦于異步IO的庫,著名的event-loop便是libuv的一大特色踩身。我們要學(xué)習(xí)Libuv胀茵,那么就要先掌握libuv的編譯。
1.1挟阻、libuv的編譯簡單介紹
和v8一樣琼娘,libuv的編譯簡單概括如下:
- 先下載GYP:
git clone https://chromium.googlesource.com/external/gyp build/gyp
- 指定ninja:
./gyp_uv.py -f ninja
- 編譯:
ninja -C out/Debug
- 跑測試:
./out/Debug/run-tests
1.2、libuv簡單使用
利用編譯好的libuv庫文件赁濒,我們可以開始寫一個簡單又經(jīng)典的例子: Hello world轨奄。
#include "stdio.h"
#include "uv.h"
int main() {
uv_loop_t *loop = uv_default_loop();
printf("hello libuv");
uv_run(loop, UV_RUN_DEFAULT);
}
喜歡動手的童鞋可以下載一開始提到的demo,其中的hello_libuv.c
便是拒炎,利用如何正確地使用v8嵌入到我們的C++應(yīng)用中這篇文章講到的運(yùn)行方式挪拟,我們借助CLion
軟件和CMakeLists.txt
文件來編譯所有的demo模塊,這方面就不再贅述了击你,記得將CMakeLists.txt
文件中的include_directories
和link_directories
改成你在第一小節(jié)編譯出來的Libuv靜態(tài)庫文件的目錄位置玉组。
好了,有了上面的基礎(chǔ)之后丁侄,我們開始結(jié)合demo來入門這個深藏眾多秘密的代碼庫惯雳。接下去的文章可能會比較長,一次讀不完的話建議收藏起來鸿摇,多讀幾次~
2石景、libuv的基礎(chǔ)概念介紹與實踐
看懂libuv之前,我們需要理解下面這些概念拙吉,并用實際用例來測試這些概念潮孽。
2.1、event-loop線程
我們都知道線程是操作系統(tǒng)最基本的調(diào)度單元筷黔,而進(jìn)程是操作系統(tǒng)的最基本的資源分配單元往史,因此可以知道進(jìn)程其實是不能運(yùn)行,能運(yùn)行的是進(jìn)程中的線程佛舱。進(jìn)程僅僅是一個容器椎例,包含了線程運(yùn)行中所需要的數(shù)據(jù)結(jié)構(gòu)等信息挨决。一個進(jìn)程創(chuàng)建時,操作系統(tǒng)會創(chuàng)建一個線程订歪,這就是主線程脖祈,而其他的從線程,都要在主線程的代碼來創(chuàng)建陌粹,也就是由程序員來創(chuàng)建撒犀。因此每一個可執(zhí)行的運(yùn)用程序都至少有一個線程
于是libuv一開始便啟動了event-loop線程,再在這個主線程上利用線程池去創(chuàng)建更多的線程掏秩。在event-loop線程中是一段while(1)
的死循環(huán)代碼,直到?jīng)]有活躍的句柄的時候才會退出荆姆,這個時候libuv進(jìn)程才被銷毀掉蒙幻。清楚這點對于后面的學(xué)習(xí)至關(guān)重要。
2.2胆筒、Handle
中文翻譯為句柄邮破,如[譯文]libuv設(shè)計思想概述一文所屬,整個libuv的實現(xiàn)都是基于Handle和Request仆救。所以理解句柄以及l(fā)ibuv提供的所有句柄實例才能夠真的掌握libuv抒和。按照原文所述,句柄是:
表示能夠在活動時執(zhí)行某些操作的長生命周期對象彤蔽。
理解這句話的意思,首先我們抓住兩個關(guān)鍵詞:長生命周期摧莽、對象。Libuv所有的句柄都需要初始化顿痪,而初始化都會調(diào)用類似這種函數(shù):uv_xxx_init
镊辕。xxx
表示句柄的類型,在該函數(shù)中蚁袭,會將傳入的形參handle
初始化征懈,并賦值返回具體的對象,比如初始化tcp句柄:
... // 隨便截取一段初始化代碼
handle->tcp.serv.accept_reqs = NULL;
handle->tcp.serv.pending_accepts = NULL;
handle->socket = INVALID_SOCKET;
handle->reqs_pending = 0;
handle->tcp.serv.func_acceptex = NULL;
handle->tcp.conn.func_connectex = NULL;
handle->tcp.serv.processed_accepts = 0;
handle->delayed_error = 0
...
理解了句柄其實就是個對象揩悄,那么長生命周期要是怎樣的卖哎?還是以TCP句柄為例子,你在這個例子tcpserver.c中删性,可以看到后面tcp服務(wù)器的操作:綁定端口亏娜、監(jiān)聽端口都是基于tcp句柄,整個句柄存活于整個應(yīng)用程序镇匀,只要tcp服務(wù)器沒有掛掉就一直在照藻,因此說是長生命周期的對象。
libuv提供的所有句柄如下:
接下去我們簡單介紹以下所有的Libuv的句柄
2.2.1汗侵、uv_handle_t
首先libuv有一個基本的handle, uv_handle_t
幸缕,libuv是所有其他handle的基本范式群发,任何handle都可以強(qiáng)轉(zhuǎn)為該類型,并且和該Handle相關(guān)的所有API都可以為其他handle使用发乔。
libuv能否一直運(yùn)行下去的前提是檢查是否有活躍的句柄存在熟妓,而檢查一個句柄是否活躍(可以使用方法uv_is_active(const uv_handle_t* handle)
檢查),根據(jù)句柄類型不同栏尚,其含義也不一樣:
-
uv_async_t
句柄總是活躍的并且不能停用起愈,除非使用uv_close
關(guān)閉掉 -
uv_pipe_t
、uv_tcp_t
,uv_udp_t
等译仗,這些牽扯到I/O的句柄一般也都是活躍 -
uv_check_t
,uv_idle_t
,uv_timer_t
等抬虽,當(dāng)這些句柄開始調(diào)用uv_check_start()
,uv_idle_start()
的時候也是活躍的。
而檢查哪些句柄活躍則可以使用這個方法:uv_print_active_handles(handle->loop, stderr);
以tcpserver.c為例子纵菌,我們啟動tcp服務(wù)器后阐污,啟動一個定時器去打印存在的句柄,結(jié)果如下:
[-AI] async 0x10f78e9d8
[RA-] tcp 0x10f78e660
[RA-] timer 0x7ffee049d7c0
可以看到tcp的例子中一直存活的句柄是async咱圆、tcp笛辟、timer。它們前面中括號的標(biāo)志解釋如下:
R 表示該句柄被引用著
A 表示該句柄此時處于活躍狀態(tài)
I 表示該句柄是內(nèi)部使用的
2.2.2序苏、uv_timer_t
顧名思義手幢,Libuv的計時器,用來在將來某個時候調(diào)用對應(yīng)設(shè)置的回調(diào)函數(shù)忱详。其調(diào)用時機(jī)是在整個輪詢的最最開始围来,后面我們會說到輪詢的整個步驟。
2.2.3踱阿、uv_idle_t
Idle句柄在每次循環(huán)迭代中運(yùn)行一次給定的回調(diào)管钳,而且執(zhí)行順序是在prepare句柄
之前。
與prepare句柄
的顯著區(qū)別在于软舌,當(dāng)存在活動的空閑句柄時才漆,循環(huán)將執(zhí)行零超時輪詢,而不是阻塞I/O佛点。
在uv_backend_timeout
方法中我們可以看到返回的輪詢I/O超時時間是0:
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
idle句柄的回調(diào)一般用來執(zhí)行一些低優(yōu)先級的任務(wù)醇滥。
**注意:盡管名稱叫做“idle”,空閑句柄在每次循環(huán)迭代時都會調(diào)用它們的回調(diào)函數(shù)超营,而不是在循環(huán)實際上是“空閑”的時候鸳玩。**
2.2.3、uv_prepare_t
prepare句柄將在每次循環(huán)迭代中運(yùn)行一次給定的回調(diào)演闭,而且是選擇在I/O輪詢之前不跟。
問題是:libuv為什么要創(chuàng)造這么一種句柄?其實從名稱來猜測米碰,libuv應(yīng)該是想提供一種方式讓你可以在輪詢I/O之前做些事情窝革,然后在輪詢I/O之后使用check句柄進(jìn)行一些結(jié)果的校驗购城。
2.2.4、uv_check_t
check句柄將在每次循環(huán)迭代中運(yùn)行一次給定的回調(diào)虐译,而且是選擇在I/O輪詢之后瘪板。其目的在上面已經(jīng)提過
2.2.5、uv_async_t
Async句柄允許用戶“喚醒”事件循環(huán)漆诽,并在主線程(原文翻譯為another thread侮攀,其實不對)調(diào)用一開始注冊的回調(diào)。這里說的喚醒其實就是發(fā)送消息給主線程(event-loop線程)厢拭,讓其可以執(zhí)行一開始注冊的回調(diào)了兰英。
**注意:libuv會對`uv_async_send()`做一個聚合處理。也就是說它并不會調(diào)用一次就執(zhí)行一次回調(diào)供鸠。**
我們使用thread.c為例子箭昵,使用uv_queue_work
和uv_async_send
來實踐,得到的結(jié)果打印如下:
// 打印出主進(jìn)程ID號和event-loop線程ID
I am the master process, processId => 90714
I am event loop thread => 0x7fff8c2d9380
// 這個是uv_queue_work執(zhí)行的回調(diào)回季,從線程ID可以看到回調(diào)函數(shù)是在線程池中的某個線程中執(zhí)行
I am work callback, calling in some thread in thread pool, pid=>90714
work_cb thread id 0x700001266000
// 這個是uv_queue_work執(zhí)行完回調(diào)后結(jié)束的回調(diào),從線程ID可以看到這個回調(diào)已經(jīng)回到了主線程中執(zhí)行
I am after work callback, calling from event loop thread, pid=>90714
after_work_cb thread id 0x7fff8c2d9380
// 這個是uv_async_init的回調(diào)正林,其觸發(fā)是因為在work callback中執(zhí)行了uv_async_send泡一,可以從0x700001266000得到驗證,該回調(diào)也是在主線程中執(zhí)行
I am async callback, calling from event loop thread, pid=>90714
async_cb thread id 0x7fff8c2d9380
I am receiving msg: This msg from another thread: 0x700001266000
2.2.6觅廓、uv_poll_t
Poll句柄用于監(jiān)視文件描述符的可讀性鼻忠、可寫性和斷開連接,類似于poll(2)
的目的杈绸。
Poll句柄的目的是支持集成外部庫帖蔓,這些庫依賴于事件循環(huán)來通知套接字狀態(tài)的更改,比如c-ares
或libssh2
瞳脓。不建議將uv_poll_t用于任何其他目的;因為像uv_tcp_t
塑娇、uv_udp_t
等提供了一個比uv_poll_t
更快、更可伸縮的實現(xiàn)劫侧,尤其是在Windows上埋酬。
可能輪詢處理偶爾會發(fā)出信號,表明文件描述符是可讀或可寫的烧栋,即使它不是写妥。因此,當(dāng)用戶試圖從fd讀取或?qū)懭霑r审姓,應(yīng)該總是準(zhǔn)備再次處理EAGAIN錯誤或類似的EAGAIN錯誤珍特。
同一個套接字不能有多個活躍的Poll句柄,因為這可能會導(dǎo)致libuv出現(xiàn)busyloop
或其他故障魔吐。
當(dāng)活躍的Poll句柄輪詢文件描述符時扎筒,用戶不應(yīng)關(guān)閉該文件描述符莱找。否則可能導(dǎo)致句柄報告錯誤,但也可能開始輪詢另一個套接字砸琅。但是宋距,可以在調(diào)用uv_poll_stop()
或uv_close()
之后立即安全地關(guān)閉fd。
**在Windows上症脂,只有套接字的文件描述符可以被輪詢谚赎,Linux上,任何[`poll(2)`](http://linux.die.net/man/2/poll)接受的文件描述符都可以被輪詢**
下面羅列的是輪詢的事件類型:
enum uv_poll_event {
UV_READABLE = 1,
UV_WRITABLE = 2,
UV_DISCONNECT = 4,
UV_PRIORITIZED = 8
};
2.2.7诱篷、uv_signal_t
Signal句柄在每個事件循環(huán)的基礎(chǔ)上實現(xiàn)Unix風(fēng)格的信號處理壶唤。在udpserver.c中展示了Signal句柄的使用方式:
uv_signal_t signal_handle;
r = uv_signal_init(loop, &signal_handle);
CHECK(r, "uv_signal_init");
r = uv_signal_start(&signal_handle, signal_cb, SIGINT);
void signal_cb(uv_signal_t *handle, int signum) {
printf("signal_cb: recvd CTRL+C shutting down\n");
uv_stop(uv_default_loop()); //stops the event loop
}
關(guān)于Signal句柄有幾個點要知悉:
- 以編程方式調(diào)用
raise()
或abort()
觸發(fā)的信號不會被libuv檢測到;所以這些信號不會對應(yīng)的回調(diào)函數(shù)。 - SIGKILL和SIGSTOP是不可能被捕捉到的
- 通過libuv處理SIGBUS棕所、SIGFPE闸盔、SIGILL或SIGSEGV會導(dǎo)致未定義的行為
2.2.8、uv_process_t
process句柄將會新建一個新的進(jìn)程并且能夠允許用戶控制該進(jìn)程并使用流去建立通信通道琳省。對應(yīng)的demo可以查看:process.c迎吵,值得注意的是,args
中提供的結(jié)構(gòu)體的第一個參數(shù)path指的是可執(zhí)行程序的路徑针贬,比如在demo中:
const char* exepath = exepath_for_process();
char *args[3] = { (char*) exepath, NULL, NULL };
實例中的exepath是:FsHandle
的執(zhí)行路徑击费。
另外一個注意點就是父子進(jìn)程的std的配置,demo中提供了一些參考桦他,如果使用管道的話還可以參考另外一個demo:pipe
2.2.9蔫巩、uv_stream_t
流句柄提供了雙工通信通道的抽象。uv_stream_t
是一種抽象類型快压,libuv以uv_tcp_t
圆仔、uv_pipe_t
和uv_tty_t
的形式提供了3種流實現(xiàn)。這個沒有具體實例蔫劣。但是libuv有好幾個方法的入?yún)⒍际?code>uv_stream_t坪郭,說明這些方法都是可以被tcp/pipe/tty
使用,具體有:
int uv_shutdown(uv_shutdown_t* req, uv_stream_t* handle, uv_shutdown_cb cb)
int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb)
int uv_accept(uv_stream_t* server, uv_stream_t* client)
int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb)
int uv_read_stop(uv_stream_t*)
int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb)
int uv_write2(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_stream_t* send_handle, uv_write_cb cb)
2.2.10拦宣、uv_tcp_t
tcp句柄可以用來表示TCP流和服務(wù)器截粗。上小節(jié)說到的uv_stream_t
是uv_tcp_t
的”父類“,這里使用結(jié)構(gòu)體繼承的方式實現(xiàn)鸵隧,uv_handle_t
绸罗、uv_stream_t
、uv_tcp_t
三者的結(jié)構(gòu)關(guān)系如下圖:
使用libuv創(chuàng)建tcp服務(wù)器的步驟可以歸納為:
1豆瘫、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
2珊蟀、綁定地址:uv_tcp_bind
3、監(jiān)聽連接:uv_listen
4、每當(dāng)有一個連接進(jìn)來之后育灸,調(diào)用uv_listen的回調(diào)腻窒,回調(diào)里要做如下事情:
4.1、初始化客戶端的tcp句柄:uv_tcp_init()
4.2磅崭、接收該客戶端的連接:uv_accept()
4.3儿子、開始讀取客戶端請求的數(shù)據(jù):uv_read_start()
4.4、讀取結(jié)束之后做對應(yīng)操作砸喻,如果需要響應(yīng)客戶端數(shù)據(jù)柔逼,調(diào)用uv_write,回寫數(shù)據(jù)即可割岛。
更多細(xì)節(jié)參考demo
2.2.11愉适、uv_pipe_t
Pipe句柄在Unix上提供了對本地域套接字的抽象,在Windows上提供了命名管道癣漆。它是uv_stream_t
的“子類”维咸。管道的用途很多,可以用來讀寫文件惠爽,還可以用來做線程間的通信癌蓖。我們在實例中用來實現(xiàn)主線程與多個子線程的互相通信。實現(xiàn)的模型是這樣的:
從模型中可以看出婚肆,我們利用管道將客戶端的連接綁定到隨機(jī)的一個線程上费坊,之后的操作都是該線程和客戶端的通信。
2.2.12旬痹、uv_tty_t
TTY句柄表示控制臺的一種流,用的比較少讨越,就不多說了~
2.2.13两残、uv_udp_t
UDP句柄為客戶端和服務(wù)器封裝UDP通信。使用libuv創(chuàng)建udp服務(wù)器的步驟可以概括為:
1把跨、初始化接收端的uv_udp_t: uv_udp_init(loop, &receive_socket_handle)
2人弓、綁定地址:uv_udp_bind
3、開始接收消息:uv_udp_recv_start
4着逐、uv_udp_recv_start里執(zhí)行回調(diào)崔赌,可以使用下面方法回寫數(shù)據(jù)發(fā)送給客戶端
4.1、uv_udp_init初始化send_socket_handle
4.2耸别、uv_udp_bind綁定發(fā)送者的地址健芭,地址可以從recv獲取
4.3、uv_udp_send發(fā)送指定消息
如果是官方文檔給出的示例的話秀姐,那么會使用uv_udp_set_broadcast
設(shè)置廣播的地址慈迈。具體可以參考udp
2.2.14、uv_fs_event_t
FS事件句柄允許用戶監(jiān)視一個給定的路徑的更新事件省有,例如痒留,如果文件被重命名或其中有一個通用更改谴麦。這個句柄使用每個平臺上最佳的解決方案。
2.2.15伸头、uv_fs_poll_t
FS輪詢句柄允許用戶監(jiān)視給定的更改路徑匾效。與uv_fs_event_t
不同,fs poll句柄使用stat
檢測文件何時發(fā)生了更改恤磷,這樣它們就可以在不支持fs事件句柄的文件系統(tǒng)上工作面哼。
2.3、Request
那么接下去就說到Request這個短生命周期的概念碗殷,中文翻譯為”請求“精绎,類似于nodejs中的req,它也是一個結(jié)構(gòu)體。還是以上述的tcp服務(wù)器為例子锌妻,有這么一段代碼:
if (r < 0) {
// 如果接受連接失敗代乃,需要清理一些東西
uv_shutdown_t *shutdown_req = malloc(sizeof(uv_shutdown_t));
r = uv_shutdown(shutdown_req, (uv_stream_t *)tcp_client_handle, shutdown_cb);
CHECK(r, "uv_shutdown");
}
當(dāng)客戶端連接失敗炭菌,需要關(guān)閉掉這個連接颈渊,于是我們就會初始化一個request
,然后傳遞給我們需要請求的操作寓娩,這里是關(guān)閉請求shutdown
吭历。
關(guān)于libuv提供的句柄和request堕仔,我這里整理一張思維導(dǎo)圖,可以一看:
libuv的Request操作對比于句柄晌区,還是比較少的摩骨。上圖把每一個request的使用說明都講得一清二楚了。我們能做的就是隨時翻閱這篇文章即可朗若。
2.3.1恼五、uv_request_t
uv_request_t
是基本的request,其他任何request都是基于該結(jié)構(gòu)進(jìn)行擴(kuò)展哭懈,它定義的所有api其他request都可以使用灾馒。和uv_handle_t
一樣的功效。
2.4遣总、libuv運(yùn)行的三種模式
接著說說Libuv提供的三種運(yùn)行模式:
- UV_RUN_DEFAULT 默認(rèn)輪詢模式睬罗,此模式會一直運(yùn)行事件循環(huán)直到?jīng)]有活躍句柄、引用句柄旭斥、和請求句柄
- UV_RUN_ONCE 一次輪詢模式容达,此模式如果pending_queue中有回調(diào),則會執(zhí)行回調(diào)而直接跨過uv__io_poll垂券。如果沒有董饰,則此方式只會執(zhí)行一次I/O輪詢(uv__io_poll)。如果在執(zhí)行過后有回調(diào)壓入到了pending_queue中,則uv_run會返回非0卒暂,你需要在未來的某個時間再次觸發(fā)一次uv_run來清空pending_queue啄栓。
- UV_RUN_NOWAIT 一次輪詢(無視pending_queue)模式,此模式類似UV_RUN_ONCE但是不會判斷pending_queue是否存在回調(diào)也祠,直接進(jìn)行一次I/O輪詢昙楚。
最后
ok,限于篇幅诈嘿,libuv的基礎(chǔ)篇仍未結(jié)束堪旧,你可以點我繼續(xù)閱讀第二篇,也可以先自己消化消化~