本文首发于 https://github.com/whxaxes/blog/issues/9html
平时作 node 开发的时候,经过 node inspector 来进行断点调试是一个很经常使用的 debug 方式。可是有几个问题会致使咱们的调试效率下降。node
问题一:当使用 vscode 进行断点调试时,若是应用是经过 cluster 启动的 inspector,那么每次当 worker 挂了重启后,inspector 的端口都会自增。虽然在 node8.x 版本中能够指定 inspectPort 固定调试端口,可是在 node6.x 中是不支持的。这样会致使每次 worker 重启了就得在 vscode 中从新指定调试端口。git
问题二:当使用 devtools 调试的时候,每次调试都须要拷贝 devtools 连接到 chrome 上调试,而上面说的端口变动问题则会致使 devtools 的连接变动,除此以外,每次从新启动 inspector 也会致使 devtools 的连接变动,由于 websocket id 变了。github
而把上面的两个问题简化一下就是:web
目前业界已经有解决方案就是 chrome 插件 Node Inspector Manager(Nim)
,不过这个只能解决在同个 inspector 端口下的应用重启后连接更改的问题,却没法解决 cluster 启动致使的端口自增问题,除非在 Nim 中提早指定好多个端口,再者 Nim 是 chrome 上的插件,对于在 vscode 中的调试却无能为力了。chrome
因此最佳的解决方案天然是使用 node 来作 inspector 代理,解决方案以下:json
对于第一个问题,在 vscode 上,它是会本身去调用 /json 接口获取最新的 websocket id,而后使用新的 websocket id 链接到 node inspector 服务上。所以解决方法就是实现一个 tcp 代理功能作数据转发便可。bash
对于第二个问题,因为 devtools 是不会自动去获取新的 websocket id 的,因此咱们须要作动态替换,因此解决方案就是代理服务去 /json 获取 websocket id,而后在 websocket 握手的时候将 websocket id 进行动态替换到请求头上。websocket
画了一张流程图:socket
首先,先实现一个 tcp 代理的功能,其实很简单,就是经过 node 的 net 模块建立一个代理端口的 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 到你的应用。
这一步开始,就是为了解决 devtools 连接不变的状况下可以从新 attach 的问题了,在启动 node inspector server 的时候,inspector 服务还提供了一个 /json 的 http 接口用来获取 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 了。
拿到了 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 而后转发到 node 的 inspector 服务上。
对上面的 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,而后转发到 node 的 inspector server 上,便可完成 websocket 的握手,接下来的 websocket 通讯就不须要对数据作处理,直接转发便可。
接下来就算各类重启应用,或者更换 inspector 的端口,都不须要更换 debug 连接,只须要再 inspector server 重启的时候,在下图的弹窗中
点击一下 Reconnect DevTools 便可恢复 debug。
上面的详细代码能够在下面的 git 中找到: