Node.js調試指南

現今 Node.js 愈發受歡迎,應用場景也愈來愈多,學會高效調試 Node.js 會讓平常開發更高效。下面講下使用inspector調試nodejs程序html

Node6.3+ 的版本提供了兩個用於調試的協議:v8 Debugger Protocolv8 Inspector Protocol 可使用第三方的 Client/IDE 等監測和介入 Node(v8) 運行過程,進行調試。node

v8 Inspector Protocol 是新加入的調試協議,經過 websocket (一般使用 9229 端口)與 Client/IDE 交互,同時基於 Chrome/Chromium 瀏覽器的 devtools 提供了圖形化的調試界面。git

1 開啓調試

1.1 調試服務器代碼

若是你的腳本搭建http或者net服務器,你能夠直接使用--inspectgithub

const Koa = require('koa')
const app = new Koa()

app.use(async ctx => {
  
  let a = 0
  const longCall = () => { 
    while (a < 10e8) { 
      a++
    }
  }
  longCall()
  ctx.body = `Hello ${a}`
})

app.listen(3000, () => { 
  console.log('程序監聽了3000端口')
})
複製代碼

使用 node --inspect=9229 app.js 啓動你的腳本,9229 是指定的端口號web

# 控制檯會輸出以下:
/usr/local/bin/node --inspect=9229 src/inspector/demo.js 
Debugger listening on ws://127.0.0.1:9229/c4f1e345-e811-47a2-b44a-65f68c0c2cc3
Debugger attached.
# 能夠在瀏覽器裏打開:http://127.0.0.1:9229/json 看到一些信息, c4f1e345-e811-47a2-b44a-65f68c0c2cc3 爲uuid,不一樣調試面板的uuid來區分;
複製代碼

--inspect 對於通常的程序都是一閃而過,斷點信號還沒發送出去,就執行完畢了。 斷點根本不起做用,能夠--inspect-brkchrome

1.2 調試腳本代碼

若是你的腳本運行完以後直接結束進程,那麼你須要使用--inspect-brk來啓動調試器,這樣使得腳本能夠代碼執行以前break,不然,整個代碼直接運行到代碼結尾,結束進程,根本沒法進行調試。json

node --inspect-brk=9229 app.js瀏覽器

2 調試工具接入

2.1 VS Code

Vs Code 內置了 Node debugger ,支持 v8 Debugger Protocolv8 Inspector Protocol 兩種協議。對於 v8 Inspector Protocol ,只須要在配置裏添加一條 Attach 類型配置服務器

Debug 控制面板, 點擊 settings 圖標,打開 .vscode/launch.json. 點擊 「Node.js」 進行初始配置便可.websocket

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/app.js"
    }
  ]
}
複製代碼

2.2 Chrome DevTools

  • 方法1:在Chrome瀏覽器打開 chrome://inspect點擊Configure按鈕,肯定host和端口在列表中。

  • 方法2:從上述host和端口/json/list複製 devtoolsFrontendUrl–-inspect 提示信息,並複製到Chrome.

2.2.1 Console Panel

chrome 接入要調試的 node 進程後,能夠在 Console 中代理 Node 進程中全部的控制檯輸出,提供了靈活的 Filter 過濾功能,還能夠在 Node 進程代碼的上下文中直接執行代碼。

2.2.2 Sources Panel

Sources 中能夠查看全部加載的腳本,還包括第三方庫和Node 核心庫,選中文件能夠進行編輯,Ctrl + C 保存能夠直接修改運行中的腳本。

2.2.3 Profile Panel

Profile 用於對運行中的腳本進行性能監測,包括CPU和內存的使用,CPU profile,能夠記錄時間線上 Javascript 函數執行時佔用的 CPU 時間.

profile 記錄時間段有兩種

  • 手動開始/中止:單擊 start 開始記錄,單擊 stop 中止記錄
  • 在代碼中插入開始/中止的 API 調用 console.profile('tag') console.profileEnd('tag') ,能夠在 Sources 面板中直接編輯保存代碼,而後 F5 刷新一下。

profile有三種視圖

  • chart:俗稱火焰圖,以時間爲橫軸顯示函數調用棧。下面簡單舉例分析

火焰圖的函數調用棧是倒置的,最上面爲棧底,最下面爲棧頂。一個棧是一個 tick ,一個 tick 必定是由 Node 底層開始調用的,在 Node 中使用 process.nextTick(fn)setTimeout(fn, deloy) 的系統回調會產生新的 tick ,對應產生新的調用棧。

函數的調用順序是從棧底到棧頂。上圖中第一個棧 parserOnHeadersComplete 由底層調用,parserOnHeadersComplete 中調用了 parserOnIncomingparserOnIncoming 中調用了emit...依次類推。

調用棧的寬度是函數執行的時間。一個函數的執行時間包含了其內部調用其餘函數的執行時間,因此相對靠近棧底的函數的調用時間必定比靠近棧頂的函數的調用時間長。除去內部調用其餘函數的執行時間,就是當前函數的執行時間。

點擊函數會跳轉到 Sources 面板中函數定義的位置。

將鼠標懸停在函數上可顯示其名稱和數據:

下面解釋摘自 chrome-devtools 文檔

  • Name:函數的名稱。

  • Self time:完成函數當前的調用所需的時間,僅包含函數自己的聲明,不包含函數調用的任何函數。

  • Total time: 完成此函數和其調用的任何函數當前的調用所需的時間。

  • URL:形式爲 file.js:100 的函數定義的位置,其中 file.js 是定義函數的文件名稱,100 是定義的行號。

  • Aggregated self time:記錄中函數全部調用的總時間,不包含此函數調用的函數。

  • Aggregated total time: 函數全部調用的總時間,不包含此函數調用的函數。

  • Not optimized:若是分析器已檢測出函數存在潛在的優化,會在此處列出。

  • heavy(Bottom Up):統計數據,自底向上,底指的是火焰圖的底。

  • tree(Top Down):統計數據,自頂向下,頂指的是火焰圖的頂。

能夠看到程序大部分時間是消耗在longCall這個函數的調用上;

2.2.4 Memory profile

堆分析器能夠按頁面的 JavaScript 對象和相關 DOM 節點顯示內存分配(另請參閱對象保留樹)。使用分析器能夠拍攝 JS 堆快照分析內存圖比較快照以及查找內存泄漏.

3. Node Inspector 代理實現

經過 node inspector 來進行斷點調試是一個很經常使用的 debug 方式。可是之前的調試中有幾個問題會致使咱們的調試效率下降。

  • vscode 中調試,在 inspector 端口變動或者 websocket id 變動後要重連。
  • devtools 中調試,在inspector 端口變動或者 websocket id 變動後要重連。

那 node inspector是如何解決上述兩個問題呢?

對於第一個問題,在 vscode 上,它是會本身去調用 /json 接口獲取最新的 websocket id,而後使用新的 websocket id 鏈接到 node inspector 服務上。所以解決方法就是實現一個 tcp 代理功能作數據轉發便可。

對於第二個問題,因爲 devtools 是不會自動去獲取新的 websocket id 的,因此咱們須要作動態替換,因此解決方案就是代理服務去 /json 獲取 websocket id,而後在 websocket 握手的時候將 websocket id 進行動態替換到請求頭上。

畫了一張流程圖:

3.1 Tcp 代理

首先,先實現一個 tcp 代理的功能,其實很簡單,就是經過 nodenet 模塊建立一個代理端口的 Tcp Server,而後當有鏈接過來的時候,再建立一個鏈接到目標端口便可,而後就能夠進行數據的轉發了。

簡易的實現以下:

const net = require('net');
const proxyPort = 9229;
const forwardPort = 5858;

net.createServer(client => {
  const server = net.connect({
    host: '127.0.0.1',
    port: forwardPort,
  }, () => {
    client.pipe(server).pipe(client);
  });
  // 若是真要應用到業務中,還得監聽一下錯誤/關閉事件,在鏈接關閉時即時銷燬建立的 socket。
}).listen(proxyPort);
複製代碼

