深入理解 Node.js 的 Inspector
Node.js提供的Inspector非常強(qiáng)大,不僅可以用來(lái)調(diào)試Node.js代碼,還可以實(shí)時(shí)收集Node.js進(jìn)程的Heap Snapshot、Cpu Profile等數(shù)據(jù),同時(shí)支持靜態(tài)、動(dòng)態(tài)開(kā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è)例子開(kāi)始,下面是一個(gè)簡(jiǎn)單的 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覽器打開(kāi)開(kāi)發(fā)者工具,菜單欄多了一個(gè)調(diào)試 Node.js 的按鈕。

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

我們可以選擇某一行代碼打斷點(diǎn),比如我在第三行,這時(shí)候我們?cè)L問(wèn) 80 端口,開(kāi)發(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
我們打開(kāi)開(kāi)發(fā)者工具發(fā)現(xiàn)按鈕置灰或者找不到我們遠(yuǎn)程服務(wù)器的信息。這時(shí)候我們需要用另一種方式,通過(guò)在瀏覽器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 就可以開(kāi)始調(diào)試了,這種方式比較適合于常用的場(chǎng)景。
1.3 自動(dòng)探測(cè)
如果是我們自己調(diào)試的話(huà),1.2 這種方式看起來(lái)就有點(diǎn)麻煩,我們可以使用瀏覽器提供的自動(dòng)探測(cè)功能。
URL 輸入框輸入 chrome://inspect/#devices我們會(huì)看到以下界面

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

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

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

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

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

2 Inspector 調(diào)試的原理
下面以通過(guò) URL 的方式調(diào)試(可以看到 Network ),來(lái)看看調(diào)試的時(shí)候都發(fā)生了什么,瀏覽器和遠(yuǎn)程服務(wù)器建立連接后,是通過(guò) 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.
從說(shuō)明中我們看到,當(dāng) V8 解析腳本的時(shí)候就會(huì)觸發(fā)這個(gè)事件,告訴瀏覽器相關(guān)的信息。

我們發(fā)現(xiàn)返回的都是一些元數(shù)據(jù),沒(méi)有腳本的具體代碼內(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)容的過(guò)程,然后我們看看調(diào)試的時(shí)候是怎樣的過(guò)程。當(dāng)我們?cè)跒g覽器上點(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 訪(fǎng)問(wèn) 80 端口,服務(wù)器就會(huì)在我們?cè)O(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)的上下文。這些都是通過(guò)具體的命令獲取的數(shù)據(jù)。就不一一分析了。

