天天看點

深入了解 Node.js 的 Inspector

​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 的按鈕。

深入了解 Node.js 的 Inspector

點選這個按鈕。我們可以看到以下界面(點選切換到 Sources Tab)。

深入了解 Node.js 的 Inspector

我們可以選擇某一行代碼打斷點,比如我在第三行,這時候我們通路 ​

​80​

​ 端口,開發者工具就會停留在斷點處。這時候我們可以看到一些執行上下文。

深入了解 Node.js 的 Inspector

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​

    ​ 我們會看到以下界面
深入了解 Node.js 的 Inspector
  1. 點選​

    ​configure​

    ​ 按鈕,在彈出的彈框裡輸入你遠端伺服器的位址
深入了解 Node.js 的 Inspector
  1. 配置完畢後,我們會看到界面變成這樣了(或者打開新的 Tab,我們看到開發者工具的調試按鈕也變亮了)。
深入了解 Node.js 的 Inspector
  1. 這時候我們點選​

    ​inspect​

    ​​ 按鈕、​

    ​Open dedicated DevTools for Node​

    ​​ 按鈕或者打開新 Tab 的開發者工具,就可以開始調試,而且還可以調試​

    ​Node.js​

    ​ 的原生 JS 子產品。
深入了解 Node.js 的 Inspector

1.4 收集資料

​V8 Inspector​

​​ 是一個非常強大的工具,調試隻是它其中一個能力,他還可以擷取 ​

​Heap Snapshot​

​​、​

​CPU Profile​

​​ 等資料,具體能力請參考文章後面列出的指令文檔和 ​

​Chrome Dev Tools​

​。

  1. 收集 Cpu Profile 資訊
深入了解 Node.js 的 Inspector
  1. 擷取 Heap Snapshop
深入了解 Node.js 的 Inspector

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 位址欄通路,我們就可以看到調試頁面。

深入了解 Node.js 的 Inspector

然後打個斷點,接着新開一個 Tab 通路 ​

​http://localhost​

​ 就可以進入調試,調試完成後通路 /debug/close 關閉調試器。浏覽器界面就會顯示斷開連接配接了。

深入了解 Node.js 的 Inspector

以上方式支援調試和收集資料,如果我們隻是需要收集資料,還有另一種動态開啟 ​

​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​

​ 檔案直接看到效果。

深入了解 Node.js 的 Inspector

2 Inspector 調試的原理

下面以通過 URL 的方式調試(可以看到 Network ),來看看調試的時候都發生了什麼,浏覽器和遠端伺服器建立連接配接後,是通過 WebSocket 協定通信的,下面是一次通信的資訊。

深入了解 Node.js 的 Inspector

我們看一下這指令是什麼意思(具體可以參考 Inspector 協定文檔)。

Debugger.scriptParsed # Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger.

從說明中我們看到,當 V8 解析腳本的時候就會觸發這個事件,告訴浏覽器相關的資訊。

深入了解 Node.js 的 Inspector

我們發現傳回的都是一些中繼資料,沒有腳本的具體代碼内容,這時候浏覽器會再次發起請求(點選對應腳本對應的 JS 檔案時),

深入了解 Node.js 的 Inspector

我們看到這個腳本的 scriptId 是 103。是以請求裡帶了這個 scriptId。對應的請求 id 是 11。接着看一下響應。

深入了解 Node.js 的 Inspector

至此,我們了解了擷取腳本内容的過程,然後我們看看調試的時候是怎樣的過程。當我們在浏覽器上點選某一行設定斷點的時候,浏覽器就會發送一個請求。

深入了解 Node.js 的 Inspector

這個指令的意義顧名思義,我們看一下具體定義:

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.

接着服務傳回響應。

深入了解 Node.js 的 Inspector

這時候我們從另外一個 Tab 通路 80 端口,伺服器就會在我們設定的斷點處停留,并且通知浏覽器。

深入了解 Node.js 的 Inspector

我們看一下這個指令的意思。

深入了解 Node.js 的 Inspector

這個指令就是當伺服器執行到斷點時通知浏覽器,并且傳回執行的一些上下文,比如執行到哪個斷點停留了。這時候浏覽器側也會停留在對應的地方,當我們 hover 某個變量時,就會看到對應的上下文。這些都是通過具體的指令擷取的資料。就不一一分析了。

深入了解 Node.js 的 Inspector

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​

​ 對象,最終結構如下所示。

深入了解 Node.js 的 Inspector

分析完後我們繼續看 ​

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

這時候結構如下:

深入了解 Node.js 的 Inspector

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

執行完後形成以下結構:

深入了解 Node.js 的 Inspector

接着我們看啟動伺服器的邏輯:

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

執行完後結構如下:

深入了解 Node.js 的 Inspector

接着看一下 ​

​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​

​ 的映射關系。結構如下圖所示:

深入了解 Node.js 的 Inspector

接着看一下 ​

​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 協定進行通信.

我們看一下這時候的結構圖:

深入了解 Node.js 的 Inspector

至此,就完成了連接配接處理的分析!(撒花,你學廢了麼)

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​

​ 中記錄映射關系。結構圖如下:

深入了解 Node.js 的 Inspector

接下來看一下 ​

​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 的 Inspector

用戶端到 ​

​Node.js​

​​ 到 ​

​V8 Inspector​

​ 的整體架構如下:

深入了解 Node.js 的 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​

​ 可寫的時候發送資料給用戶端。

4 總結

繼續閱讀