深入理解 Node.js 的 Inspector
Node.js提供的Inspector?非常強(qiáng)大,不僅可以用來調(diào)試Node.js代碼,還可以實(shí)時(shí)收集Node.js進(jìn)程的Heap Snapshot、Cpu Profile等數(shù)據(jù),同時(shí)支持靜態(tài)、動(dòng)態(tài)開啟,是一個(gè)非常強(qiáng)大的工具,也是我們調(diào)試和診斷Node.js進(jìn)程非常好的方式。本文從使用和原理詳細(xì)講解Node.js的Inspector。
Node.js 的文檔中對(duì) Inspector 的描述很少,但是如果深入探索,其實(shí)里面的內(nèi)容還是挺多的。我們先看一下 Inspector 的使用。
1 Inspector 的使用
1.1 ?本地調(diào)試
我們先從一個(gè)例子開始,下面是一個(gè)簡單的 HTTP 服務(wù)器。
const?http?=?require('http');
http.createServer((req,?res)?=>?{
????res.end('ok');
}).listen(80);
然后我們以 node --inspect httpServer.js 的方式啟動(dòng)。我們可以看到以下輸出。
Debugger?listening?on?ws://127.0.0.1:9229/fbbd9d8f-e088-48cc-b1e0-e16bfe58db44
For?help,?see:?https://nodejs.org/en/docs/inspector
9229 端口是 Node.js 默認(rèn)選擇的端口,當(dāng)然我們也可以自定義,具體可參考 Node.js 官方文檔。這時(shí)候我們?nèi)g覽器打開開發(fā)者工具,菜單欄多了一個(gè)調(diào)試 Node.js 的按鈕。

點(diǎn)擊這個(gè)按鈕。我們可以看到以下界面(點(diǎn)擊切換到 Sources Tab)。

我們可以選擇某一行代碼打斷點(diǎn),比如我在第三行,這時(shí)候我們訪問 80 端口,開發(fā)者工具就會(huì)停留在斷點(diǎn)處。這時(shí)候我們可以看到一些執(zhí)行上下文。

1.2 遠(yuǎn)程調(diào)試
但很多時(shí)候我們可能需要遠(yuǎn)程調(diào)試。比如我在一臺(tái)云服務(wù)器上部署以上服務(wù)器代碼。然后執(zhí)行
node?--inspect=0.0.0.0:8888?httpServer.js
我們打開開發(fā)者工具發(fā)現(xiàn)按鈕置灰或者找不到我們遠(yuǎn)程服務(wù)器的信息。這時(shí)候我們需要用另一種方式,通過在瀏覽器url輸入框輸入:
devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws={host}:{port}/{path}?
的方式(替換 {} 里面的內(nèi)容為你執(zhí)行 Node.js 時(shí)輸出的信息),瀏覽器就會(huì)去連接指定的地址,比如執(zhí)行上面的命令輸出的是 ws://0.0.0.0:8888/f6e42278-d915-48dc-af4d-453a23d330ab,假設(shè)公網(wǎng)IP是 1.1.1.1。那么最后瀏覽器url輸入框里就填入 devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=1.1.1.1:8888/f6e42278-d915-48dc-af4d-453a23d330ab 就可以開始調(diào)試了,這種方式比較適合于常用的場景。
1.3 自動(dòng)探測
如果是我們自己調(diào)試的話,1.2 這種方式看起來就有點(diǎn)麻煩,我們可以使用瀏覽器提供的自動(dòng)探測功能。
URL 輸入框輸入 chrome://inspect/#devices我們會(huì)看到以下界面

點(diǎn)擊 configure按鈕,在彈出的彈框里輸入你遠(yuǎn)程服務(wù)器的地址

配置完畢后,我們會(huì)看到界面變成這樣了(或者打開新的 Tab,我們看到開發(fā)者工具的調(diào)試按鈕也變亮了)。

這時(shí)候我們點(diǎn)擊 inspect按鈕、Open dedicated DevTools for Node按鈕或者打開新 Tab 的開發(fā)者工具,就可以開始調(diào)試,而且還可以調(diào)試Node.js的原生 JS 模塊。

1.4 收集數(shù)據(jù)
V8 Inspector 是一個(gè)非常強(qiáng)大的工具,調(diào)試只是它其中一個(gè)能力,他還可以獲取 Heap Snapshot、CPU?Profile 等數(shù)據(jù),具體能力請(qǐng)參考文章后面列出的指令文檔和 Chrome Dev Tools。
收集 Cpu Profile 信息

獲取 Heap Snapshop