3 Node.js Inspector 的實(shí)現(xiàn)
大致了解了瀏覽器和服務(wù)器的交互過(guò)程和協(xié)議后,我們?cè)賮?lái)深入了解一下關(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)的過(guò)程中,就會(huì)初始化 Inspector 相關(guān)的邏輯。
inspector_agent_ = std::make_unique<inspector::Agent>(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() 是重要的邏輯,我們先來(lái)分析該函數(shù)。
std::shared_ptr<MainThreadHandle> getThreadHandle() {
if (!interface_) {
interface_ = std::make_shared<MainThreadInterface>(env_->inspector_agent(), ...);
}
return interface_->GetHandle();
}
getThreadHandle 首先創(chuàng)建來(lái)一個(gè) MainThreadInterface 對(duì)象,接著又調(diào)用了他的 GetHandle 方法,我們看一下該方法的邏輯。
std::shared_ptr<MainThreadHandle> MainThreadInterface::GetHandle() {
if (handle_ == nullptr)
handle_ = std::make_shared<MainThreadHandle>(this);
return handle_;
}
GetHandle 了創(chuàng)建了一個(gè) MainThreadHandle 對(duì)象,最終結(jié)構(gòu)如下所示。

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

InspectorIo 創(chuàng)建了一個(gè)子線(xiàn)程, Inspector 在子線(xiàn)程里啟動(dòng)的原因主要有兩個(gè)。
如果在主線(xiàn)程里運(yùn)行,那么當(dāng)我們斷點(diǎn)調(diào)試的時(shí)候, Node.js主線(xiàn)程就會(huì)被停住,也就無(wú)法處理客戶(hù)端發(fā)過(guò)來(lái)的調(diào)試指令。如果主線(xiàn)程陷入死循環(huán),我們就無(wú)法實(shí)時(shí)抓取進(jìn)程的 Profile數(shù)據(jù)來(lái)分析原因。
接著繼續(xù)看一下子線(xiàn)程里執(zhí)行 InspectorIo::ThreadMain 的邏輯:
void InspectorIo::ThreadMain(void* io) {
static_cast<InspectorIo*>(io)->ThreadMain();
}
void InspectorIo::ThreadMain() {
uv_loop_t loop;
loop.data = nullptr;
// 在子線(xiàn)程開(kāi)啟一個(gè)新的事件循環(huán)
int err = uv_loop_init(&loop);
std::shared_ptr<RequestQueueData> queue(new RequestQueueData(&loop), ...);
// 新建一個(gè) delegate,用于處理請(qǐng)求
std::unique_ptr<InspectorIoDelegate> 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)。 開(kāi)啟事件循環(huán)。
接下來(lái)看一下服務(wù)器的邏輯,首先看一下創(chuàng)建服務(wù)器的邏輯:
InspectorSocketServer::InspectorSocketServer(std::unique_ptr<SocketServerDelegate> 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)聽(tīng)解析到的 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)聽(tīng) 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)有連接到來(lái)時(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<SocketSession> 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 的封裝(和客戶(hù)端通信的 socket,區(qū)別于服務(wù)器的監(jiān)聽(tīng) 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);
// 注冊(cè)等待可讀事件,有數(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 的封裝,主要通過(guò) HTTP 解析器 llhttp 對(duì) HTTP 協(xié)議進(jìn)行解析。
調(diào)用 inspector->SwitchProtocol() 切換當(dāng)前協(xié)議處理器為 HTTP,建立 TCP 連接后,首先要經(jīng)過(guò)一個(gè) HTTP 請(qǐng)求從 HTTP 協(xié)議升級(jí)到 WebSocket 協(xié)議,升級(jí)成功后就使用 Websocket 協(xié)議進(jìn)行通信.
我們看一下這時(shí)候的結(jié)構(gòu)圖:

至此,就完成了連接處理的分析!(撒花,你學(xué)廢了么)
3.3 協(xié)議升級(jí)
完成了 TCP 連接的處理后,接下來(lái)要完成協(xié)議升級(jí),因?yàn)?Inspector 是通過(guò) WebSocket 協(xié)議和客戶(hù)端通信的,所以需要通過(guò)一個(gè) HTTP 請(qǐng)求來(lái)完成 HTTP 到 WebSocekt 協(xié)議的升級(jí)。從剛才的分析中看當(dāng)有數(shù)據(jù)到來(lái)時(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),并通過(guò) 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ì)象。來(lái)看一下該對(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);
}
首先通過(guò) session_id 找到建立 TCP 連接時(shí)分配的 SocketSession 對(duì)象:
執(zhí)行 session->Accept(ws_key) 回復(fù)客戶(hù)端同意協(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 給客戶(hù)端
WriteRaw(reply, WriteRequest::Cleanup);
// 切換 handler 為 WebSocket handler
inspector_->SwitchProtocol(new WsHandler(inspector_, std::move(tcp_)));
}
AcceptUpgradeh 首先回復(fù)客戶(hù)端 101 表示同意升級(jí)到 WebSocket 協(xié)議,然后切換數(shù)據(jù)處理器為 WsHandler,即后續(xù)的數(shù)據(jù)按照 WebSocket 協(xié)議處理。
執(zhí)行 delegate_->StartSession(session_id, id)建立和V8 Inspector的會(huì)話(huà)。delegate_是InspectorIoDelegate對(duì)象:
void InspectorIoDelegate::StartSession(int session_id,
const std::string& target_id) {
auto session = main_thread_->Connect(
std::unique_ptr<InspectorSessionDelegate>(
new IoSessionDelegate(request_queue_->handle(), session_id)
),
true);
if (session) {
sessions_[session_id] = std::move(session);
fprintf(stderr, "Debugger attached.\n");
}
}
首先通過(guò) main_thread_->Connect 拿到一個(gè) session,并在 InspectorIoDelegate 中記錄映射關(guān)系。結(jié)構(gòu)圖如下:

