聊聊Debug里的那些协议

avatar
FE @字节跳动

背景

debug的重要性对于我们来说不言而喻,那debug背后又有多少协议在做着默默地付出呢,这也主要是本文要介绍的内容。

Node Debug

Debug的划分

image.png

如何debug不在这里赘述,文档很多,本文会关注在debug里的一些细节上。

把大象🐘装进冰箱需要几步

一个test.js

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

众所周知,我们可以通过node --inspect(-brk) 来启动一个debug server。

此时命令行会输出Debugger listening on ws://127.0.0.1:9229/e0bb9202-8a42-4637-9bf1-2a714a17e383 ,这样就代表debug server启动成功了。

那接下来如何才能调试呢,既然是个websocket服务,那我们可以尝试通过websocket进行连接

const socket = new WebSocket('ws://127.0.0.1:9229/e0bb9202-8a42-4637-9bf1-2a714a17e383');

当我们在连接websocket后,你就会在命令行里看到Debugger attached. 的输出。

结论

启动一个websocket服务 -> connect (attatch)

  1. 先通过 node --inspect(-brk) 来激活debug服务
  1. 通过ws链接debug server ,连接成功后控制台就会打印 Debugger attached

目前的架构

image.png

所以,现在的问题就变成了如何实现一个debug client了,在我们自己实现之前我们可以先参照下一个著名的client: chrome dev tool

聊聊Chrome dev tool

在debug服务启动后,我们可以通过访问http://localhost:9229/json来看到一些debug信息

[  {    "description": "node.js instance",    "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=localhost:9229/19c4418d-9cfc-4535-b27b-a4825641dbf5",    "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=localhost:9229/19c4418d-9cfc-4535-b27b-a4825641dbf5",    "faviconUrl": "https://nodejs.org/static/images/favicons/favicon.ico",    "id": "19c4418d-9cfc-4535-b27b-a4825641dbf5",    "title": "test.js",    "type": "node",    "url": "file:///Users/ld/Documents/test/pptr-test/test.js",    "webSocketDebuggerUrl": "ws://localhost:9229/19c4418d-9cfc-4535-b27b-a4825641dbf5"  }]

使用打开devtoolsFrontendUrlCompat所对应的地址,会看到一个熟悉的界面。

那么我们debug过程中是如何交互的呢

任何网络的问题,都可以通过抓包工具 charles 或 wireshark 分析定位解决。

我们尝试使用wireshark抓取本地包,我们通过一个get动作(将鼠标移到res上来获取res的值)来观察报文:

  • 请求报文

  • 响应报文

从以上截图可以看到,当我们ui界面上鼠标移入res上后,会发出Runtime.getProperties请求,之后server端会给我们返回res这个值的信息,然后再显示到ui界面上。也就是说我们在ui界面上做的每一个动作(比如next 步进等操作),都会对应一个method,我们把这些一一对应的集合称之为CDP协议

CDP协议以及PPTR

CDP协议(Chrome DevTools Protocol)

CDP协议贯穿在chrome浏览器的方方面面,它是我们跟浏览器沟通的桥梁。

image.png

点击此处了解更多关于CDP协议

如何更方便的看到CDP协议呢

可以通过开启protocol monitor来在chrome dev tool查看。

如果没有Protocol monitor选项,可以在设置 - 实验特性里打开。

举个🌰 - 从CDP协议看console面板执行脚本

可以看出在console面板上执行一段js脚本后的CDP消息

  • Runtime.compileScript
  • Runtime.evaluate
  • Debugger.scriptParsed
  • Debugger.scriptParsed
  • Runtime.consoleAPICalled

再举个🌰 - 从CDP协议看profiler

我们一般会通过Memory面板来排查内存泄露问题,那么它涉及到哪些CDP协议呢。

  • HeapProfiler.takeHeapSnapshot
  • HeapProfiler.reportHeapSnapshotProgress
  • HeapProfiler.addHeapSnapshotChunk
  • Runtime.getHeapUsage