1.5 動(dòng)態(tài)開啟 Inspector
默認(rèn)打開 Inspector 能力是不安全的,這意味著能連上服務(wù)器的客戶端都能通過協(xié)議控制 Node.js 進(jìn)程(雖然 URL 并不容易猜對(duì)),通常我們是在 Node.js 進(jìn)程出現(xiàn)問題的時(shí)候,動(dòng)態(tài)開啟 Inspector,我們看一下下面的例子。
const?inspector?=?require('inspector');
const?http?=?require('http');
let?isOpend?=?false;
function?getHTML()?{
????return?`
??????
??????
????????復(fù)制到新?Tab?打開該?URL?開始調(diào)試?devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=${inspector.url().replace("ws://",?'')}
??????
????`;
}
http.createServer((req,?res)?=>?{
??if?(req.url?==?'/debug/open')?{
????????//?還沒開啟則開啟
????????if?(!isOpend)?{
??????????isOpend?=?true;
??????????//?打開調(diào)試器
??????????inspector.open();
????????}
????????//?返回給前端的內(nèi)容
????????const?html?=?getHTML()?;
????????res.end(html);
??}?else?if?(req.url?==?'/debug/close')?{
????????//?如果開啟了則關(guān)閉
????????if?(isOpend)?{
??????????inspector.close();
??????????isOpend?=?false;
????????}?
????????res.end('ok');
??}?else?{
????res.end('ok');
??}
}).listen(80);
當(dāng)我們需要調(diào)試的時(shí)候,通過訪問 /debug/open 打開調(diào)試器。前端界面可以看到以下輸出。
復(fù)制到新?Tab?打開該?URL?開始調(diào)試?devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/9efd4c80-956a-4422-b23c-4348e6613304
接著新開一個(gè) Tab,然后復(fù)制上面的 URL,粘貼到瀏覽器 URL 地址欄訪問,我們就可以看到調(diào)試頁面。

然后打個(gè)斷點(diǎn),接著新開一個(gè) Tab 訪問 http://localhost 就可以進(jìn)入調(diào)試,調(diào)試完成后訪問 /debug/close 關(guān)閉調(diào)試器。瀏覽器界面就會(huì)顯示斷開連接了。

以上方式支持調(diào)試和收集數(shù)據(jù),如果我們只是需要收集數(shù)據(jù),還有另一種動(dòng)態(tài)開啟 Inspector 的方式
const?http?=?require('http');
const?inspector?=?require('inspector');
const?fs?=?require('fs');
function?getCpuprofile(req,?res)?{
????//?打開一個(gè)和?V8?Inspector?的會(huì)話
????const?session?=?new?inspector.Session();
????session.connect();
????//?向V8?Inspector?提交命令,開啟?Cpu?Profile?并收集數(shù)據(jù)
????session.post('Profiler.enable',?()?=>?{
????session.post('Profiler.start',?()?=>?{
??????//?收集一段時(shí)間后提交停止收集命令
??????setTimeout(()?=>?{
????????session.post('Profiler.stop',?(err,?{?profile?})?=>?{
??????????//?把數(shù)據(jù)寫入文件
??????????if?(!err)?{
????????????fs.writeFileSync('./profile.cpuprofile',?JSON.stringify(profile));
??????????}
??????????//?斷開會(huì)話
??????????session.disconnect();
??????????//?回復(fù)客戶端
??????????res.end('ok');
????????});
??????},?3000)
????});
??});
}
http.createServer((req,?res)?=>?{
??if?(req.url?==?'/debug/getCpuprofile')?{
????????getCpuprofile(req,?res);
??}?else?{
????????res.end('ok');
??}
}).listen(80);
我們可以通過 Inspector Session 的能力,實(shí)時(shí)和 V8 Inspector 交互而不需要啟動(dòng)一個(gè) WebSocket 服務(wù)。本地調(diào)試時(shí)還可以在 VSCode 里點(diǎn)擊 Profile 文件直接看到效果。

2 Inspector 調(diào)試的原理
下面以通過 URL 的方式調(diào)試(可以看到 Network ),來看看調(diào)試的時(shí)候都發(fā)生了什么,瀏覽器和遠(yuǎn)程服務(wù)器建立連接后,是通過 WebSocket 協(xié)議通信的,下面是一次通信的信息。

我們看一下這命令是什么意思(具體可以參考 Inspector 協(xié)議文檔)。
Debugger.scriptParsed # Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger.
從說明中我們看到,當(dāng) V8 解析腳本的時(shí)候就會(huì)觸發(fā)這個(gè)事件,告訴瀏覽器相關(guān)的信息。

我們發(fā)現(xiàn)返回的都是一些元數(shù)據(jù),沒有腳本的具體代碼內(nèi)容,這時(shí)候?yàn)g覽器會(huì)再次發(fā)起請(qǐng)求(點(diǎn)擊對(duì)應(yīng)腳本對(duì)應(yīng)的 JS 文件時(shí)),

我們看到這個(gè)腳本的 scriptId 是 103。所以請(qǐng)求里帶了這個(gè) scriptId。對(duì)應(yīng)的請(qǐng)求 id 是 11。接著看一下響應(yīng)。

至此,我們了解了獲取腳本內(nèi)容的過程,然后我們看看調(diào)試的時(shí)候是怎樣的過程。當(dāng)我們在瀏覽器上點(diǎn)擊某一行設(shè)置斷點(diǎn)的時(shí)候,瀏覽器就會(huì)發(fā)送一個(gè)請(qǐng)求。

這個(gè)命令的意義顧名思義,我們看一下具體定義:
Debugger.setBreakpointByUrl # Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this command is issued, all existing parsed scripts will have breakpoints resolved and returned in locations property. Further matching script parsing will result in subsequent breakpointResolved events issued. This logical breakpoint will survive page reloads.
接著服務(wù)返回響應(yīng)。

這時(shí)候我們從另外一個(gè) Tab 訪問 80 端口,服務(wù)器就會(huì)在我們設(shè)置的斷點(diǎn)處停留,并且通知瀏覽器。

