李恒道 发表于 2023-1-14 21:01:41

自写Webscoket实现CDP协议联通

# 前文
跟一之哥有了那个想法
被毒打之后
个人还是有一点不服
于是决定开始尝试CDP协议的分析和学习
# 原理介绍
Chrome的调试和开发者工具实际是分离的
chrome内部起一个webscoket服务器
如果是使用Chrome内置的开发者工具
并不会使用Webscoket
而是使用chrome内部的全局函数进行通信
但是其大致原理是不变的
我们可以将chrome内部的服务器称之为backed
而将vscode或者f12等进行调试的称之为fronted
通信的协议称之为CDP协议
而诸如Websocket,或全局函数称之为通信的媒介
以上来自于神光大佬的前端调试通关秘籍
网址https://s.juejin.cn/ds/kuLGA1b/
大家有兴趣也可以买一本
# 实践
那我们可以开始尝试一下,首先创建一个快捷方式,在末尾加上
--remote-debugging-port=9222
然后启动
注意这里,启动之前必须打开任务管理器确保没有chrome
有时候可能挂在后台进程,导致启动合并而没有打开端口
接下来我们尝试一下链接
这里我用的ws库
```js
var WebSocketClient = require("ws");
const chromePort = 9222;
var chromeSocket = new WebSocketClient("ws://localhost:" + chromePort);
```
发现失败了
为什么?
我们可以看一个已经完成的实现【chrome-remote-interface】
他的使用方式还是挺简单的,我们看一下demo
```js
async function example() {
let client;
try {
    // connect to endpoint
    client = await CDP();
    // extract domains
    const { Network, Page } = client;
    // setup handlers
    // enable events then start!
    await Network.enable();
    await Page.enable();
    await Page.navigate({ url: "https://github.com" });
    await Page.loadEventFired();
} catch (err) {
    console.error(err);
} finally {
    if (client) {
      await client.close();
    }
}
}

example();
```
那我们就去看看CDP源码
```js
function CDP(options, callback) {
    if (typeof options === 'function') {
      callback = options;
      options = undefined;
    }
    const notifier = new EventEmitter();
    if (typeof callback === 'function') {
      // allow to register the error callback later
      process.nextTick(() => {
            new Chrome(options, notifier);
      });
      return notifier.once('connect', callback);
    } else {
      return new Promise((fulfill, reject) => {
            notifier.once('connect', fulfill);
            notifier.once('error', reject);
            new Chrome(options, notifier);
      });
    }
}
```
可以发现默认应该是调用了new Chrome
我们继续往里看
```js
class Chrome extends EventEmitter {
    constructor(options, notifier) {
      super();
      // options
      const defaultTarget = (targets) => {
            // prefer type = 'page' inspectable targets as they represents
            // browser tabs (fall back to the first inspectable target
            // otherwise)
            let backup;
            let target = targets.find((target) => {
                if (target.webSocketDebuggerUrl) {
                  backup = backup || target;
                  return target.type === 'page';
                } else {
                  return false;
                }
            });
            target = target || backup;
            if (target) {
                return target;
            } else {
                throw new Error('No inspectable targets');
            }
      };
      options = options || {};
      this.host = options.host || defaults.HOST;
      this.port = options.port || defaults.PORT;
      this.secure = !!(options.secure);
      this.useHostName = !!(options.useHostName);
      this.alterPath = options.alterPath || ((path) => path);
      this.protocol = options.protocol;
      this.local = !!(options.local);
      this.target = options.target || defaultTarget;
      // locals
      this._notifier = notifier;
      this._callbacks = {};
      this._nextCommandId = 1;
      // properties
      this.webSocketUrl = undefined;
      // operations
      this._start();
    }
}
```
可以没什么操作,调用了_start()
我们继续往里看
```js
    async _start() {
      const options = {
            host: this.host,
            port: this.port,
            secure: this.secure,
            useHostName: this.useHostName,
            alterPath: this.alterPath
      };
      try {
            // fetch the WebSocket debugger URL
            const url = await this._fetchDebuggerURL(options);
            // allow the user to alter the URL
            const urlObject = parseUrl(url);
            urlObject.pathname = options.alterPath(urlObject.pathname);
            this.webSocketUrl = formatUrl(urlObject);
            // update the connection parameters using the debugging URL
            options.host = urlObject.hostname;
            options.port = urlObject.port || options.port;
            // fetch the protocol and prepare the API
            const protocol = await this._fetchProtocol(options);
            api.prepare(this, protocol);
            // finally connect to the WebSocket
            await this._connectToWebSocket();
            // since the handler is executed synchronously, the emit() must be
            // performed in the next tick so that uncaught errors in the client code
            // are not intercepted by the Promise mechanism and therefore reported
            // via the 'error' event
            process.nextTick(() => {
                this._notifier.emit('connect', this);
            });
      } catch (err) {
            this._notifier.emit('error', err);
      }
    }
```
发现这里进行了Websocket的链接
await this._connectToWebSocket();
追一下发现他直接进行了链接,传入的是webSocketUrl
![图片.png](data/attachment/forum/202301/14/210651t2h6gehryzh3zthr.png)
我们可以发现他用的也是ws库,那基本可能就是webSocketUrl的问题了
![图片.png](data/attachment/forum/202301/14/210704xmcfxringrz7o1ox.png)
我们看一下他的webSocketUrl哪里来的
搜一下找到了_start里
```js
            // fetch the WebSocket debugger URL
            const url = await this._fetchDebuggerURL(options);
            // allow the user to alter the URL
            const urlObject = parseUrl(url);
            urlObject.pathname = options.alterPath(urlObject.pathname);
            this.webSocketUrl = formatUrl(urlObject);
```
看一下fetchDebuggerURL里
```js
    // fetch the WebSocket URL according to 'target'
    async _fetchDebuggerURL(options) {
      const userTarget = this.target;
      switch (typeof userTarget) {
      case 'string': {
            let idOrUrl = userTarget;
            // use default host and port if omitted (and a relative URL is specified)
            if (idOrUrl.startsWith('/')) {
                idOrUrl = `ws://${this.host}:${this.port}${idOrUrl}`;
            }
            // a WebSocket URL is specified by the user (e.g., node-inspector)
            if (idOrUrl.match(/^wss?:/i)) {
                return idOrUrl; // done!
            }
            // a target id is specified by the user
            else {
                const targets = await devtools.List(options);
                const object = targets.find((target) => target.id === idOrUrl);
                return object.webSocketDebuggerUrl;
            }
      }
      case 'object': {
            const object = userTarget;
            return object.webSocketDebuggerUrl;
      }
      case 'function': {
            const func = userTarget;
            const targets = await devtools.List(options);
            const result = func(targets);
            const object = typeof result === 'number' ? targets : result;
            return object.webSocketDebuggerUrl;
      }
      default:
            throw new Error(`Invalid target argument "${this.target}"`);
      }
    }
