學習完nodejs基石之一的v8基礎篇(還沒看過的童鞋請跳轉到這裏:nodejs深刻學習系列之v8基礎篇),咱們此次將要繼續學習另一塊基石:libuv。關於libuv的設計思想,我已經翻譯成中文,還沒看過的童鞋仍是請跳轉到這裏: [譯文]libuv設計思想概述,若是還沒看完這篇文章的童鞋,下面的內容也不建議細看了,由於會有」代溝「的問題~html
本文的全部示例代碼均可以在這個倉庫中找到: libuv-demonode
libuv是一個跨平臺聚焦於異步IO的庫,著名的event-loop即是libuv的一大特點。咱們要學習Libuv,那麼就要先掌握libuv的編譯。linux
和v8同樣,libuv的編譯簡單歸納以下:git
git clone https://chromium.googlesource.com/external/gyp build/gyp
./gyp_uv.py -f ninja
ninja -C out/Debug
./out/Debug/run-tests
利用編譯好的libuv庫文件,咱們能夠開始寫一個簡單又經典的例子: 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++應用中這篇文章講到的運行方式,咱們藉助CLion
軟件和CMakeLists.txt
文件來編譯全部的demo模塊,這方面就再也不贅述了,記得將CMakeLists.txt
文件中的include_directories
和link_directories
改爲你在第一小節編譯出來的Libuv靜態庫文件的目錄位置。github
好了,有了上面的基礎以後,咱們開始結合demo來入門這個深藏衆多祕密的代碼庫。接下去的文章可能會比較長,一次讀不完的話建議收藏起來,多讀幾回~編程
看懂libuv以前,咱們須要理解下面這些概念,並用實際用例來測試這些概念。api
咱們都知道線程是操做系統最基本的調度單元,而進程是操做系統的最基本的資源分配單元,所以能夠知道進程實際上是不能運行,能運行的是進程中的線程。進程僅僅是一個容器,包含了線程運行中所須要的數據結構等信息。一個進程建立時,操做系統會建立一個線程,這就是主線程,而其餘的從線程,都要在主線程的代碼來建立,也就是由程序員來建立。所以每個可執行的運用程序都至少有一個線程安全
因而libuv一開始便啓動了event-loop線程,再在這個主線程上利用線程池去建立更多的線程。在event-loop線程中是一段while(1)
的死循環代碼,直到沒有活躍的句柄的時候纔會退出,這個時候libuv進程才被銷燬掉。清楚這點對於後面的學習相當重要。bash
中文翻譯爲句柄,如[譯文]libuv設計思想概述一文所屬,整個libuv的實現都是基於Handle和Request。因此理解句柄以及libuv提供的全部句柄實例纔可以真的掌握libuv。按照原文所述,句柄是:
表示可以在活動時執行某些操做的長生命週期對象。
複製代碼
理解這句話的意思,首先咱們抓住兩個關鍵詞:長生命週期、對象。Libuv全部的句柄都須要初始化,而初始化都會調用相似這種函數:uv_xxx_init
。xxx
表示句柄的類型,在該函數中,會將傳入的形參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服務器的操做:綁定端口、監聽端口都是基於tcp句柄,整個句柄存活於整個應用程序,只要tcp服務器沒有掛掉就一直在,所以說是長生命週期的對象。
libuv提供的全部句柄以下:
接下去咱們簡單介紹如下全部的Libuv的句柄
首先libuv有一個基本的handle, uv_handle_t
,libuv是全部其餘handle的基本範式,任何handle均可以強轉爲該類型,而且和該Handle相關的全部API均可覺得其餘handle使用。
libuv可否一直運行下去的前提是檢查是否有活躍的句柄存在,而檢查一個句柄是否活躍(可使用方法uv_is_active(const uv_handle_t* handle)
檢查),根據句柄類型不一樣,其含義也不同:
uv_async_t
句柄老是活躍的而且不能停用,除非使用uv_close
關閉掉uv_pipe_t
、uv_tcp_t
, uv_udp_t
等,這些牽扯到I/O的句柄通常也都是活躍uv_check_t
, uv_idle_t
, uv_timer_t
等,當這些句柄開始調用uv_check_start()
, uv_idle_start()
的時候也是活躍的。而檢查哪些句柄活躍則可使用這個方法:uv_print_active_handles(handle->loop, stderr);
以tcpserver.c爲例子,咱們啓動tcp服務器後,啓動一個定時器去打印存在的句柄,結果以下:
[-AI] async 0x10f78e9d8
[RA-] tcp 0x10f78e660
[RA-] timer 0x7ffee049d7c0
複製代碼
能夠看到tcp的例子中一直存活的句柄是async、tcp、timer。它們前面中括號的標誌解釋以下:
R 表示該句柄被引用着
A 表示該句柄此時處於活躍狀態
I 表示該句柄是內部使用的
複製代碼
顧名思義,Libuv的計時器,用來在未來某個時候調用對應設置的回調函數。其調用時機是在整個輪詢的最最開始,後面咱們會說到輪詢的整個步驟。
Idle句柄在每次循環迭代中運行一次給定的回調,並且執行順序是在prepare句柄
以前。
與prepare句柄
的顯著區別在於,當存在活動的空閒句柄時,循環將執行零超時輪詢,而不是阻塞I/O。
在uv_backend_timeout
方法中咱們能夠看到返回的輪詢I/O超時時間是0:
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
複製代碼
idle句柄的回調通常用來執行一些低優先級的任務。
**注意:儘管名稱叫作「idle」,空閒句柄在每次循環迭代時都會調用它們的回調函數,而不是在循環其實是「空閒」的時候。**
複製代碼
prepare句柄將在每次循環迭代中運行一次給定的回調,並且是選擇在I/O輪詢以前。
問題是:libuv爲何要創造這麼一種句柄?其實從名稱來猜想,libuv應該是想提供一種方式讓你能夠在輪詢I/O以前作些事情,而後在輪詢I/O以後使用check句柄進行一些結果的校驗。
check句柄將在每次循環迭代中運行一次給定的回調,並且是選擇在I/O輪詢以後。其目的在上面已經提過
Async句柄容許用戶「喚醒」事件循環,並在主線程(原文翻譯爲another thread,其實不對)調用一開始註冊的回調。這裏說的喚醒其實就是發送消息給主線程(event-loop線程),讓其能夠執行一開始註冊的回調了。
**注意:libuv會對`uv_async_send()`作一個聚合處理。也就是說它並不會調用一次就執行一次回調。**
複製代碼
咱們使用thread.c爲例子,使用uv_queue_work
和uv_async_send
來實踐,獲得的結果打印以下:
// 打印出主進程ID號和event-loop線程ID
I am the master process, processId => 90714
I am event loop thread => 0x7fff8c2d9380
// 這個是uv_queue_work執行的回調,從線程ID能夠看到回調函數是在線程池中的某個線程中執行
I am work callback, calling in some thread in thread pool, pid=>90714
work_cb thread id 0x700001266000
// 這個是uv_queue_work執行完回調後結束的回調,從線程ID能夠看到這個回調已經回到了主線程中執行
I am after work callback, calling from event loop thread, pid=>90714
after_work_cb thread id 0x7fff8c2d9380
// 這個是uv_async_init的回調,其觸發是由於在work callback中執行了uv_async_send,能夠從0x700001266000獲得驗證,該回調也是在主線程中執行
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
複製代碼
Poll句柄用於監視文件描述符的可讀性、可寫性和斷開鏈接,相似於poll(2)
的目的。
Poll句柄的目的是支持集成外部庫,這些庫依賴於事件循環來通知套接字狀態的更改,好比c-ares
或libssh2
。不建議將uv_poll_t用於任何其餘目的;由於像uv_tcp_t
、uv_udp_t
等提供了一個比uv_poll_t
更快、更可伸縮的實現,尤爲是在Windows上。
可能輪詢處理偶爾會發出信號,代表文件描述符是可讀或可寫的,即便它不是。所以,當用戶試圖從fd讀取或寫入時,應該老是準備再次處理EAGAIN錯誤或相似的EAGAIN錯誤。
同一個套接字不能有多個活躍的Poll句柄,由於這可能會致使libuv出現busyloop
或其餘故障。
當活躍的Poll句柄輪詢文件描述符時,用戶不該關閉該文件描述符。不然可能致使句柄報告錯誤,但也可能開始輪詢另外一個套接字。可是,能夠在調用uv_poll_stop()
或uv_close()
以後當即安全地關閉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
};
複製代碼
Signal句柄在每一個事件循環的基礎上實現Unix風格的信號處理。在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
}
複製代碼
關於Signal句柄有幾個點要知悉:
raise()
或abort()
觸發的信號不會被libuv檢測到;因此這些信號不會對應的回調函數。process句柄將會新建一個新的進程而且可以容許用戶控制該進程並使用流去創建通訊通道。對應的demo能夠查看:process.c,值得注意的是,args
中提供的結構體的第一個參數path指的是可執行程序的路徑,好比在demo中:
const char* exepath = exepath_for_process();
char *args[3] = { (char*) exepath, NULL, NULL };
複製代碼
實例中的exepath是:FsHandle
的執行路徑。
另一個注意點就是父子進程的std的配置,demo中提供了一些參考,若是使用管道的話還能夠參考另一個demo:pipe
流句柄提供了雙工通訊通道的抽象。uv_stream_t
是一種抽象類型,libuv以uv_tcp_t
、uv_pipe_t
和uv_tty_t
的形式提供了3種流實現。這個沒有具體實例。可是libuv有好幾個方法的入參都是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)
複製代碼
tcp句柄能夠用來表示TCP流和服務器。上小節說到的uv_stream_t
是uv_tcp_t
的」父類「,這裏使用結構體繼承的方式實現,uv_handle_t
、uv_stream_t
、uv_tcp_t
三者的結構關係以下圖:
使用libuv建立tcp服務器的步驟能夠概括爲:
一、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
二、綁定地址:uv_tcp_bind
三、監聽鏈接:uv_listen
四、每當有一個鏈接進來以後,調用uv_listen的回調,回調裏要作以下事情:
4.一、初始化客戶端的tcp句柄:uv_tcp_init()
4.二、接收該客戶端的鏈接:uv_accept()
4.三、開始讀取客戶端請求的數據:uv_read_start()
4.四、讀取結束以後作對應操做,若是須要響應客戶端數據,調用uv_write,回寫數據便可。
複製代碼
更多細節參考demo
Pipe句柄在Unix上提供了對本地域套接字的抽象,在Windows上提供了命名管道。它是uv_stream_t
的「子類」。管道的用途不少,能夠用來讀寫文件,還能夠用來作線程間的通訊。咱們在實例中用來實現主線程與多個子線程的互相通訊。實現的模型是這樣的:
從模型中能夠看出,咱們利用管道將客戶端的鏈接綁定到隨機的一個線程上,以後的操做都是該線程和客戶端的通訊。
TTY句柄表示控制檯的一種流,用的比較少,就很少說了~
UDP句柄爲客戶端和服務器封裝UDP通訊。使用libuv建立udp服務器的步驟能夠歸納爲:
一、初始化接收端的uv_udp_t: uv_udp_init(loop, &receive_socket_handle)
二、綁定地址:uv_udp_bind
三、開始接收消息:uv_udp_recv_start
四、uv_udp_recv_start裏執行回調,可使用下面方法回寫數據發送給客戶端
4.一、uv_udp_init初始化send_socket_handle
4.二、uv_udp_bind綁定發送者的地址,地址能夠從recv獲取
4.三、uv_udp_send發送指定消息
複製代碼
若是是官方文檔給出的示例的話,那麼會使用uv_udp_set_broadcast
設置廣播的地址。具體能夠參考udp
FS事件句柄容許用戶監視一個給定的路徑的更新事件,例如,若是文件被重命名或其中有一個通用更改。這個句柄使用每一個平臺上最佳的解決方案。
FS輪詢句柄容許用戶監視給定的更改路徑。與uv_fs_event_t
不一樣,fs poll句柄使用stat
檢測文件什麼時候發生了更改,這樣它們就能夠在不支持fs事件句柄的文件系統上工做。
那麼接下去就說到Request這個短生命週期的概念,中文翻譯爲」請求「,相似於nodejs中的req,它也是一個結構體。仍是以上述的tcp服務器爲例子,有這麼一段代碼:
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");
}
複製代碼
當客戶端鏈接失敗,須要關閉掉這個鏈接,因而咱們就會初始化一個request
,而後傳遞給咱們須要請求的操做,這裏是關閉請求shutdown
。
關於libuv提供的句柄和request,我這裏整理一張思惟導圖,能夠一看:
libuv的Request操做對比於句柄,仍是比較少的。上圖把每個request的使用說明都講得一清二楚了。咱們能作的就是隨時翻閱這篇文章便可。
uv_request_t
是基本的request,其餘任何request都是基於該結構進行擴展,它定義的全部api其餘request均可以使用。和uv_handle_t
同樣的功效。
接着說說Libuv提供的三種運行模式:
ok,限於篇幅,libuv的基礎篇仍未結束,你能夠點我繼續閱讀第二篇,也能夠先本身消化消化~