我們看一下這個(gè)命令的意思。

這個(gè)命令就是當(dāng)服務(wù)器執(zhí)行到斷點(diǎn)時(shí)通知瀏覽器,并且返回執(zhí)行的一些上下文,比如執(zhí)行到哪個(gè)斷點(diǎn)停留了。這時(shí)候?yàn)g覽器側(cè)也會(huì)停留在對(duì)應(yīng)的地方,當(dāng)我們 hover 某個(gè)變量時(shí),就會(huì)看到對(duì)應(yīng)的上下文。這些都是通過具體的命令獲取的數(shù)據(jù)。就不一一分析了。

3 Node.js Inspector 的實(shí)現(xiàn)
大致了解了瀏覽器和服務(wù)器的交互過程和協(xié)議后,我們再來深入了解一下關(guān)于 Inspector 的一些實(shí)現(xiàn)。當(dāng)然這里不是分析 V8 中 Inspector 的實(shí)現(xiàn),而是分析如何使用 V8 的 Inspector 以及 Node.js 中關(guān)于 Inspector 的實(shí)現(xiàn)部分。
當(dāng)我們以以下方式執(zhí)行應(yīng)用時(shí)
node?--inspect?app.js
3.1 初始化
Node.js 在啟動(dòng)的過程中,就會(huì)初始化 Inspector 相關(guān)的邏輯。
inspector_agent_?=?std::make_unique(this);
Agent 是負(fù)責(zé)和 V8 Inspector 通信的對(duì)象,創(chuàng)建完后接著執(zhí)行 env->InitializeInspector({}) 啟動(dòng) Agent。
inspector_agent_->Start(...);
Start 繼續(xù)執(zhí)行 Agent::StartIoThread。
bool?Agent::StartIoThread()?{
??io_?=?InspectorIo::Start(client_->getThreadHandle(),?...);
??return?true;
}
StartIoThread 中的 client_->getThreadHandle() 是重要的邏輯,我們先來分析該函數(shù)。
std::shared_ptr?getThreadHandle()? {
????if?(!interface_)?{
??????interface_?=?std::make_shared(env_->inspector_agent(),?...);
????}
????return?interface_->GetHandle();
}
getThreadHandle 首先創(chuàng)建來一個(gè) MainThreadInterface 對(duì)象,接著又調(diào)用了他的 GetHandle 方法,我們看一下該方法的邏輯。
std::shared_ptr?MainThreadInterface::GetHandle()? {
??if?(handle_?==?nullptr)
????handle_?=?std::make_shared(this);
??return?handle_;
}
GetHandle 了創(chuàng)建了一個(gè) MainThreadHandle 對(duì)象,最終結(jié)構(gòu)如下所示。

分析完后我們繼續(xù)看 Agent::StartIoThread 中 InspectorIo::Start 的邏輯。
std::unique_ptr?InspectorIo::Start(std::shared_ptr?main_thread,?...) ? {
??auto?io?=?std::unique_ptr(new?InspectorIo(main_thread,?...));
??return?io;
}
InspectorIo::Star 里新建了一個(gè) InspectorIo 對(duì)象,我們看看 InspectorIo 構(gòu)造函數(shù)的邏輯。
InspectorIo::InspectorIo(std::shared_ptr?main_thread,?...)
????:?
????//?初始化?main_thread_
????main_thread_(main_thread))?{
??//?新建一個(gè)子線程,子線程中執(zhí)行?InspectorIo::ThreadMain
??uv_thread_create(&thread_,?InspectorIo::ThreadMain,?this);
}
這時(shí)候結(jié)構(gòu)如下:

InspectorIo 創(chuàng)建了一個(gè)子線程, Inspector 在子線程里啟動(dòng)的原因主要有兩個(gè)。
如果在主線程里運(yùn)行,那么當(dāng)我們斷點(diǎn)調(diào)試的時(shí)候, Node.js主線程就會(huì)被停住,也就無法處理客戶端發(fā)過來的調(diào)試指令。如果主線程陷入死循環(huán),我們就無法實(shí)時(shí)抓取進(jìn)程的 Profile數(shù)據(jù)來分析原因。
接著繼續(xù)看一下子線程里執(zhí)行 InspectorIo::ThreadMain 的邏輯:
void?InspectorIo::ThreadMain(void*?io)?{
??static_cast(io)->ThreadMain();
}
void?InspectorIo::ThreadMain()?{
??uv_loop_t?loop;
??loop.data?=?nullptr;
??//?在子線程開啟一個(gè)新的事件循環(huán)
??int?err?=?uv_loop_init(&loop);
??std::shared_ptr?queue(new?RequestQueueData(&loop),?...) ;
??//?新建一個(gè)?delegate,用于處理請(qǐng)求
??std::unique_ptr?delegate(
??????new?InspectorIoDelegate(queue,?main_thread_,?...)
??) ;
??InspectorSocketServer?server(std::move(delegate),?...);
??server.Start();
??//?進(jìn)入事件循環(huán)
??uv_run(&loop,?UV_RUN_DEFAULT);
}
ThreadMain?主要有三個(gè)邏輯:
創(chuàng)建一個(gè) delegate 對(duì)象,該對(duì)象是核心的對(duì)象,后面我們會(huì)看到有什么作用。 創(chuàng)建一個(gè)服務(wù)器并啟動(dòng)。 開啟事件循環(huán)。
接下來看一下服務(wù)器的邏輯,首先看一下創(chuàng)建服務(wù)器的邏輯:
InspectorSocketServer::InspectorSocketServer(std::unique_ptr?delegate,?...)
????:?//?保存?delegate
??????delegate_(std::move(delegate)),
??????//?初始化?sessionId
??????next_session_id_(0)?{
??//?設(shè)置?delegate?的?server?為當(dāng)前服務(wù)器
??delegate_->AssignServer(this);
}
執(zhí)行完后形成以下結(jié)構(gòu):

