1. 程式人生 > 其它 >深入理解 Node.js 的 Inspector

深入理解 Node.js 的 Inspector

Node.js提供的Inspector非常強大,不僅可以用來除錯Node.js程式碼,還可以實時收集Node.js程序的Heap SnapshotCpu Profile等資料,同時支援靜態、動態開啟,是一個非常強大的工具,也是我們除錯和診斷Node.js程序非常好的方式。本文從使用和原理詳細講解Node.jsInspector

Node.js的文件中對Inspector的描述很少,但是如果深入探索,其實裡面的內容還是挺多的。我們先看一下Inspector的使用。

1 Inspector 的使用

1.1 本地除錯

我們先從一個例子開始,下面是一個簡單的 HTTP 伺服器。

consthttp=require('http');
http.createServer((req,res)=>{
res.end('ok');
}).listen(80);

然後我們以node --inspect httpServer.js的方式啟動。我們可以看到以下輸出。

Debuggerlisteningonws://127.0.0.1:9229/fbbd9d8f-e088-48cc-b1e0-e16bfe58db44
Forhelp,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 這種方式看起來就有點麻煩,我們可以使用瀏覽器提供的自動探測功能。

  1. URL 輸入框輸入chrome://inspect/#devices我們會看到以下介面
  1. 點選configure按鈕,在彈出的彈框裡輸入你遠端伺服器的地址
  1. 配置完畢後,我們會看到介面變成這樣了(或者開啟新的 Tab,我們看到開發者工具的除錯按鈕也變亮了)。
  1. 這時候我們點選inspect按鈕、Open dedicated DevTools for Node按鈕或者開啟新 Tab 的開發者工具,就可以開始除錯,而且還可以除錯Node.js的原生 JS 模組。

1.4 收集資料

V8 Inspector是一個非常強大的工具,除錯只是它其中一個能力,他還可以獲取Heap SnapshotCPUProfile等資料,具體能力請參考文章後面列出的指令文件和Chrome Dev Tools

  1. 收集 Cpu Profile 資訊
  1. 獲取 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::StartIoThreadInspectorIo::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在子執行緒裡啟動的原因主要有兩個。

  1. 如果在主執行緒裡執行,那麼當我們斷點除錯的時候,Node.js主執行緒就會被停住,也就無法處理客戶端發過來的除錯指令。
  2. 如果主執行緒陷入死迴圈,我們就無法實時抓取程序的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主要有三個邏輯:

  1. 建立一個 delegate 物件,該物件是核心的物件,後面我們會看到有什麼作用。
  2. 建立一個伺服器並啟動。
  3. 開啟事件迴圈。

接下來看一下伺服器的邏輯,首先看一下建立伺服器的邏輯:

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) {}

執行完後結構如下:

接著看一下ServerSocketListen方法:

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);
  }
}

接著看InspectorSocketServerAccept是如何處理連線的:

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的首先建立裡一個SocketSessionSocketSession::Delegate物件。然後呼叫InspectorSocket::Accept,從程式碼中可以看到InspectorSocket::Accept會返回一個InspectorSocket物件。InspectorSocket是對通訊socket的封裝(和客戶端通訊的socket,區別於伺服器的監聽socket)。然後記錄session物件對應的InspectorSocket物件,同時記錄sessionIdsession的對映關係。結構如下圖所示:

接著看一下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的程式碼不多,但是邏輯還是挺多的:

  1. 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);
}
  1. 新建一個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 協議進行解析。

  1. 呼叫 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物件:

  1. 執行 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協議處理。

  1. 執行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建構函式如下:

 CrossThreadInspectorSession(...) {
    // 執行 MainThreadSessionState::Connect                             
    state_.Call(&MainThreadSessionState::Connect, std::move(delegate));
 }

繼續看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.jsV8 Inspector的整體架構如下:

3.4 客戶端到 V8 Inspector 的資料處理

TCP 連線建立了,協議升級也完成了,接下來就可以開始處理業務資料。從前面的分析中我們已經知道資料到來時會執行TcpHoldlerhandler_->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;
  }

前面已經分析過delegateTcpHoldlerdelegate,即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_->MessageReceivedInspectorSocketServerdelegate_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找到對應的sessionsession是一個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,繼續調ChannelImpldispatchProtocolMessage

 voiddispatchProtocolMessage(const StringView& message) {
   session_->dispatchProtocolMessage(message);
 }

最終呼叫和V8 Inspector的會話物件把資料傳送給 V8。至此客戶端到V8 Inspector的通訊過程就完成了。

3.5 V8 Inspector 到客戶端的資料處理

接著看從V8 inspector到客戶端的資料傳遞邏輯。V8 inspector是通過channelsendResponse函式把資料傳遞給客戶端的:

 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;
    }
  }

接著看InspectorSocketServerSend

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 handlerWrite

  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可寫的時候傳送資料給客戶端。

4 總結

從以上介紹和分析中,我們瞭解了Node.js Inspector的工作原理和使用。它方便了我們對Node.js的除錯和問題排查,提高開發效率。通過它可以收集Node.js程序的堆快照分析是否有記憶體洩漏,可以收集CPU Profile分析程式碼的效能瓶頸,從而幫助提高服務的可用性和效能。另外,它支援動態開啟,降低了安全風險,同時支援對子執行緒進行除錯,是一個非常強大的工具。

參考內容:1 Debugging Guide 2 inspector 3 開源的 inspector agent 實現 4 inspector 協議文件 5 Debugging Node.js with Chrome DevTools

轉自https://mp.weixin.qq.com/s/-fFwUGJrV5Rxia7KtoPkhg