提供的
Node.js
非常強大,不僅可以用來調試
Inspector
代碼,還可以實時收集
Node.js
程序的
Node.js
、
Heap Snapshot
等資料,同時支援靜态、動态開啟,是一個非常強大的工具,也是我們調試和診斷
Cpu Profile
程序非常好的方式。本文從使用和原理詳細講解
Node.js
的
Node.js
。
Inspector
Node.js
的文檔中對
Inspector
的描述很少,但是如果深入探索,其實裡面的内容還是挺多的。我們先看一下
Inspector
的使用。
1 Inspector 的使用
1.1 本地調試
我們先從一個例子開始,下面是一個簡單的 HTTP 伺服器。
const http = require('http');
http.createServer((req, res) => {
res.end('ok');
}).listen(80);
然後我們以
node --inspect httpServer.js
的方式啟動。我們可以看到以下輸出。
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
預設選擇的端口,當然我們也可以自定義,具體可參考
Node.js
官方文檔。這時候我們去浏覽器打開開發者工具,菜單欄多了一個調試 Node.js 的按鈕。

點選這個按鈕。我們可以看到以下界面(點選切換到 Sources Tab)。
我們可以選擇某一行代碼打斷點,比如我在第三行,這時候我們通路
80
端口,開發者工具就會停留在斷點處。這時候我們可以看到一些執行上下文。
1.2 遠端調試
但很多時候我們可能需要遠端調試。比如我在一台雲伺服器上部署以上伺服器代碼。然後執行
node --inspect=0.0.0.0:8888 httpServer.js
我們打開開發者工具發現按鈕置灰或者找不到我們遠端伺服器的資訊。這時候我們需要用另一種方式,通過在浏覽器url輸入框輸入:
devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws={host}:{port}/{path}
的方式(替換
{}
裡面的内容為你執行
Node.js
時輸出的資訊),浏覽器就會去連接配接指定的位址,比如執行上面的指令輸出的是
ws://0.0.0.0:8888/f6e42278-d915-48dc-af4d-453a23d330ab
,假設公網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
就可以開始調試了,這種方式比較适合于常用的場景。
1.3 自動探測
如果是我們自己調試的話,1.2 這種方式看起來就有點麻煩,我們可以使用浏覽器提供的自動探測功能。
- URL 輸入框輸入
我們會看到以下界面chrome://inspect/#devices
- 點選
按鈕,在彈出的彈框裡輸入你遠端伺服器的位址configure
- 配置完畢後,我們會看到界面變成這樣了(或者打開新的 Tab,我們看到開發者工具的調試按鈕也變亮了)。
- 這時候我們點選
按鈕、inspect
按鈕或者打開新 Tab 的開發者工具,就可以開始調試,而且還可以調試Open dedicated DevTools for Node
的原生 JS 子產品。Node.js
1.4 收集資料
V8 Inspector
是一個非常強大的工具,調試隻是它其中一個能力,他還可以擷取
Heap Snapshot
、
CPU Profile
等資料,具體能力請參考文章後面列出的指令文檔和
Chrome Dev Tools
。
- 收集 Cpu Profile 資訊
- 擷取 Heap Snapshop
1.5 動态開啟 Inspector
預設打開
Inspector
能力是不安全的,這意味着能連上伺服器的用戶端都能通過協定控制 Node.js 程序(雖然 URL 并不容易猜對),通常我們是在 Node.js 程序出現問題的時候,動态開啟 Inspector,我們看一下下面的例子。
const inspector = require('inspector');
const http = require('http');
let isOpend = false;
function getHTML() {
return `<html>
<meta charset="utf-8" />
<body>
複制到新 Tab 打開該 URL 開始調試 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') {
// 還沒開啟則開啟
if (!isOpend) {
isOpend = true;
// 打開調試器
inspector.open();
}
// 傳回給前端的内容
const html = getHTML() ;
res.end(html);
} else if (req.url == '/debug/close') {
// 如果開啟了則關閉
if (isOpend) {
inspector.close();
isOpend = false;
}
res.end('ok');
} else {
res.end('ok');
}
}).listen(80);
當我們需要調試的時候,通過通路 /debug/open 打開調試器。前端界面可以看到以下輸出。
複制到新 Tab 打開該 URL 開始調試 devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/9efd4c80-956a-4422-b23c-4348e6613304
接着新開一個 Tab,然後複制上面的 URL,粘貼到浏覽器 URL 位址欄通路,我們就可以看到調試頁面。
然後打個斷點,接着新開一個 Tab 通路
http://localhost
就可以進入調試,調試完成後通路 /debug/close 關閉調試器。浏覽器界面就會顯示斷開連接配接了。
以上方式支援調試和收集資料,如果我們隻是需要收集資料,還有另一種動态開啟
Inspector
的方式
const http = require('http');
const inspector = require('inspector');
const fs = require('fs');
function getCpuprofile(req, res) {
// 打開一個和 V8 Inspector 的會話
const session = new inspector.Session();
session.connect();
// 向V8 Inspector 送出指令,開啟 Cpu Profile 并收集資料
session.post('Profiler.enable', () => {
session.post('Profiler.start', () => {
// 收集一段時間後送出停止收集指令
setTimeout(() => {
session.post('Profiler.stop', (err, { profile }) => {
// 把資料寫入檔案
if (!err) {
fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile));
}
// 斷開會話
session.disconnect();
// 回複用戶端
res.end('ok');
});
}, 3000)
});
});
}
http.createServer((req, res) => {
if (req.url == '/debug/getCpuprofile') {
getCpuprofile(req, res);
} else {
res.end('ok');
}
}).listen(80);
我們可以通過
Inspector Session
的能力,實時和
V8 Inspector
互動而不需要啟動一個
WebSocket
服務。本地調試時還可以在
VSCode
裡點選
Profile
檔案直接看到效果。
2 Inspector 調試的原理
下面以通過 URL 的方式調試(可以看到 Network ),來看看調試的時候都發生了什麼,浏覽器和遠端伺服器建立連接配接後,是通過 WebSocket 協定通信的,下面是一次通信的資訊。
我們看一下這指令是什麼意思(具體可以參考 Inspector 協定文檔)。
Debugger.scriptParsed # Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger.
從說明中我們看到,當 V8 解析腳本的時候就會觸發這個事件,告訴浏覽器相關的資訊。
我們發現傳回的都是一些中繼資料,沒有腳本的具體代碼内容,這時候浏覽器會再次發起請求(點選對應腳本對應的 JS 檔案時),
我們看到這個腳本的 scriptId 是 103。是以請求裡帶了這個 scriptId。對應的請求 id 是 11。接着看一下響應。
至此,我們了解了擷取腳本内容的過程,然後我們看看調試的時候是怎樣的過程。當我們在浏覽器上點選某一行設定斷點的時候,浏覽器就會發送一個請求。
這個指令的意義顧名思義,我們看一下具體定義:
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.
接着服務傳回響應。
這時候我們從另外一個 Tab 通路 80 端口,伺服器就會在我們設定的斷點處停留,并且通知浏覽器。
我們看一下這個指令的意思。
這個指令就是當伺服器執行到斷點時通知浏覽器,并且傳回執行的一些上下文,比如執行到哪個斷點停留了。這時候浏覽器側也會停留在對應的地方,當我們 hover 某個變量時,就會看到對應的上下文。這些都是通過具體的指令擷取的資料。就不一一分析了。
3 Node.js Inspector 的實作
大緻了解了浏覽器和伺服器的互動過程和協定後,我們再來深入了解一下關于 Inspector 的一些實作。當然這裡不是分析 V8 中 Inspector 的實作,而是分析如何使用 V8 的 Inspector 以及 Node.js 中關于 Inspector 的實作部分。
當我們以以下方式執行應用時
node --inspect app.js
3.1 初始化
Node.js 在啟動的過程中,就會初始化 Inspector 相關的邏輯。
inspector_agent_ = std::make_unique<inspector::Agent>(this);
Agent 是負責和
V8 Inspector
通信的對象,建立完後接着執行
env->InitializeInspector({})
啟動
Agent
。
inspector_agent_->Start(...);
Start 繼續執行
Agent::StartIoThread
。
bool Agent::StartIoThread() {
io_ = InspectorIo::Start(client_->getThreadHandle(), ...);
return true;
}
StartIoThread
中的
client_->getThreadHandle()
是重要的邏輯,我們先來分析該函數。
std::shared_ptr<MainThreadHandle> getThreadHandle() {
if (!interface_) {
interface_ = std::make_shared<MainThreadInterface>(env_->inspector_agent(), ...);
}
return interface_->GetHandle();
}
getThreadHandle
首先建立來一個
MainThreadInterface
對象,接着又調用了他的 GetHandle 方法,我們看一下該方法的邏輯。
std::shared_ptr<MainThreadHandle> MainThreadInterface::GetHandle() {
if (handle_ == nullptr)
handle_ = std::make_shared<MainThreadHandle>(this);
return handle_;
}
GetHandle
了建立了一個
MainThreadHandle
對象,最終結構如下所示。
分析完後我們繼續看
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
裡建立了一個
InspectorIo
對象,我們看看
InspectorIo
構造函數的邏輯。
InspectorIo::InspectorIo(std::shared_ptr<MainThreadHandle> main_thread, ...)
:
// 初始化 main_thread_
main_thread_(main_thread)) {
// 建立一個子線程,子線程中執行 InspectorIo::ThreadMain
uv_thread_create(&thread_, InspectorIo::ThreadMain, this);
}
這時候結構如下:
InspectorIo
建立了一個子線程,
Inspector
在子線程裡啟動的原因主要有兩個。
- 如果在主線程裡運作,那麼當我們斷點調試的時候,
主線程就會被停住,也就無法處理用戶端發過來的調試指令。Node.js
- 如果主線程陷入死循環,我們就無法實時抓取程序的
資料來分析原因。Profile
接着繼續看一下子線程裡執行
InspectorIo::ThreadMain
的邏輯:
void InspectorIo::ThreadMain(void* io) {
static_cast<InspectorIo*>(io)->ThreadMain();
}
void InspectorIo::ThreadMain() {
uv_loop_t loop;
loop.data = nullptr;
// 在子線程開啟一個新的事件循環
int err = uv_loop_init(&loop);
std::shared_ptr<RequestQueueData> queue(new RequestQueueData(&loop), ...);
// 建立一個 delegate,用于處理請求
std::unique_ptr<InspectorIoDelegate> delegate(
new InspectorIoDelegate(queue, main_thread_, ...)
);
InspectorSocketServer server(std::move(delegate), ...);
server.Start();
// 進入事件循環
uv_run(&loop, UV_RUN_DEFAULT);
}
ThreadMain
主要有三個邏輯:
- 建立一個 delegate 對象,該對象是核心的對象,後面我們會看到有什麼作用。
- 建立一個伺服器并啟動。
- 開啟事件循環。
接下來看一下伺服器的邏輯,首先看一下建立伺服器的邏輯:
InspectorSocketServer::InspectorSocketServer(std::unique_ptr<SocketServerDelegate> delegate, ...)
: // 儲存 delegate
delegate_(std::move(delegate)),
// 初始化 sessionId
next_session_id_(0) {
// 設定 delegate 的 server 為目前伺服器
delegate_->AssignServer(this);
}
執行完後形成以下結構:
接着我們看啟動伺服器的邏輯:
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);
// 監聽解析到的 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;
}
首先根據參數做
DNS
解析,然後根據拿到的
IP
清單(通常是一個),建立對應個數的
ServerSocket
對象,并執行它的
Listen
方法。
ServerSocket
表示一個監聽
socket
,看一下
ServerSocket
的構造函數:
ServerSocket(InspectorSocketServer* server) :
tcp_socket_(uv_tcp_t()), server_(server) {}
執行完後結構如下:
接着看一下
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
調用
Libuv
的接口完成伺服器的啟動。至此,
Inspector
提供的
Weboscket
伺服器啟動了。
3.2 處理連接配接
從剛才分析中可以看到,當有連接配接到來時執行回調
ServerSocket::SocketConnectedCallback
。
void ServerSocket::SocketConnectedCallback(uv_stream_t* tcp_socket,
int status) {
if (status == 0) {
// 根據 Libuv handle 找到對應的 ServerSocket 對象
ServerSocket* server_socket = ServerSocket::FromTcpSocket(tcp_socket);
// Socket 對象的 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
的首先建立裡一個
SocketSession
和
SocketSession::Delegate
對象。然後調用
InspectorSocket::Accept
,從代碼中可以看到
InspectorSocket::Accept
會傳回一個
InspectorSocket
對象。
InspectorSocket
是對通信
socket
的封裝(和用戶端通信的
socket
,差別于伺服器的監聽
socket
)。然後記錄
session
對象對應的
InspectorSocket
對象,同時記錄
sessionId
和
session
的映射關系。結構如下圖所示:
接着看一下
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
擷取一個TcpHolder::Accept
對象。TcpHolder
TcpHolder::Pointer TcpHolder::Accept(
uv_stream_t* server,
InspectorSocket::DelegatePointer delegate) {
// 建立一個 TcpHolder 對象,TcpHolder 是對 uv_tcp_t 和 delegate 的封裝
TcpHolder* result = new TcpHolder(std::move(delegate));
// 拿到 TcpHolder 對象的 uv_tcp_t 結構體
uv_stream_t* tcp = reinterpret_cast<uv_stream_t*>(&result->tcp_);
// 初始化
int err = uv_tcp_init(server->loop, &result->tcp_);
// 摘取一個 TCP 連接配接對應的 fd 儲存到 TcpHolder 的 uv_tcp_t 結構體中(即第二個參數的 tcp 字段)
uv_accept(server, tcp);
// 注冊等待可讀事件,有資料時執行 OnDataReceivedCb 回調
uv_read_start(tcp, allocate_buffer, OnDataReceivedCb);
return TcpHolder::Pointer(result);
}
- 建立一個
對象:HttpHandler
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)) {
// 設定 TCP 資料的 handler,TCP 是隻負責傳輸,資料的解析交給 handler 處理
tcp_->SetHandler(this);
}
HttpHandler
是對
TcpHolder
的封裝,主要通過 HTTP 解析器
llhttp
對 HTTP 協定進行解析。
- 調用 inspector->SwitchProtocol() 切換目前協定處理器為 HTTP,建立 TCP 連接配接後,首先要經過一個 HTTP 請求從 HTTP 協定更新到 WebSocket 協定,更新成功後就使用 Websocket 協定進行通信.
我們看一下這時候的結構圖:
至此,就完成了連接配接處理的分析!(撒花,你學廢了麼)
3.3 協定更新
完成了 TCP 連接配接的處理後,接下來要完成協定更新,因為
Inspector
是通過
WebSocket
協定和用戶端通信的,是以需要通過一個 HTTP 請求來完成 HTTP 到
WebSocekt
協定的更新。從剛才的分析中看當有資料到來時會執行
OnDataReceivedCb
回調:
void TcpHolder::OnDataReceivedCb(uv_stream_t* tcp, ssize_t nread,
const uv_buf_t* buf) {
TcpHolder* holder = From(tcp);
holder->ReclaimUvBuf(buf, nread);
// 調用 handler 的 onData,目前 handler 是 HTTP 協定
holder->handler_->OnData(&holder->buffer);
}
TCP 層收到資料後交給應用層解析,直接調用上層的 OnData 回調。
void OnData(std::vector<char>* data) override {
// 解析 HTTP 協定
llhttp_execute(&parser_, data->data(), data->size());
// 解析完并且是更新協定的請求則調用 delegate 的回調 OnSocketUpgrade
delegate()->OnSocketUpgrade(event.host, event.path, event.ws_key);
}
OnData
可能會被多次回調,并通過
llhttp_execute
解析收到的 HTTP 封包,當發現是一個協定更新的請求後,就調用
OnSocketUpgrade
回調。
delegate
是一個
SocketSession::Delegate
對象。來看一下該對象的 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
又調用了
server_
(
InspectorSocketServer
對象)的
SessionStarted
:
void InspectorSocketServer::SessionStarted(int session_id,
const std::string& id,
const std::string& ws_key) {
// 找到對應的 session 對象
SocketSession* session = Session(session_id);
connected_sessions_[session_id].first = id;
session->Accept(ws_key);
delegate_->StartSession(session_id, id);
}
首先通過
session_id
找到建立 TCP 連接配接時配置設定的
SocketSession
對象:
- 執行 session->Accept(ws_key) 回複用戶端同意協定更新:
void Accept(const std::string& ws_key) {
ws_socket_->AcceptUpgrade(ws_key);
}
從結構圖我們可以看到
ws_socket_
是一個
InspectorSocket
對象:
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: ";
// ...
// 回複 101 給用戶端
WriteRaw(reply, WriteRequest::Cleanup);
// 切換 handler 為 WebSocket handler
inspector_->SwitchProtocol(new WsHandler(inspector_, std::move(tcp_)));
}
AcceptUpgradeh
首先回複用戶端 101 表示同意更新到
WebSocket
協定,然後切換資料處理器為
WsHandler
,即後續的資料按照
WebSocket
協定處理。
- 執行
建立和delegate_->StartSession(session_id, id)
的會話。V8 Inspector
是delegate_
對象:InspectorIoDelegate
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");
}
}
首先通過
main_thread_->Connect
拿到一個
session
,并在
InspectorIoDelegate
中記錄映射關系。結構圖如下:
接下來看一下
main_thread_->Connect
的邏輯(
main_thread_
是
MainThreadHandle
對象):
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
函數建立了一個
CrossThreadInspectorSession
對象。
CrossThreadInspectorSession
構造函數如下:
繼續看
MainThreadSessionState::Connect
:
void Connect(std::unique_ptr<InspectorSessionDelegate> delegate) {
Agent* agent = thread_->inspector_agent();
session_ = agent->Connect(std::move(delegate), prevent_shutdown_);
}
繼續調
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_));
}
繼續調
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
建立了一個
ChannelImpl
并且在
channels_
中儲存了映射關系。看看
ChannelImpl
的構造函數:
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
調用
inspector->connect
建立了一個和
V8 Inspector
的會話。結構圖大緻如下:
用戶端到
Node.js
到
V8 Inspector
的整體架構如下:
3.4 用戶端到 V8 Inspector 的資料處理
TCP 連接配接建立了,協定更新也完成了,接下來就可以開始處理業務資料。從前面的分析中我們已經知道資料到來時會執行
TcpHoldler
的
handler_->OnData
回調。因為已經完成了協定更新,是以這時候的
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
協定:
int ParseWsFrames(const std::vector<char>& buffer) {
int bytes_consumed = 0;
std::vector<char> output;
bool compressed = false;
// 解析WebSocket協定
ws_decode_result r = decode_frame_hybi17(buffer,
true /* client_frame */,
&bytes_consumed, &output,
&compressed);
// 執行delegate的回調
delegate()->OnWsFrame(output);
return bytes_consumed;
}
前面已經分析過
delegate
是
TcpHoldler
的
delegate
,即
SocketSession::Delegate
對象:
void SocketSession::Delegate::OnWsFrame(const std::vector<char>& data) {
server_->MessageReceived(session_id_,
std::string(data.data(),
data.size()));
}
繼續回調
server_->MessageReceived
。從結構圖可以看到
server_
是
InspectorSocketServer
對象:
void MessageReceived(int session_id, const std::string& message) {
delegate_->MessageReceived(session_id, message);
}
繼續回調
delegate_->MessageReceived
,
InspectorSocketServer
的
delegate_
是
InspectorIoDelegate
對象:
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
找到對應的
session
。
session
是一個
CrossThreadInspectorSession
對象。看看他的
Dispatch
方法:
void Dispatch(const StringView& message) override {
state_.Call(&MainThreadSessionState::Dispatch,
StringBuffer::create(message));
}
執行
MainThreadSessionState::Dispatch
:
void Dispatch(std::unique_ptr<StringBuffer> message) {
session_->Dispatch(message->string());
}
session_
是
SameThreadInspectorSession
對象:
void SameThreadInspectorSession::Dispatch(
const v8_inspector::StringView& message) {
auto client = client_.lock();
if (client)
client->dispatchMessageFromFrontend(session_id_, message);
}
繼續調
client->dispatchMessageFromFrontend
:
void dispatchMessageFromFrontend(int session_id, const StringView& message) {
channels_[session_id]->dispatchProtocolMessage(message);
}
通過
session_id
找到對應的
ChannelImpl
,繼續調
ChannelImpl
的
dispatchProtocolMessage
:
const StringView& message) {
session_->dispatchProtocolMessage(message);
}
最終調用和
V8 Inspector
的會話對象把資料發送給 V8。至此用戶端到
V8 Inspector
的通信過程就完成了。
3.5 V8 Inspector 到用戶端的資料處理
接着看從
V8 inspector
到用戶端的資料傳遞邏輯。
V8 inspector
是通過
channel
的
sendResponse
函數把資料傳遞給用戶端的:
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
對象:
void SendMessageToFrontend(const v8_inspector::StringView& message) override {
request_queue_->Post(id_, TransportAction::kSendMessage,
StringBuffer::create(message));
}
request_queue_ 是 RequestQueueData 對象。
void Post(int session_id,
TransportAction action,
std::unique_ptr<StringBuffer> message) {
Mutex::ScopedLock scoped_lock(state_lock_);
bool notify = messages_.empty();
// 消息入隊
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
首先把消息入隊,然後通過異步的方式通知
async_
,接着看
async_
的處理函數(在子線程的事件循環裡執行):
uv_async_init(loop, &async_, [](uv_async_t* async) {
// 拿到async對應的上下文
RequestQueueData* wrapper = node::ContainerOf(&RequestQueueData::async_, async);
// 執行RequestQueueData的DoDispatch
wrapper->DoDispatch();
});
回調函數裡調用了
wrapper->DoDispatch()
:
void DoDispatch() {
for (const auto& request : GetMessages()) {
request.Dispatch(server_);
}
}
request 是 RequestToServer 對象。
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
代表可用戶端的一個連接配接:
void SocketSession::Send(const std::string& message) {
ws_socket_->Write(message.data(), message.length());
}
接着調用
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
實作的:
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;
}
建立一個寫請求,
socket
可寫的時候發送資料給用戶端。