接著我們看啟動(dòng)服務(wù)器的邏輯:
bool?InspectorSocketServer::Start()?{
??//?DNS?解析,比如輸入的是localhost
??struct?addrinfo?hints;
??memset(&hints,?0,?sizeof(hints));
??hints.ai_flags?=?AI_NUMERICSERV;
??hints.ai_socktype?=?SOCK_STREAM;
??uv_getaddrinfo_t?req;
??const?std::string?port_string?=?std::to_string(port_);
??uv_getaddrinfo(loop_,?&req,?nullptr,?host_.c_str(),
???????????????????????????port_string.c_str(),?&hints);
??//?監(jiān)聽解析到的?IP?列表?????????????????
??for?(addrinfo*?address?=?req.addrinfo;?
?????????address?!=?nullptr;
???????address?=?address->ai_next)?{
????auto?server_socket?=?ServerSocketPtr(new?ServerSocket(this));
????err?=?server_socket->Listen(address->ai_addr,?loop_);
????if?(err?==?0)
??????server_sockets_.push_back(std::move(server_socket));
??}
??return?true;
}
首先根據(jù)參數(shù)做 DNS 解析,然后根據(jù)拿到的 IP 列表(通常是一個(gè)),創(chuàng)建對(duì)應(yīng)個(gè)數(shù)的 ServerSocket 對(duì)象,并執(zhí)行它的 Listen 方法。ServerSocket 表示一個(gè)監(jiān)聽 socket,看一下 ServerSocket 的構(gòu)造函數(shù):
ServerSocket(InspectorSocketServer*?server)?:?
????tcp_socket_(uv_tcp_t()),?server_(server)?{}
執(zhí)行完后結(jié)構(gòu)如下:

接著看一下 ServerSocket 的 Listen 方法:
int?ServerSocket::Listen(sockaddr*?addr,?uv_loop_t*?loop)?{
??uv_tcp_t*?server?=?&tcp_socket_;
??uv_tcp_init(loop,?server)
??uv_tcp_bind(server,?addr,?0);
??uv_listen(reinterpret_cast<uv_stream_t*>(server),?
????????????511,
????????????ServerSocket::SocketConnectedCallback);
}
Listen 調(diào)用 Libuv 的接口完成服務(wù)器的啟動(dòng)。至此,Inspector 提供的 Weboscket 服務(wù)器啟動(dòng)了。
3.2 處理連接
從剛才分析中可以看到,當(dāng)有連接到來時(shí)執(zhí)行回調(diào) ServerSocket::SocketConnectedCallback。
void?ServerSocket::SocketConnectedCallback(uv_stream_t*?tcp_socket,
???????????????????????????????????????????int?status)?{
??if?(status?==?0)?{
????//?根據(jù)?Libuv?handle?找到對(duì)應(yīng)的?ServerSocket?對(duì)象
????ServerSocket*?server_socket?=?ServerSocket::FromTcpSocket(tcp_socket);
????//?Socket?對(duì)象的?server_?字段保存了所在的?InspectorSocketServer
????server_socket->server_->Accept(server_socket->port_,?tcp_socket);
??}
}
接著看 InspectorSocketServer 的 Accept 是如何處理連接的:
void?InspectorSocketServer::Accept(int?server_port,
???????????????????????????????????uv_stream_t*?server_socket)?{
??std::unique_ptr?session(
??????new?SocketSession(this,?next_session_id_++,?server_port)
??) ;
??InspectorSocket::DelegatePointer?delegate?=
??????InspectorSocket::DelegatePointer(
??????????new?SocketSession::Delegate(this,?session->id())
??????);
??InspectorSocket::Pointer?inspector?=
??????InspectorSocket::Accept(server_socket,?std::move(delegate));
??if?(inspector)?{
????session->Own(std::move(inspector));
????connected_sessions_[session->id()].second?=?std::move(session);
??}
}
Accept 的首先創(chuàng)建里一個(gè) SocketSession 和 SocketSession::Delegate 對(duì)象。然后調(diào)用 InspectorSocket::Accept,從代碼中可以看到 InspectorSocket::Accept 會(huì)返回一個(gè) InspectorSocket 對(duì)象。InspectorSocket 是對(duì)通信 socket 的封裝(和客戶端通信的 socket,區(qū)別于服務(wù)器的監(jiān)聽 socket)。然后記錄 session 對(duì)象對(duì)應(yīng)的 InspectorSocket 對(duì)象,同時(shí)記錄 sessionId 和 session 的映射關(guān)系。結(jié)構(gòu)如下圖所示:

