本作品采用知識共享署名 4.0 國際許可協議進行許可。轉載保留聲明頭部與原文鏈接https://luzeshu.com/blog/nodesource6
本博客同步在https://cnodejs.org/topic/5716137fe84805cd5410ea21
本博客同步在http://www.cnblogs.com/papertree/p/5398008.html
我們在第3-5篇博客講了js代碼如何調用到C++接口的機制,其中暗含的require、process.binding這些過程。
這篇博客以server.listen(80)為例,講這兩點:
1. js代碼深入、作用到libuv事件循環的過程【1.1節的問題2】
2. libuv事件循環本身的過程【1.1節的問題3】
6.1 js到事件循環 —— 數據結構
6.1.1事件循環的核心數據結構 —— struct uv_loop_s default_loop_struct;
還記得2.2節的流程圖嗎,js代碼里面執行網絡io操作,最終保存一個io觀察者到default_loop_struct,在node進入事件循環的時候,再獲取io觀察者進行監聽。
來看看struct uv_loop_s 的結構體定義:
圖6-1-1
在這篇博客里主要關系的是watcher_queue、watchers、nwatchers、nfds這四個成員。
watcher_queue:io觀察者鏈表,鏈表原理看6.4節。
watchers:是一個uv__io_t 類型的二級指針。這里維護的是一個io觀察者映射表【實際是以fd為下標索引的數組】。
nwatchers:watchers數組的size,因為是堆分配的動態數組,所以需要維護數組的長度。
nfds:監聽了多少個fd,不同於nwatchers,因為watchers里面很多元素是空的。
【注:c語言里面經常會有 “typedef struct uv_loop_s uv_loop_t”、“typedef struct uv__io_s uv__io_t”這種寫法去給結構體類型起別名,這樣的好處是用uv_loop_s去定義一個變量需要加上struct,而通過typedef的別名不用,比如:
struct uv_loop_s default_loop_struct; uv_loop_t default_loop_struct;
這兩種寫法是一樣的。】
6.1.2 io觀察者結構體 —— struct uv__io_s
6.1.1中看到,我們的網絡io操作最終會封裝成一個io觀察者,保存到default_loop_struct的io觀察者映射表——watchers 里面。
來看一下封裝的io觀察者的定義:
圖6-1-2
可以看到一個io觀察者封裝了:
fd:文件描述符,操作系統對進程監聽的網絡端口、或者打開文件的一個標記
cb:回調函數,當相應的io觀察者監聽的事件被激活之后,被libuv事件循環調用的回調函數
events:交給libuv的事件循環(epoll_wait)進行監聽的事件
6.1.3 持有io觀察者的結構體 —— 比如struct uv_tcp_s
io觀察者結構體(uv__io_s) 是我們調用server.listen()之后,與libuv事件循環的交互數據。
事件循環數據結構default_loop_struct 維護uv__io_s的映射表 —— watchers成員。
而用戶的每一個io操作流程,最終也通過某個結構體來持有這個io觀察者。比如當進行tcp的 io操作時,其對應的io觀察者,由uv_tcp_s 結構體的 io_watcher成員持有:
圖6-1-3
6.2 js到事件循環 —— 流程
6.1節講了幾個結構體和數據類型。這一節以這幾行示例代碼,介紹從js代碼的io操作到保存io觀察者的流程:
var http = require('http'); function requestListener(req, res) { res.end('hello world'); } var server = http.createServer(requestListener); server.listen(80);
代碼6-2-1
其實這里http模塊里面做的事情很簡單,6-2-1示例代碼等效於:
const Server = require('_http_server').Server; function requestListener(req, res) { res.end('hello world'); } var server = new Server(requestListener); server.listen(80);
代碼6-2-2
面向用戶的接口僅僅是一個requestListener回調函數、監聽端口,那么調用server.listen(80)之后,經過多少個環節才形成一個io觀察者?io觀察者的回調函數被調用之后,又經過多少個環節才回調到用戶的requestListener?
來看下有多少層:
6.2.1 http層Server類 —— lib/_http_server.js
上述示例代碼直接交互的是http Server類,看代碼:
圖6-2-1
A. 設置環節 —— requestListener
當用戶new Server產生一個server對象時,server添加'request'事件監聽器。
B. 回調環節 —— connectionListener
可以看到http層的Server類繼承了socket層(net.js)的Server類。並添加'connection'事件監聽器,當有連接到來時,由socket層的Server類發射'connection'事件,http層connectionListener被調用,拿到來自socket層的一個socket對象,進行跟http協議相關的處理,把http請求相關的數據封裝成req、res兩個對象,emit 'request'事件,把req、res傳給用戶的requestListener回調函數。
6.2.2 socket層Server類 —— lib/net.js
net.Server是負責socket層的Server類,也是http.Server的基類:
圖6-2-2
A. listen環節 —— 'connection'事件
在執行listen操作時,socket層Server類給self._handle.onconnection賦上回調函數。self._handle是更下層的TCP類對象。
圖6-2-3
B. 回調環節 —— onconnection函數
當有連接到來時,底層回調了TCP類的onconnection函數(self._handle.onconnection),並傳過來一個clientHandle,onconnection把clientHandle封裝成socket對象,並發射'connection'事件,把socket傳給上層的connectionListener監聽器。
6.2.3 node C++層TCP類 —— src/tcp_wrap.cc
上面說到socket層的Server類與下層的交互是通過this._handle —— TCP類對象。【注意了TCP不是C++本身的類,而是C++用來表示js類的 FunctionTemplate】
圖6-2-4
A. listen環節 —— TCPWrap::OnConnection
看到TCP這一層,執行listen時傳給下層的回調函數是TCPWrap::OnConnection,而且可以看到與這一層交互的下一層就是libuv的接口了 —— uv_listen。
B. 回調環節 —— onconnection
上面講到socket層Server類通過self._handle.onconnection = onconnection去設置回調函數。
這一層可以看到onconnection函數在TCPWrap::OnConnection里面通過tcp_wrap->MakeCallback去回調。
關於MakeCallback的實現在AsyncWrap類 —— TCPWrap的基類:
圖6-2-5
這里有一行重要的代碼 env() -> tick_callback_function() -> Call()。里面確保了當每次從C++陷入js領域、執行完js代碼之后,會執行到諸如process.nextTick()設置的回調函數。
通過2.2節我們可以知道,執行js代碼只有兩個時機:
1. 剛啟動的時候執行app.js文件
2. 異步回調函數被觸發(注意回調函數有可能是被同步回調的)
那么這里的AsyncWrap::MakeCallback()就是每次執行js異步回調函數時,從C++域陷入js域的位置。
6.2.4 libuv層 uv_tcp_t結構體 —— deps/uv/src/unix/tcp.c
在app.js里面的server.listen(80),通過http.Server -> net.Server -> TCPWrap,終於到達了libuv層。這一層,我們看到6.1節的數據結構的使用細節。關於io觀察者如何被保存、如何被事件循環取出使用的細節,我們看6.3節。
圖6-2-6
看到uv_tcp_listen操作,通過調用uv__io_start 把自身的io_watcher(定義在6.1.2節)注冊進tcp->loop(理解成6.1.1節里面的default_loop_struct —— 事件循環的數據結構)。
這里注意到,從上層傳過來的cb(TCPWrap::OnConnection)保存在了tcp->connection_cb,而tcp->io_watcher.cb 保存的是 uv__server_io。
當有連接到來時,事件循環直接調用的cb是io_watcher里面的uv__server_io,里面先執行uv__accept等操作,再回調到stream->connection_cb。【注意到右邊文件的stream->connection_cb實際上就是左邊文件的tcp->connection_cb,uv_stream_t可以理解成uv_tcp_t的一個基類】
6.3 事件循環與io觀察者
6.3.1 io觀察者的保存
6.2.4節講到libuv層封裝了io觀察者,通過uv__io_start,把io觀察者保存到指定的事件循環數據結構 —— loop。來看看uv__io_start的細節:
圖6-3-1
這里的loop就是6.1.1節中的事件循環數據結構體,w就是6.1.2節中的io觀察者結構體。
可以看到,添加一個io觀察者需要兩步操作:
1. 使用QUEUE_INSERT_TAIL 往loop->watcher_queue 添加io觀察者,鏈表原理看6.4節。
2. 把io觀察者保存在loop->watchers中 —— 以fd為索引的數組。loop->watchers實際上類似於映射表的功能,而不是觀察者隊列。
6.3.2 事件循環的核心 —— io觀察者的取出與回調
在2.2節的運行流程中知道事件循環最終調用了uv_run()進入了epoll_wait()等待,而uv_run的這個事件循環是調用了uv__io_poll(),那么來看看這個最終的循環:
圖6-3-2
通過2.2節的運行流程,我們知道在js代碼里面添加一個io觀察者(比如調用server.listen())是先通過保存io觀察者(uv__io_t 結構體)到uv_loop_t結構體的watcher_queue里面,而不是馬上注冊到epoll_wait()進行監聽的。
當js代碼執行完畢,進入C++域,再進入到uv__io_poll的時候,就需要這幾個步驟:
1. 遍歷 loop->watcher_queue,取出所有io觀察者,這里取出的w就是圖6-3-1中調用uv__io_start保存的io觀察者 —— w。
2. 取出了w之后,調用epoll_ctl(),把w->fd(io觀察者對應的fd)注冊給系統的epoll機制,那么epoll_wait()時就監聽對應的fd。
3. 當epoll_wait()返回了,拿出有事件到來的fd,這個時候loop->watchers 映射表就起到作用了,通過fd拿出對應的io觀察者 —— w,調用w->cb()。
6.3.3 setTimeout —— epoll_wait的timeout
看到epoll_wait有個timeout參數,這里正是setTimeout的原理。試想一下,epoll_wait所監聽的所有io觀察者對應的fd都沒有事件觸發,而setTimeout所設置的timeout到達了,那么epoll_wait()也是需要返回,讓setTimeout的回調函數能夠得以運行的。
6.4 io觀察者鏈表
注意到4個點:
1. uv_loop_t 結構體的io觀察者鏈表是void* [2]類型的watcher_queue來維護。
2. uv__io_t(io觀察者) 結構體也擁有一個void* watcher_queue[2]。
3. 在uv__io_start里面,通過QUEUE_INSERT_TAIL宏,往loop->watcher_queue里面添加w->watcher_queue,而不是w(io觀察者本身)。
4. 在uv__io_poll里面,通過QUEUE_HEAD宏,從loop->watcher_queue里面取出元素 q,這個q事實上只是w->watcher_queue字段,需要通過QUEUE_DATA宏,從q去取出w。
【這跟c語言結構體的內存模型有關,可以通過一個成員的地址減去結構體內成員的偏移量,計算出結構體的在進程空間的內存地址。這也是QUEUE_DATA宏所做的事。】
可以先來看看這幾個宏的定義:
圖6-4-1
我們來看看下面這個圖,第一個狀態是uv_loop_t和兩個uv__io_t里的watcher_queue成員執行了QUEUE_ININ之后的狀態。
第二、三個狀態是依次通過QUEUE_INSERT_TAIL宏往uv_loop_t的watcher_queue里面添加uv__io_t的watcher_queue之后的狀態。
圖6-4-2