CDP可以用来做什么

  • 可以用来观测chrome的行为,帮我们深度了解chrome,比如我们在console里执行一段脚本,浏览器会在背地里做出哪些事情。
  • 浏览器自动化脚本的基石(比如pptr - puppeteer)
    • pptr的实现 pptr的很多方法也是基于CDP来实现的,比如jsHandle.getProperties()这个方法。

      • async getProperties(): Promise<Map<string, JSHandle>> {
            // 通过发送 Runtime.getProperties 消息来获取值
            const response = await this._client.send('Runtime.getProperties', {
              objectId: this._remoteObject.objectId,
              ownProperties: true,
            });
        }
        
    • lighthouse

      •     而我们常用的lighthose 就是基于pptrcdp的。

从架构图上可以看到,DAP在其中有着举足轻重的作用。

那么,lighthouse是如何获取我们一些常见的性能数据呢(FP、FCP、LCP等),这里依赖到了DAP里的tracing模块了。

  • 由上图可知,主要是通过Tracing.start Tracing.end 以及 Tracing.dataCollected Tracing.compolete组合完成Performance的信息收集。
    • 扩展pptr的能力
    •   灵活使用PPTR里的CDPSession,可以实现PPTR提供的api所不能实现的事情,也就是说,如果PPTR提供的api无法满足你的诉求,可以考虑使用底层的CDP来去实现,比如PPTR无法拦截由servierworker发出的请求,但可以通过CDPFetch模块进行拦截设置,如下所示:
const client = await target.createCDPSession();
await client.send("Fetch.enable");

client.on("Fetch.requestPaused", async event => {
  const { requestId, request } = event;
  if (/cdn-tos(-[a-zA-Z0-9]+)*.bytedance.net/.test(request.url)) {
    await client.send('Fetch.failRequest', { requestId, errorReason: 'Failed' });
  } else {
    await client.send("Fetch.continueRequest", { requestId });
  }
});

Golang Debug

通过vscode来启动debug,可以从输出的信息来看,启动了一个DAP Server(一个TCP服务)

我们还是通过get的方式来举例子,比如要获取pathname的信息,然后通过wireshark抓包来观察报文。

Content-Length: 119
{"command":"evaluate","arguments":{"expression":"pathname","frameId":1000,"context":"hover"},"type":"request","seq":37}

{"seq":0,"type":"response","request_seq":37,"success":true,"command":"evaluate","body":{"result":""/react"","presentationHint":{},"variablesReference":0}}

实现一个 Debug Client 其实成本挺高的,你需要吃透所有的调试协议,如 V8 Debugging Protocol,包含了几十个指令,每个指令都需要进行通讯适配和 UI 适配,这还只是一种语言,如果你的 IDE 面向多种语言,你就需要适配多种调试协议,不同协议之前的差异可能还挺大的,这些工作完全会让你崩溃。

另外,站在社区的角度,这种建设是可以被抽象的,试想一下,Atom 调试 Node.js 需要自己实现一个 Debug Client,Sublime Text 如此、VS Code 如此,你自建 IDE 也是如此,是不是完全没必要,因此就有了 Debug Adaptor Protocol,它是微软提出的一套 通用调试协议,目前已经成为了社区的事实标准。

DAP(Debug Adapter Protocol)

DAP的意义

The Debug Adapter Protocol makes it possible to implement a generic debugger for a development tool that can communicate with different debuggers via Debug Adapters. And Debug Adapters can be re-used across multiple development tools which significantly reduces the effort to support a new debugger in different tools.

DAP协议规范

microsoft.github.io/debug-adapt…

主要内容包含了 Request Response Event三个模块的定义规范,类型如下:

interface ProtocolMessage {
  seq: number;
  type: 'request' | 'response' | 'event' | string;
}

interface Request extends ProtocolMessage {
  type: 'request';
  command: string;
  arguments?: any;
}

interface Response extends ProtocolMessage {
  type: 'response';
  request_seq: number;
  success: boolean;
  command: string;
  message?: 'cancelled' | string;
  body?: any;
}

interface Event extends ProtocolMessage {
  type: 'event';
  event: string;
  body?: any;
}

现有的adapter

microsoft.github.io/debug-adapt…

实现一个debug client

vscode-node-debug2

vscode源码 以及 cloudide debug

github.com/erha19/mona…

可以参考这个项目

总结

  1. debug的背后存在着CDP DAP,以及网络层的Websocket TCP等协议
  2. 了解CDP可以对我们理解chrome的运行机制大有裨益,也对我们熟练操作pptr来操控chromium做更多自动化的事情。
  3. 了解DAP可以对我们理解各种语言的debug有所帮助