接著看一下 InspectorSocket::Accept 返回 InspectorSocket 的邏輯:
InspectorSocket::Pointer?InspectorSocket::Accept(uv_stream_t*?server,
?????????????????????????????????????????????????DelegatePointer?delegate)?{
??auto?tcp?=?TcpHolder::Accept(server,?std::move(delegate));
??InspectorSocket*?inspector?=?new?InspectorSocket();
??inspector->SwitchProtocol(new?HttpHandler(inspector,?std::move(tcp)));
??return?InspectorSocket::Pointer(inspector);
}
InspectorSocket::Accept 的代碼不多,但是邏輯還是挺多的:
InspectorSocket::Accept再次調(diào)用TcpHolder::Accept獲取一個(gè)TcpHolder對(duì)象。
TcpHolder::Pointer?TcpHolder::Accept(
????uv_stream_t*?server,
????InspectorSocket::DelegatePointer?delegate)?{
????
??//?新建一個(gè)?TcpHolder?對(duì)象,TcpHolder?是對(duì)?uv_tcp_t?和?delegate?的封裝
??TcpHolder*?result?=?new?TcpHolder(std::move(delegate));
??//?拿到?TcpHolder?對(duì)象的?uv_tcp_t?結(jié)構(gòu)體
??uv_stream_t*?tcp?=?reinterpret_cast<uv_stream_t*>(&result->tcp_);
??//?初始化
??int?err?=?uv_tcp_init(server->loop,?&result->tcp_);
??//?摘取一個(gè)?TCP?連接對(duì)應(yīng)的?fd?保存到?TcpHolder?的?uv_tcp_t?結(jié)構(gòu)體中(即第二個(gè)參數(shù)的?tcp?字段)
??uv_accept(server,?tcp);
??//?注冊等待可讀事件,有數(shù)據(jù)時(shí)執(zhí)行?OnDataReceivedCb?回調(diào)
??uv_read_start(tcp,?allocate_buffer,?OnDataReceivedCb);
??return?TcpHolder::Pointer(result);
}
新建一個(gè) HttpHandler對(duì)象:
explicit?HttpHandler(InspectorSocket*?inspector,?TcpHolder::Pointer?tcp)
?????????????????????:?ProtocolHandler(inspector,?std::move(tcp)){
??llhttp_init(&parser_,?HTTP_REQUEST,?&parser_settings);
??llhttp_settings_init(&parser_settings);
??parser_settings.on_header_field?=?OnHeaderField;
??//?...
}
ProtocolHandler::ProtocolHandler(InspectorSocket*?inspector,
?????????????????????????????????TcpHolder::Pointer?tcp)
?????????????????????????????????:?inspector_(inspector),?tcp_(std::move(tcp))?{
??//?設(shè)置?TCP?數(shù)據(jù)的?handler,TCP?是只負(fù)責(zé)傳輸,數(shù)據(jù)的解析交給?handler?處理???????????????????????????????
??tcp_->SetHandler(this);
}
HttpHandler 是對(duì) TcpHolder 的封裝,主要通過 HTTP 解析器 llhttp 對(duì) HTTP 協(xié)議進(jìn)行解析。
調(diào)用 inspector->SwitchProtocol() 切換當(dāng)前協(xié)議處理器為 HTTP,建立 TCP 連接后,首先要經(jīng)過一個(gè) HTTP 請(qǐng)求從 HTTP 協(xié)議升級(jí)到 WebSocket 協(xié)議,升級(jí)成功后就使用 Websocket 協(xié)議進(jìn)行通信.
我們看一下這時(shí)候的結(jié)構(gòu)圖:

