node源碼詳解(六) —— 從server.listen 到事件循環


 知識共享許可協議本作品采用知識共享署名 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

 


免責聲明!

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



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