```
根据userTarget判断不同类型,他来自于this.target;
如果不给任何参数,默认来自于
```js
      const defaultTarget = (targets) => {
            // prefer type = 'page' inspectable targets as they represents
            // browser tabs (fall back to the first inspectable target
            // otherwise)
            let backup;
            let target = targets.find((target) => {
                if (target.webSocketDebuggerUrl) {
                  backup = backup || target;
                  return target.type === 'page';
                } else {
                  return false;
                }
            });
            target = target || backup;
            if (target) {
                return target;
            } else {
                throw new Error('No inspectable targets');
            }
      };
```
相当于获取所有targets,看代码来说基本是获取第一个存在webSocketDebuggerUrl,并且type为page的,那targets哪里来的?
我们找到devtools.List(options)里,发现了
```js
function List(options, callback) {
    devToolsInterface('/json/list', options, (err, tabs) => {
      if (err) {
            callback(err);
      } else {
            callback(null, JSON.parse(tabs));
      }
    });
}
```
devToolsInterface类似于直接访问网页
我们直接访问http://127.0.0.1:9222/json/list
![图片.png](data/attachment/forum/202301/14/211433ym02av00vzk0xkla.png)
就找到了socket的地址,那我们自己实现一下
```js
var WebSocketClient = require("ws").WebSocket;
var axios = require("axios");
const chromePort = 9222;
function getWebscoketUrl() {
return new Promise((resolve) => {
    axios
      .get("http://127.0.0.1:" + chromePort + "/json/list")
      .then((response) => {
      const target = response.data.find((item) => {
          return (
            item.webSocketDebuggerUrl !== undefined && item.type === "page"
          );
      });
      resolve(target.webSocketDebuggerUrl);
      });
});
}
async function main() {
const getWebsocketUrl= await getWebscoketUrl();
var chromeSocket = new WebSocketClient(getWebsocketUrl);
chromeSocket.onopen = function (e) {
console.log("Connection to server opened");
    };
}
main();
```
测试发现正常提示了Connection to server opened
那我们就完成了对CDP的连结
# 结语
撒花~

脚本体验师001 发表于 2023-1-15 19:18:27

我就知道,我就知道没那么简单
道哥哥一开动脑筋必定飞沙走石鬼见愁,惊雷这通天修为天塌地陷紫金锤

哥哥知道吗,卓别林唯一一点比不上希特勒的地方是卓别林没有手下

李恒道 发表于 2023-1-15 22:00:09

脚本体验师001 发表于 2023-1-15 19:18
我就知道,我就知道没那么简单
道哥哥一开动脑筋必定飞沙走石鬼见愁,惊雷这通天修为天塌地陷紫金锤



笑死了
哥哥都喊上社会语录了

王一之 发表于 2023-1-17 09:59:22

## 什么是 cdp 协议
cdp 协议简称 chrome 调试协议,是基于 scoket(websocket、usb、adb )消息的 json rpc 协议。用来调用 chrome 内部的方法实现 js,css ,dom 的开发调试。
可以将 实现了 cdp 协议的应用 看做 rpc 调用的服务端( chrome ,puppeteer), 将调试面板看做 rpc 调用的客户端(devtools)。

哥哥用websocket实现了也可以,学习了!
页: [1]
查看完整版本: 自写Webscoket实现CDP协议联通