浅谈Node Inspector 代理实现


Posted in Javascript onOctober 19, 2017

背景

平时做 node 开发的时候,通过 node inspector 来进行断点调试是一个很常用的 debug 方式。但是有几个问题会导致我们的调试效率降低。

问题一:当使用 vscode 进行断点调试时,如果应用是通过 cluster 启动的 inspector,那么每次当 worker 挂了重启后,inspector 的端口都会自增。虽然在 node8.x 版本中可以指定 inspectPort 固定调试端口,但是在 node6.x 中是不支持的。这样会导致每次 worker 重启了就得在 vscode 中重新指定调试端口。

问题二:当使用 devtools 调试的时候,每次调试都需要拷贝 devtools 链接到 chrome 上调试,而上面说的端口变更问题则会导致 devtools 的链接变更,除此之外,每次重新启动 inspector 也会导致 devtools 的链接变更,因为 websocket id 变了。

而把上面的两个问题简化一下就是:

  • 在 vscode 中调试,在 inspector 端口变更或者 websocket id 变更后能够重连。
  • 在 devtools 中调试,在 inspector 端口变更或者 websocket id 变更后能够重连。

解决方案

目前业界已经有解决方案就是 chrome 插件 Node Inspector Manager(Nim) ,不过这个只能解决在同个 inspector 端口下的应用重启后链接更改的问题,却无法解决 cluster 启动导致的端口自增问题,除非在 Nim 中提前指定好多个端口,再者 Nim 是 chrome 上的插件,对于在 vscode 中的调试却无能为力了。

所以最佳的解决方案自然是使用 node 来做 inspector 代理,解决方案如下:

对于第一个问题,在 vscode 上,它是会自己去调用 /json 接口获取最新的 websocket id,然后使用新的 websocket id 连接到 node inspector 服务上。因此解决方法就是实现一个 tcp 代理功能做数据转发即可。

对于第二个问题,由于 devtools 是不会自动去获取新的 websocket id 的,所以我们需要做动态替换,所以解决方案就是代理服务去 /json 获取 websocket id,然后在 websocket 握手的时候将 websocket id 进行动态替换到请求头上。

画了一张流程图:

浅谈Node Inspector 代理实现

实现步骤

一、Tcp 代理

首先,先实现一个 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 到你的应用。

二、获取 websocketId

这一步开始,就是为了解决 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 了。

三、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 然后转发到 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 重启的时候,在下图的弹窗中

浅谈Node Inspector 代理实现

点击一下 Reconnect DevTools 即可恢复 debug。

最后

上面的详细代码可以在下面的 git 中找到:

  • Tcp 代理:https://github.com/whxaxes/tcp-proxy.js
  • Inspector 代理:https://github.com/whxaxes/inspector-proxy

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
xml 封装与解析(javascript和C#中)
Jul 26 Javascript
jQuery对表单的操作代码集合
Apr 06 Javascript
一个分享按钮的插件使用介绍(可扩展,内附开发制作流程)
Sep 19 Javascript
浅析jQuery 3.0中的Data
Jun 14 Javascript
原生js更改css样式的两种方式
Mar 15 Javascript
vue.js实现条件渲染的实例代码
Jun 22 Javascript
深入理解Vue transition源码分析
Jul 30 Javascript
微信小程序实现图片懒加载的示例代码
Dec 13 Javascript
webpack4 css打包压缩问题的解决
May 18 Javascript
JavaScript实现浅拷贝与深拷贝的方法分析
Jul 05 Javascript
小程序中手机号识别的示例
Dec 14 Javascript
详解Vue的sync修饰符
May 15 Vue.js
AngularJS表单验证功能
Oct 19 #Javascript
JS实现的按钮点击颜色切换功能示例
Oct 19 #Javascript
Vue如何从1.0迁移到2.0
Oct 19 #Javascript
bootstrap Table服务端处理分页(后台是.net)
Oct 19 #Javascript
安装vue-cli报错 -4058 的解决方法
Oct 19 #Javascript
BootStrap模态框不垂直居中的解决方法
Oct 19 #Javascript
微信小程序组件之srcoll-view的详解
Oct 19 #Javascript
You might like
火车头采集器3.0采集图文教程
2007/03/17 PHP
php中jQuery插件autocomplate的简单使用笔记
2012/06/14 PHP
用php来改写404错误页让你的页面更友好
2013/01/24 PHP
关于php操作mysql执行数据库查询的一些常用操作汇总
2013/06/24 PHP
php实现在服务器上创建目录的方法
2015/03/16 PHP
php简单复制文件的方法
2016/05/09 PHP
详细对比php中类继承和接口继承
2018/10/11 PHP
JavaScript 弹出窗体点击按钮返回选择数据的实现
2010/04/01 Javascript
这段js代码得节约你多少时间
2011/12/20 Javascript
一个JavaScript去除字符串末尾的空白实例代码
2014/09/22 Javascript
jQuery实现的多屏图像图层切换效果实例
2015/05/07 Javascript
JS 动态判断PC和手机浏览器实现代码
2016/09/21 Javascript
JS判断鼠标进入容器的方向与window.open新窗口被拦截的问题
2016/12/23 Javascript
xmlplus组件设计系列之文本框(TextBox)(3)
2017/05/03 Javascript
Angular 2父子组件数据传递之@Input和@Output详解(下)
2017/07/05 Javascript
Easy UI动态树点击文字实现展开关闭功能
2017/09/30 Javascript
vue form check 表单验证的实现代码
2018/12/09 Javascript
小程序:授权、登录、session_key、unionId的详解
2019/05/15 Javascript
如何对react hooks进行单元测试的方法
2019/08/14 Javascript
JavaScript制作3D旋转相册
2020/08/02 Javascript
[02:14]完美“圣”典2016风云人物:xiao8专访
2016/12/01 DOTA
Python二维码生成库qrcode安装和使用示例
2014/12/16 Python
在Python的Django框架中加载模版的方法
2015/07/16 Python
使用Python爬了4400条淘宝商品数据,竟发现了这些“潜规则”
2018/03/23 Python
Python使用docx模块实现刷题功能代码
2020/02/13 Python
django中related_name的用法说明
2020/05/20 Python
基于Python模拟浏览器发送http请求
2020/11/06 Python
HTML5和CSS3实例教程总结(推荐)
2016/07/18 HTML / CSS
德国EGOIST网店:销售畅销的设计师品牌
2017/04/18 全球购物
服务之星获奖感言
2014/01/21 职场文书
小学师德师风整改措施
2014/10/27 职场文书
2015大学生暑期实习报告
2015/07/13 职场文书
宣传部部长竞选稿
2015/11/21 职场文书
应届生们该怎么书写求职信?
2019/07/05 职场文书
Java比较两个对象中全部属性值是否相等的方法
2021/08/07 Java/Android
Golang 链表的学习和使用
2022/04/19 Golang