上面實現了比較簡單的一個代理服務,經過 pipe 方法將兩個服務的數據連通起來。client 有數據的時候會被轉發到 server 中,server 有數據的時候也會轉發到 client 中。

當完成這個 Tcp 代理功能以後,就已經能夠實現 vscode 的調試需求了,在 vscode 中項目下 launch.json 中指定端口爲代理端口,在 configurations 中添加配置

{
  "type": "node",
  "request": "attach",
  "name": "Attach",
  "protocol": "inspector",
  "restart": true,
  "port": 9229
}
複製代碼

那麼當應用重啓,或者更換 inspect 的端口,vscode 都能自動從新經過代理端口 attach 到你的應用。

3.2 獲取 websocketId

這一步開始,就是爲了解決 devtools連接不變的狀況下可以從新 attach 的問題了,在啓動 node inspector server的時候,inspector 服務還提供了一個 /jsonhttp 接口用來獲取 websocket id

這個就至關簡單了,直接發個 http 請求到目標端口的 /json,就能夠獲取到數據了:

[ { description: 'node.js instance',
    devtoolsFrontendUrl: '...',
    faviconUrl: 'https://nodejs.org/static/favicon.ico',
    id: 'e7ef6313-1ce0-4b07-b690-d3cf5274d8b0',
    title: '/Users/wanghx/Workspace/larva-team/vscode-log/index.js',
    type: 'node',
    url: 'file:///Users/wanghx/Workspace/larva-team/vscode-log/index.js',
    webSocketDebuggerUrl: 'ws://127.0.0.1:5858/e7ef6313-1ce0-4b07-b690-d3cf5274d8b0' } ]
複製代碼

上面數據中的 id 字段,就是咱們須要的 websocket id 了。

3.3 Inspector 代理

拿到了 websocket id後,就能夠在 tcp 代理中作 websocket id 的動態替換了,首先咱們須要固定連接,所以先定一個代理連接,好比個人代理服務端口是 9229,那麼 chrome devtools 的代理連接就是:

chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/__ws_proxy__

上面除了最後面的 ws=127.0.0.1:9229/__ws_proxy__ 其餘都是固定的,而最後這個也一眼就能夠看出來是 websocket 的連接。其中 __ws_proxy__則是用來佔位的,用於在 chrome devtools 向這個代理連接發起 websocket 握手請求的時候,將 __ws_proxy__ 替換成 websocket id 而後轉發到 nodeinspector 服務上。

對上面的 tcp 代理中的 pipe 邏輯的代碼作一些小修改便可。

const through = require('through2')

client
    .pipe(through.obj((chunk, enc, done) => {
        if (chunk[0] === 0x47 && chunk[1] === 0x45 && chunk[2] === 0x54) {
          const content = chunk.toString();
          if (content.includes('__ws_proxy__')) {
            return done(null, Buffer.from(content.replace('__ws_proxy__', websocketId)));
          }
        }
        done(null, chunk);
      }))
    .pipe(server)
    .pipe(client)
複製代碼

經過 through2 建立一個 transform 流來對傳輸的數據進行一下更改。

簡單判斷一下 chunk 的頭三個字節是否爲GET,若是是 GET 說明這多是個 http 請求,也就多是 websocket 的協議升級請求。把請求頭打印出來就是這個樣子的:

GET /__ws_proxy__ HTTP/1.1
Host: 127.0.0.1:9229
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: chrome-devtools://devtools
Sec-WebSocket-Version: 13
複製代碼

而後將其中的路徑/__ws_proxy替換成對應的 websocketId,而後轉發到 nodeinspector server 上,便可完成websocket 的握手,接下來的 websocket 通訊就不須要對數據作處理,直接轉發便可。

接下來就算各類重啓應用,或者更換 inspector 的端口,都不須要更換 debug 連接,只須要再 inspector server 重啓的時候,在下圖的彈窗中

點擊一下 Reconnect DevTools 便可恢復 debug。

參考:Node Inspector 代理實現

相關文章
相關標籤/搜索