eyasliu / blog

博客文章
179 stars 13 forks source link

CDP (Chrome Devtools Protocol) 踩坑之旅 #32

Open eyasliu opened 3 years ago

eyasliu commented 3 years ago

什么是 CDP

Chrome Devtools Protocol, 就是 Chrome 浏览器用于开发调试的协议,Chrome 开发者工具底层就是调用的该协议,所以 chrome 开发者工具能干的事都是基于 CDP 的接口,就能想到 CDP 能干的事有多少了。事实上 CDP 有些功能在 Chrome 开发者工具并没有体现出来,也就是说 CDP 还更强大。 简单来说,CDP 就是用来控制 Chrome 的方方面面。

事实上 CDP 能控制的并不局限于 Chrome 浏览器,任何实现了该协议的工具都能被控制。比如 Node.js, Firefox(没错,Firefox也实现了 CDP,但只能在 nightly 才能开启),还有所有基于 Chromium 内核的浏览器(360极速浏览器,Oper,edge,搜狗浏览器 等等)。

而任何实现了 CDP 的工具,都能使用 Chrome 开发者工具去调试与分析。所以 Chrome 开发者工具能调试 Node.js, Chromium 内核浏览器,甚至是 Firefox。

CDP 协议版本

CDP 有好几个版本,分别对应了不同的Chrome 版本或者不同端

体验 CDP

其实打开 Chrome 开发工具就是最直接的体验了。不过这是用来调试页面的,CDP不仅于此。

启动 CDP 服务器

首先先启动 CDP 服务,这在实现了 CDP 的工具都会有说明,举例如下:

$ google-chrome --remote-debugging-port=9222 # pc chrome
$ adb forward tcp:9222 localabstract:chrome_devtools_remote # 安卓 chrome
$ opera --remote-debugging-port=9222 # Opera 浏览器
$ node --inspect=9222 script.js # Nodejs
$ msedge --remote-debugging-port=9222 # 基于 Chromium 的新版 Edge
$ MicrosoftEdge.exe --devtools-server-port 9222 about:blank # 旧版本 edge
$ firefox --remote-debugging-port 9222 # firefox nightly 浏览器

CDP 服务的默认端口就是 9222, 所以上面即使不指定端口号也是默认启动在 9222 端口上,这是个 http 服务。启动后可以浏览器直接访问 http://localhost:9222/json/version 一下试试

$ curl http://localhost:9222/json/version
{
   "Browser": "Chrome/85.0.4183.83",
   "Protocol-Version": "1.3",
   "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
   "V8-Version": "8.5.210.20",
   "WebKit-Version": "537.36 (@94abc2237ae0c9a4cb5f035431c8adfb94324633)",
   "webSocketDebuggerUrl": "ws://localhost:53686/devtools/browser/8ee011a6-03f5-4ee4-bc1e-bf35a3789af0"
}

这是最简单的一种连接方式,即 http

CDP 客户端

服务启动好了,接下来是客户端工具。

官方推荐使用 chrome-remote-interface ,这是个nodejs包,这样安装

$ npm i -g chrome-remote-interface

可以看看 chrome-remote-interface 的说明文档, chrome-remote-interface 既是一个命令行工具,也支持作为工具库编程使用。

$ chrome-remote-interface -h

  Usage: client [options] [command]

  Options:

    -v, --v              Show this module version
    -t, --host <host>    HTTP frontend host
    -p, --port <port>    HTTP frontend port
    -s, --secure         HTTPS/WSS frontend
    -n, --use-host-name  Do not perform a DNS lookup of the host
    -h, --help           output usage information

  Commands:

    inspect [options] [<target>]  inspect a target (defaults to the first available target)
    list                          list all the available targets/tabs
    new [<url>]                   create a new target/tab
    activate <id>                 activate a target/tab by id
    close <id>                    close a target/tab by id
    version                       show the browser version
    protocol [options]            show the currently available protocol descriptor

还可以交互模式使用

$ chrome-remote-interface -p 9222 inspect
>>> Target.getTargetInfo()
{
  targetInfo: {
    targetId: 'D4E5B0E2EAC5E5E2CABEF59C42CB23A1',
    type: 'page',
    title: 'GitHub: Where the world builds software · GitHub',
    url: 'https://github.com/',
    attached: true,
    browserContextId: 'B38349EC1C55BD049E626719D6C919C3'
  }
}
>>> Browser.getVersion()
{
  protocolVersion: '1.3',
  product: 'Chrome/85.0.4183.83',
  revision: '@94abc2237ae0c9a4cb5f035431c8adfb94324633',
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36',
  jsVersion: '8.5.210.20'
}
>>>