至此,就完成了連接處理的分析!(撒花,你學(xué)廢了么)
3.3 協(xié)議升級(jí)
完成了 TCP 連接的處理后,接下來要完成協(xié)議升級(jí),因?yàn)?Inspector 是通過 WebSocket 協(xié)議和客戶端通信的,所以需要通過一個(gè) HTTP 請(qǐng)求來完成 HTTP 到 WebSocekt 協(xié)議的升級(jí)。從剛才的分析中看當(dāng)有數(shù)據(jù)到來時(shí)會(huì)執(zhí)行 OnDataReceivedCb 回調(diào):
void?TcpHolder::OnDataReceivedCb(uv_stream_t*?tcp,?ssize_t?nread,
?????????????????????????????????const?uv_buf_t*?buf)?{
??TcpHolder*?holder?=?From(tcp);
??holder->ReclaimUvBuf(buf,?nread);
??//?調(diào)用?handler?的?onData,目前?handler?是?HTTP?協(xié)議
??holder->handler_->OnData(&holder->buffer);
}
TCP 層收到數(shù)據(jù)后交給應(yīng)用層解析,直接調(diào)用上層的 OnData 回調(diào)。
void?OnData(std::vector<char>*?data)?override?{
????//?解析?HTTP?協(xié)議
????llhttp_execute(&parser_,?data->data(),?data->size());
????//?解析完并且是升級(jí)協(xié)議的請(qǐng)求則調(diào)用?delegate?的回調(diào)?OnSocketUpgrade
????delegate()->OnSocketUpgrade(event.host,?event.path,?event.ws_key);
}
OnData 可能會(huì)被多次回調(diào),并通過 llhttp_execute 解析收到的 HTTP 報(bào)文,當(dāng)發(fā)現(xiàn)是一個(gè)協(xié)議升級(jí)的請(qǐng)求后,就調(diào)用 OnSocketUpgrade 回調(diào)。delegate 是一個(gè) SocketSession::Delegate 對(duì)象。來看一下該對(duì)象的 OnSocketUpgrade 方法:
void?SocketSession::Delegate::OnSocketUpgrade(const?std::string&?host,
??????????????????????????????????????????????const?std::string&?path,
??????????????????????????????????????????????const?std::string&?ws_key)?{
??std::string?id?=?path.empty()???path?:?path.substr(1);
??server_->SessionStarted(session_id_,?id,?ws_key);
}
OnSocketUpgrade 又調(diào)用了 server_ (InspectorSocketServer 對(duì)象)的 SessionStarted:
void?InspectorSocketServer::SessionStarted(int?session_id,
???????????????????????????????????????????const?std::string&?id,
???????????????????????????????????????????const?std::string&?ws_key)?{
??//?找到對(duì)應(yīng)的?session?對(duì)象???????????????????????????????????????????
??SocketSession*?session?=?Session(session_id);
??connected_sessions_[session_id].first?=?id;
??session->Accept(ws_key);
??delegate_->StartSession(session_id,?id);
}
首先通過 session_id 找到建立 TCP 連接時(shí)分配的 SocketSession 對(duì)象:
執(zhí)行 session->Accept(ws_key) 回復(fù)客戶端同意協(xié)議升級(jí):
void?Accept(const?std::string&?ws_key)?{
??ws_socket_->AcceptUpgrade(ws_key);
}
從結(jié)構(gòu)圖我們可以看到 ws_socket_ 是一個(gè) InspectorSocket 對(duì)象:
void?AcceptUpgrade(const?std::string&?accept_key)?override?{
????char?accept_string[ACCEPT_KEY_LENGTH];
????generate_accept_string(accept_key,?&accept_string);
????const?char?accept_ws_prefix[]?=?"HTTP/1.1?101?Switching?Protocols\r\n"
????????????????????????????????????"Upgrade:?websocket\r\n"
????????????????????????????????????"Connection:?Upgrade\r\n"
????????????????????????????????????"Sec-WebSocket-Accept:?";
????//?...
????//?回復(fù)?101?給客戶端?????????????
????WriteRaw(reply,?WriteRequest::Cleanup);
????//?切換?handler?為?WebSocket?handler
????inspector_->SwitchProtocol(new?WsHandler(inspector_,?std::move(tcp_)));
}
AcceptUpgradeh 首先回復(fù)客戶端 101 表示同意升級(jí)到?WebSocket 協(xié)議,然后切換數(shù)據(jù)處理器為 WsHandler,即后續(xù)的數(shù)據(jù)按照 WebSocket 協(xié)議處理。
執(zhí)行 delegate_->StartSession(session_id, id)建立和V8 Inspector的會(huì)話。delegate_?是InspectorIoDelegate對(duì)象:
void?InspectorIoDelegate::StartSession(int?session_id,
???????????????????????????????????????const?std::string&?target_id)?{
??auto?session?=?main_thread_->Connect(
??????std::unique_ptr(
??????????new?IoSessionDelegate(request_queue_->handle(),?session_id)
??????),?
??????true);
??if?(session)?{
????sessions_[session_id]?=?std::move(session);
????fprintf(stderr,?"Debugger?attached.\n");
??}
}
首先通過 main_thread_->Connect 拿到一個(gè) session,并在 InspectorIoDelegate 中記錄映射關(guān)系。結(jié)構(gòu)圖如下:

接下來看一下 main_thread_->Connect 的邏輯(main_thread_ 是 MainThreadHandle 對(duì)象):
std::unique_ptr?MainThreadHandle::Connect(
????std::unique_ptr?delegate,
????bool?prevent_shutdown) ? {
??return?std::unique_ptr(
??????new?CrossThreadInspectorSession(++next_session_id_,
??????????????????????????????????????shared_from_this(),
??????????????????????????????????????std::move(delegate),
??????????????????????????????????????prevent_shutdown));
}
Connect 函數(shù)新建了一個(gè) CrossThreadInspectorSession 對(duì)象。CrossThreadInspectorSession 構(gòu)造函數(shù)如下:
?CrossThreadInspectorSession(...)?{
????//?執(zhí)行?MainThreadSessionState::Connect?????????????????????????????
????state_.Call(&MainThreadSessionState::Connect,?std::move(delegate));
?}
繼續(xù)看 MainThreadSessionState::Connect:
void?Connect(std::unique_ptr?delegate) ?{
????Agent*?agent?=?thread_->inspector_agent();
????session_?=?agent->Connect(std::move(delegate),?prevent_shutdown_);
}
繼續(xù)調(diào) agent->Connect:
std::unique_ptr?Agent::Connect(
????std::unique_ptr?delegate,
????bool?prevent_shutdown) ? {
??int?session_id?=?client_->connectFrontend(std::move(delegate),
????????????????????????????????????????????prevent_shutdown);
??return?std::unique_ptr(
??????new?SameThreadInspectorSession(session_id,?client_));
}
繼續(xù)調(diào) connectFrontend:
??int?connectFrontend(std::unique_ptr?delegate,
??????????????????????bool?prevent_shutdown) ?{
????int?session_id?=?next_session_id_++;
????channels_[session_id]?=?std::make_unique(env_,
??????????????????????????????????????????????????????????client_,
??????????????????????????????????????????????????????????getWorkerManager(),
??????????????????????????????????????????????????????????std::move(delegate),
??????????????????????????????????????????????????????????getThreadHandle(),
??????????????????????????????????????????????????????????prevent_shutdown);
????return?session_id;
??}
connectFrontend 創(chuàng)建了一個(gè) ChannelImpl 并且在 channels_ 中保存了映射關(guān)系。看看 ChannelImpl 的構(gòu)造函數(shù):
explicit?ChannelImpl(Environment*?env,
?????????????????????const?std::unique_ptr&?inspector,
?????????????????????std::unique_ptr?delegate,?...)
??????:?delegate_(std::move(delegate))?{
????session_?=?inspector->connect(CONTEXT_GROUP_ID,?this,?StringView());
}
ChannelImpl 調(diào)用 inspector->connect 建立了一個(gè)和 V8 Inspector 的會(huì)話。結(jié)構(gòu)圖大致如下:

客戶端到 Node.js 到 V8 Inspector 的整體架構(gòu)如下:

3.4 客戶端到 V8 Inspector 的數(shù)據(jù)處理
TCP 連接建立了,協(xié)議升級(jí)也完成了,接下來就可以開始處理業(yè)務(wù)數(shù)據(jù)。從前面的分析中我們已經(jīng)知道數(shù)據(jù)到來時(shí)會(huì)執(zhí)行 TcpHoldler 的 handler_->OnData 回調(diào)。因?yàn)橐呀?jīng)完成了協(xié)議升級(jí),所以這時(shí)候的 handler 變成了 WeSocket handler:
??void?OnData(std::vector<char>*?data)?override?
????int?processed?=?0;
????do?{
??????processed?=?ParseWsFrames(*data);
??????//?...
????}?while?(processed?>?0?&&?!data->empty());
??}
OnData 通過 ParseWsFrames 解析 WebSocket 協(xié)議:
int?ParseWsFrames(const?std::vector<char>&?buffer)?{
????int?bytes_consumed?=?0;
????std::vector<char>?output;
????bool?compressed?=?false;
????//?解析WebSocket協(xié)議
????ws_decode_result?r?=??decode_frame_hybi17(buffer,
??????????????????????????????????????????????true?/*?client_frame?*/,
??????????????????????????????????????????????&bytes_consumed,?&output,
??????????????????????????????????????????????&compressed);
????//?執(zhí)行delegate的回調(diào)????????????????????????????????????????
????delegate()->OnWsFrame(output);
????return?bytes_consumed;
??}
前面已經(jīng)分析過 delegate 是 TcpHoldler 的 delegate,即 SocketSession::Delegate 對(duì)象:
void?SocketSession::Delegate::OnWsFrame(const?std::vector<char>&?data)?{
??server_->MessageReceived(session_id_,
???????????????????????????std::string(data.data(),?
???????????????????????????data.size()));
}
繼續(xù)回調(diào) server_->MessageReceived。從結(jié)構(gòu)圖可以看到 server_ 是 InspectorSocketServer 對(duì)象:
void?MessageReceived(int?session_id,?const?std::string&?message)?{
??delegate_->MessageReceived(session_id,?message);
}
繼續(xù)回調(diào) delegate_->MessageReceived,InspectorSocketServer 的 delegate_ 是 InspectorIoDelegate 對(duì)象:
void?InspectorIoDelegate::MessageReceived(int?session_id,
??????????????????????????????????????????const?std::string&?message)?{
??auto?session?=?sessions_.find(session_id);
??if?(session?!=?sessions_.end())
????session->second->Dispatch(Utf8ToStringView(message)->string());
}
首先通過 session_id 找到對(duì)應(yīng)的 session。session 是一個(gè) CrossThreadInspectorSession 對(duì)象??纯此?Dispatch 方法:
?void?Dispatch(const?StringView&?message)?override?{
????state_.Call(&MainThreadSessionState::Dispatch,
????????????????StringBuffer::create(message));
??}
執(zhí)行 MainThreadSessionState::Dispatch:
void?Dispatch(std::unique_ptr?message) ?{
??session_->Dispatch(message->string());
}
session_ 是 SameThreadInspectorSession 對(duì)象:
void?SameThreadInspectorSession::Dispatch(
????const?v8_inspector::StringView&?message)?{
??auto?client?=?client_.lock();
??if?(client)
????client->dispatchMessageFromFrontend(session_id_,?message);
}
繼續(xù)調(diào) client->dispatchMessageFromFrontend:
?void?dispatchMessageFromFrontend(int?session_id,?const?StringView&?message)?{
???channels_[session_id]->dispatchProtocolMessage(message);
?}
通過 session_id 找到對(duì)應(yīng)的 ChannelImpl,繼續(xù)調(diào) ChannelImpl 的 dispatchProtocolMessage:
?voiddispatchProtocolMessage(const?StringView&?message)?{
???session_->dispatchProtocolMessage(message);
?}
最終調(diào)用和 V8 Inspector 的會(huì)話對(duì)象把數(shù)據(jù)發(fā)送給 V8。至此客戶端到 V8 Inspector 的通信過程就完成了。
3.5 V8 Inspector 到客戶端的數(shù)據(jù)處理
接著看從 V8 inspector 到客戶端的數(shù)據(jù)傳遞邏輯。V8 inspector 是通過 channel 的 sendResponse 函數(shù)把數(shù)據(jù)傳遞給客戶端的:
?void?sendResponse(
??????int?callId,
??????std::unique_ptr?message) ?override?{
????sendMessageToFrontend(message->string());
??}
?void?sendMessageToFrontend(const?StringView&?message)?{
????delegate_->SendMessageToFrontend(message);
?}
delegate_ 是 IoSessionDelegate 對(duì)象:
void?SendMessageToFrontend(const?v8_inspector::StringView&?message)?override?{
????request_queue_->Post(id_,?TransportAction::kSendMessage,
?????????????????????????StringBuffer::create(message));
??}
request_queue_?是 RequestQueueData 對(duì)象。
?void?Post(int?session_id,
????????????TransportAction?action,
????????????std::unique_ptr?message) ?{
????Mutex::ScopedLock?scoped_lock(state_lock_);
????bool?notify?=?messages_.empty();
????//?消息入隊(duì)
????messages_.emplace_back(action,?session_id,?std::move(message));
????if?(notify)?{
??????CHECK_EQ(0,?uv_async_send(&async_));
??????incoming_message_cond_.Broadcast(scoped_lock);
????}
??}
Post 首先把消息入隊(duì),然后通過異步的方式通知 async_,接著看 async_ 的處理函數(shù)(在子線程的事件循環(huán)里執(zhí)行):
uv_async_init(loop,?&async_,?[](uv_async_t*?async)?{
???//?拿到async對(duì)應(yīng)的上下文
???RequestQueueData*?wrapper?=?node::ContainerOf(&RequestQueueData::async_,?async);
???//?執(zhí)行RequestQueueData的DoDispatch
???wrapper->DoDispatch();
});
回調(diào)函數(shù)里調(diào)用了 wrapper->DoDispatch():
void?DoDispatch()?{
????for?(const?auto&?request?:?GetMessages())?{
??????request.Dispatch(server_);
????}
}
request 是 RequestToServer 對(duì)象。
??void?Dispatch(InspectorSocketServer*?server)?const?{
????switch?(action_)?{
??????case?TransportAction::kSendMessage:
????????server->Send(
????????????session_id_,
????????????protocol::StringUtil::StringViewToUtf8(message_->string()));
????????break;
????}
??}
接著看 InspectorSocketServer 的 Send:
void?InspectorSocketServer::Send(int?session_id,?const?std::string&?message)?{
??SocketSession*?session?=?Session(session_id);
??if?(session?!=?nullptr)?{
????session->Send(message);
??}
}
session 代表可客戶端的一個(gè)連接:
void?SocketSession::Send(const?std::string&?message)?{
??ws_socket_->Write(message.data(),?message.length());
}
接著調(diào)用 WebSocket handler 的 Write:
??void?Write(const?std::vector<char>?data)?override?{
????std::vector<char>?output?=?encode_frame_hybi17(data);
????WriteRaw(output,?WriteRequest::Cleanup);
??}
WriteRaw 是基類 ProtocolHandler 實(shí)現(xiàn)的:
int?ProtocolHandler::WriteRaw(const?std::vector<char>&?buffer,
??????????????????????????????uv_write_cb?write_cb)?{
??return?tcp_->WriteRaw(buffer,?write_cb);
}
最終是通過 TCP 連接返回給客戶端:
int?TcpHolder::WriteRaw(const?std::vector<char>&?buffer,?uv_write_cb?write_cb)?{
??//?Freed?in?write_request_cleanup
??WriteRequest*?wr?=?new?WriteRequest(handler_,?buffer);
??uv_stream_t*?stream?=?reinterpret_cast<uv_stream_t*>(&tcp_);
??int?err?=?uv_write(&wr->req,?stream,?&wr->buf,?1,?write_cb);
??if?(err?0)
????delete?wr;
??return?err?0;
}
新建一個(gè)寫請(qǐng)求,socket 可寫的時(shí)候發(fā)送數(shù)據(jù)給客戶端。
4 總結(jié)
從以上介紹和分析中,我們了解了 Node.js Inspector 的工作原理和使用。它方便了我們對(duì) Node.js 的調(diào)試和問題排查,提高開發(fā)效率。通過它可以收集 Node.js 進(jìn)程的堆快照分析是否有內(nèi)存泄漏,可以收集 CPU Profile 分析代碼的性能瓶頸,從而幫助提高服務(wù)的可用性和性能。另外,它支持動(dòng)態(tài)開啟,降低了安全風(fēng)險(xiǎn),同時(shí)支持對(duì)子線程進(jìn)行調(diào)試,是一個(gè)非常強(qiáng)大的工具。
參考內(nèi)容:1 Debugging Guide 2 inspector 3 開源的 inspector agent 實(shí)現(xiàn) 4 inspector 協(xié)議文檔 5 Debugging Node.js with Chrome DevTools
- END -