接下來(lái)看一下 main_thread_->Connect 的邏輯(main_thread_ 是 MainThreadHandle 對(duì)象):
std::unique_ptr<InspectorSession> MainThreadHandle::Connect(
std::unique_ptr<InspectorSessionDelegate> delegate,
bool prevent_shutdown) {
return std::unique_ptr<InspectorSession>(
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<InspectorSessionDelegate> delegate) {
Agent* agent = thread_->inspector_agent();
session_ = agent->Connect(std::move(delegate), prevent_shutdown_);
}
繼續(xù)調(diào) agent->Connect:
std::unique_ptr<InspectorSession> Agent::Connect(
std::unique_ptr<InspectorSessionDelegate> delegate,
bool prevent_shutdown) {
int session_id = client_->connectFrontend(std::move(delegate),
prevent_shutdown);
return std::unique_ptr<InspectorSession>(
new SameThreadInspectorSession(session_id, client_));
}
繼續(xù)調(diào) connectFrontend:
int connectFrontend(std::unique_ptr<InspectorSessionDelegate> delegate,
bool prevent_shutdown) {
int session_id = next_session_id_++;
channels_[session_id] = std::make_unique<ChannelImpl>(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<V8Inspector>& inspector,
std::unique_ptr<InspectorSessionDelegate> delegate, ...)
: delegate_(std::move(delegate)) {
session_ = inspector->connect(CONTEXT_GROUP_ID, this, StringView());
}
ChannelImpl 調(diào)用 inspector->connect 建立了一個(gè)和 V8 Inspector 的會(huì)話(huà)。結(jié)構(gòu)圖大致如下:

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

3.4 客戶(hù)端到 V8 Inspector 的數(shù)據(jù)處理
TCP 連接建立了,協(xié)議升級(jí)也完成了,接下來(lái)就可以開(kāi)始處理業(yè)務(wù)數(shù)據(jù)。從前面的分析中我們已經(jīng)知道數(shù)據(jù)到來(lái)時(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 通過(guò) 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)分析過(guò) 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());
}
首先通過(guò) 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<StringBuffer> 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);
}
通過(guò) session_id 找到對(duì)應(yīng)的 ChannelImpl,繼續(xù)調(diào) ChannelImpl 的 dispatchProtocolMessage:
voiddispatchProtocolMessage(const StringView& message) {
session_->dispatchProtocolMessage(message);
}
最終調(diào)用和 V8 Inspector 的會(huì)話(huà)對(duì)象把數(shù)據(jù)發(fā)送給 V8。至此客戶(hù)端到 V8 Inspector 的通信過(guò)程就完成了。
3.5 V8 Inspector 到客戶(hù)端的數(shù)據(jù)處理
接著看從 V8 inspector 到客戶(hù)端的數(shù)據(jù)傳遞邏輯。V8 inspector 是通過(guò) channel 的 sendResponse 函數(shù)把數(shù)據(jù)傳遞給客戶(hù)端的:
void sendResponse(
int callId,
std::unique_ptr<v8_inspector::StringBuffer> 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<StringBuffer> 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ì),然后通過(guò)異步的方式通知 async_,接著看 async_ 的處理函數(shù)(在子線(xiàn)程的事件循環(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 代表可客戶(hù)端的一個(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 是基類(lèi) ProtocolHandler 實(shí)現(xiàn)的:
int ProtocolHandler::WriteRaw(const std::vector<char>& buffer,
uv_write_cb write_cb) {
return tcp_->WriteRaw(buffer, write_cb);
}
最終是通過(guò) TCP 連接返回給客戶(hù)端:
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è)寫(xiě)請(qǐng)求,socket 可寫(xiě)的時(shí)候發(fā)送數(shù)據(jù)給客戶(hù)端。
4 總結(jié)
從以上介紹和分析中,我們了解了 Node.js Inspector 的工作原理和使用。它方便了我們對(duì) Node.js 的調(diào)試和問(wèn)題排查,提高開(kāi)發(fā)效率。通過(guò)它可以收集 Node.js 進(jìn)程的堆快照分析是否有內(nèi)存泄漏,可以收集 CPU Profile 分析代碼的性能瓶頸,從而幫助提高服務(wù)的可用性和性能。另外,它支持動(dòng)態(tài)開(kāi)啟,降低了安全風(fēng)險(xiǎn),同時(shí)支持對(duì)子線(xiàn)程進(jìn)行調(diào)試,是一個(gè)非常強(qiáng)大的工具。
參考內(nèi)容:1 Debugging Guide 2 inspector 3 開(kāi)源的 inspector agent 實(shí)現(xiàn) 4 inspector 協(xié)議文檔 5 Debugging Node.js with Chrome DevTools
內(nèi)推社群
我組建了一個(gè)氛圍特別好的騰訊內(nèi)推社群,如果你對(duì)加入騰訊感興趣的話(huà)(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行面試相關(guān)的答疑、聊聊面試的故事、并且在你準(zhǔn)備好的時(shí)候隨時(shí)幫你內(nèi)推。下方加 winty 好友回復(fù)「面試」即可。