好了, CDP 的 hello world 之旅结束

CDP 开发

CDP 通常用作爬虫、自动化测试场景,所以你能在网上找到的资料,大部分都是讲 CDP 怎么开发爬虫和自动化测试。如果你要做爬虫或者自动化测试,可以先到 Awesome Chrome DevTools 先看看,看看有没有你使用的编程语言的实现。

Puppeteer

这是一个 nodejs 的包,封装了方方面面的高级 API 供使用,是做自动化测试和爬虫的首选工具。它还有其他编程语言的实现

怎么使用 puppeteer 可以出门随便转找教程,满大街都是,而我仅对他的实现感兴趣,这是一个非常好的学习 CDP 的项目,API 优雅,调试也简单,深入研究其原理后还可以自己实现一个。现在我重头开始踩坑

CDP 连接

使用 http 与 websocket 通讯

在上文启动 CDP 服务器那里介绍了CDP的默认端口号是 9222,也只是个默认值,可以随意更改的。但是如果 --remote-debugging-port=0,就有点特殊了,它会随机使用一个没被使用的端口号,而这么做也更安全可靠,因为天知道你指定的端口号会不会被占用。在启动了服务后,它会在进程的 stderr 将调试地址打印出来,这里面就有端口号了。

$ chrome --remote-debugging-port=0 2>&1 | tee

DevTools listening on ws://localhost:56707/devtools/browser/509ddc0f-e333-42bd-b85f-cffbc90143c0

一个进程在启动后,默认会分配三个通道(pipe) 0,1,2 。 0 是 stdin,1 是 stdout,2 是 stderr,上述命令含义为将 stderr 重定向到 stdout,然后 tee 读取 stdout 的数据

我们拿到了一个 websocket 地址 ws://localhost:56707/devtools/browser/509ddc0f-e333-42bd-b85f-cffbc90143c0,可以看到其实 CDP 是使用 websocket 做通信的。我们用 chrome-remote-interface 连接上试试看

$ chrome-remote-interface inspect -w ws://localhost:56707/devtools/browser/509ddc0f-e333-42bd-b85f-cffbc90143c0
>>> Browser.getVersion()
{
  protocolVersion: '1.3',
  product: 'Chrome/85.0.4183.83',
  revision: '@94abc2237ae0c9a4cb5f035431c8adfb94324633',
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36',
  jsVersion: '8.5.210.20'
}
>>> Target.getTargetInfo()
{
  targetInfo: {
    targetId: '849dd5c4-1938-4e42-98e3-321650fc374a',
    type: 'browser',
    title: '',
    url: '',
    attached: true
  }
}

挺好,能用。不过有坑,可以试试 Page.enable(),后面说。

通过新建管道通讯

通过 websocket 传输没有毛病,而且都是本地服务,速度上应该也不会多大延迟,这是通过网络传输的,还有种方式是使用 IPC(InterProcess Communication 进程间通信),相比于网络传输, IPC 的效率更高,而且完全不依赖网络,更轻量级。

$ chrome --remote-debuggin-pipe

启动后,使用 3 号通道发送数据,使用 4 号通道读取数据。这通常只会在编程时才用到。

怎么用? TODO, 参考 Puppeteer 代码

Session

上文说到启动好CDP服务后,会在 stderr 输出CDP的 websocket 地址,而且用 chrome-remote-interface 确实连接上去了,但是我尝试调用一下 Page.enable() 却报错了,这个命令是启动页面事件通知,在最初的协议版本就有,可是它报错了。

$ chrome-remote-interface inspect -w ws://localhost:56707/devtools/browser/509ddc0f-e333-42bd-b85f-cffbc90143c0
>>> Page.enable()
Uncaught ProtocolError: 'Page.enable' wasn't found
    at C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\lib\chrome.js:93:35
    at Chrome._handleMessage (C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\lib\chrome.js:256:17)
    at WebSocket.<anonymous> (C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\lib\chrome.js:234:22)
    at WebSocket.emit (events.js:314:20)
    at WebSocket.EventEmitter.emit (domain.js:486:12)
    at Receiver.receiverOnMessage (C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\node_modules\ws\lib\websocket.js:825:20)
    at Receiver.emit (events.js:314:20)
    at Receiver.EventEmitter.emit (domain.js:486:12)
    at Receiver.dataMessage (C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\node_modules\ws\lib\receiver.js:437:14) {
  request: { method: 'Page.enable', params: undefined },
  response: { code: -32601, message: "'Page.enable' wasn't found" }
}

这样连上以后,默认的会话就是浏览器本身,还不是页面,我要调用页面的协议,就报错了。正确方式是,要么直接用http连接,要么连接指定页面的 websocket。