远程代理整体架构¶
本文档将通过按照启动代理、连接客户端和调试目标所需的步骤顺序,介绍远程代理架构。
远程代理启动¶
一切始于 RemoteAgent
实现,它处理命令行参数(–remote-debugging-port),最终启动一个在 TCP 端口 9222(或命令行指定的端口)上监听的服务器。浏览器目标 websocket URL 将打印到标准错误输出。为此,此组件将三个主要的高级组件粘合在一起
server/HTTPD
这是/netwerk/
文件夹中 httpd.js 的副本。这是一个 HTTP 服务器的 JS 实现。这将用于实现 CDP 的各种 HTTP 端点。JSONHandler
实现了一些静态 URL,以及每个目标一个动态 URL。cdp/JSONHandler
实现以下三个静态 HTTP 端点/json/version
:返回有关运行时以及浏览器目标 websocket URL 的信息。/json/list
:返回所有可调试目标的列表,每个目标都有其动态 websocket URL。目前它只报告选项卡,但一旦我们支持它们,它将报告工作线程和插件。主浏览器目标是唯一未在此处列出的目标。/json/protocol
:返回一个大型字典,描述支持的协议。这目前是硬编码的,并返回完整的 CDP 协议模式,包括我们不支持的 API。我们将来打算修复此问题,并仅报告 Firefox 实现的内容。您可以连接到这些 websocket URL 以便调试。cdp/targets/TargetList
:此组件负责维护所有可调试目标的列表。目前它可以是主浏览器目标 一个特殊的目标,允许检查浏览器,但不检查任何特定选项卡。这是由
cdp/targets/MainProcessTarget
实现的,并在启动时实例化。选项卡目标 每个打开的选项卡都将有一个相关的
cdp/targets/TabTarget
在其打开时或服务器启动时为已打开的选项卡实例化。每个目标旨在专注于一个特定的上下文。此上下文通常在一个特定的环境中运行。这可以是特定的进程或线程。将来,我们很可能会支持工作线程和插件的目标。所有目标都继承自cdp/targets/Target
。
连接到 Websocket 端点¶
每个目标的 websocket URL 将通过 server/HTTPD:registerPathHandler
注册为 HTTP 端点(此注册是从 RemoteAgentParentProcess:#listen
完成的)。一旦发生 HTTP 请求,server/HTTPD
将调用传递给 registerPathHandler
的对象上的 handle
方法。对于 JSONHandler
注册的静态端点,这将调用 JSONHandler:handle
并返回一个 JSON 字符串作为 HTTP 主体。对于目标端点,它稍微复杂一些,因为它需要特殊的握手才能将 HTTP 连接转换为 WebSocket 连接。然后,WebSocket 将保持长期存在,并用于随着时间的推移检查目标。当对目标 URL 发出请求时,将调用 cdp/targets/Target:handle
,并且
将复杂的 HTTP 到 WebSocket 握手操作委托给
server/WebSocketHandshake:upgrade
。作为回报,我们检索一个 WebSocket 对象。将此 WebSocket 交给
server/WebSocketTransport
并获取一个传输对象作为回报。传输在 WebSocket 上实现了一个基本的 JSON 流。有了它,您可以在 WebSocket 连接上发送和接收 JSON 对象。将传输对象传递给新实例化的
Connection
。Connection 有两个目标通过读取 JSON 对象属性(
id
、method
、params
和sessionId
)来解释传入的 CDP 数据包。这在Connection:onPacket
中完成。通过为命令响应(
id
、result
和sessionId
)和事件(method
、params
和sessionId
)编写正确的 JSON 对象来格式化传出的 CDP 数据包。将 CDP 数据包重定向到正确的会话。一个连接可能有多个会话附加到它。
实例化默认会话。会话特定于每种目标类型,并且它们都继承自
cdp/session/Session
。例如,选项卡目标使用cdp/session/TabSession
,主浏览器目标使用cdp/session/MainProcessSession
。使用哪个会话类由 Target 子类的构造函数定义,该构造函数将会话类引用传递给cdp/targets/Target:constructor
。会话主要负责适应目标的最终跨进程/跨线程方面。我们目前正在描述的代码(cdp/targets/Target:handle
)正在父进程中运行。会话类从连接接收 CDP 命令,首先尝试在父进程中执行 Domain 命令。然后,如果目标实际上在其他上下文中运行,则会话尝试将此命令转发到此其他上下文,该上下文可以是线程或进程。通常,cdp/sessions/TabSession
将 CDP 命令转发到选项卡所在的內容进程。它还将该进程返回的命令响应以及 Domain 事件重定向回父进程,以便将其转发到连接。会话将使用DomainCache
类作为辅助工具来管理给定上下文中 Domain 实现的列表。
调试其他目标¶
从给定的连接中,您可以了解其他潜在的目标。您通常通过 Target.setDiscoverTargets()
执行此操作,它将发出 Target.targetCreated
事件,提供目标 ID。您可以通过将 ID 传递给 Target.attachToTarget()
创建新目标的新会话,它将返回会话 ID。“Target”此处是指在 cdp/domains/parent/Target.sys.mjs
中实现的 CDP Domain。这与 cdp/targets/Target
类不同,后者是远程代理的实现细节。
然后,有两种方法可以与其他目标通信
使用
Target.sendMessageToTarget()
和Target.receivedMessageFromTarget
您将通过Target.sendMessageToTarget()
命令手动发送命令,并通过Target.receivedMessageFromTarget
接收命令的响应以及事件。在这两种情况下,会话 ID 属性都会在命令或事件参数中传递,以便选择您正在与之通信的附加目标。使用
Target.attachToTarget({ flatten: true })
并包含sessionId
在 CDP 数据包中 这需要一个特殊的客户端,它将使用Target.attachToTarget()
返回的sessionId
来生成一个单独的客户端实例。此客户端将重用相同的 WebSocket 连接,但每个 CDP 数据包都将包含一个额外的sessionId
属性。这有助于区分与原始目标以及您可能附加的多个附加目标相关的数据包。
在这两种情况下,Target.attachToTarget()
都是特殊的,因为它将为您正在附加的选项卡生成 cdp/session/TabSession
。这是创建非默认会话的代码路径。默认会话与您最初连接到的目标相关,因此您不需要为此会话使用任何 ID。当您想通过单个连接调试多个目标时,您需要其他会话,这些会话将具有唯一的 ID。Target.attachToTarget
将计算此 ID 并实例化一个绑定到给定目标的新会话。此附加会话将由 Connection
类管理,当您使用扁平化会话时,它将重定向 CDP 数据包到正确的会话。
跨进程/层¶
由于目标可能在不同的上下文中运行,因此远程代理代码在不同的进程中运行。远程代理代码的主要和启动代码在父进程中运行。命令行的处理以及所有 HTTP 和 WebSocket 工作都在父进程中完成。浏览器目标也全部在父进程中实现。但是,当涉及选项卡目标时,由于选项卡在內容进程中运行,因此我们也必须在那里运行代码。让我们从 cdp/sessions/TabSession
类开始,该类已在前面描述过。我们在这里从 WebSocket 连接接收 JSON 数据包,并且我们在父进程中。在此类中,我们首先将消息路由到父进程域。如果没有域或特定方法的实现,我们将命令转发到在选项卡的內容进程中运行的 cdp/session/ContentProcessSession
。这两个会话类将相互交互,以便转发我们刚刚调用的方法的返回值,以及将任何由任一进程中实现的 Domain 发送的事件回传。
所有类的组织结构图¶
┌─────────────────────────────────────────────────┐
│ │
1 ▼ │
┌───────────────┐ 1 ┌───────────────┐ 1..n┌───────────────┐
│ RemoteAgent │──────▶│ HttpServer │◀───────▶│ JsonHandler │
└───────────────┘ └───────────────┘ 1 └───────────────┘
│
│
│ 1 ┌────────────────┐ 1
└───────────────▶│ TargetList │◀─┐
└────────────────┘ │
│ │
▼ 1..n │
┌────────────┐ │
┌─────────────────│ Target [1]│ │
│ └────────────┘ │
│ ▲ 1 │
▼ 1..n │ │
┌────────────┐ 1..n┌────────────┐ │
│ Connection │◀─────────▶│ Session [2]│──────┘
└────────────┘ 1 └────────────┘
│ 1 ▲
│ │
▼ 1 ▼ 1
┌────────────────────┐ ┌──────────────┐ 1..n┌────────────┐
│ WebSocketTransport │ │ DomainCache | │──────────▶│ Domain [3]│
└────────────────────┘ └──────────────┘ └────────────┘
[1] Target 由 TabTarget 和 MainProcessTarget 继承。[2] Session 由 TabSession 和 MainProcessSession 继承。[3] Domain 由 Log、Page、Browser、Target 等继承……即所有域实现。来自 cdp/domains/parent 和 cdp/domains/content 文件夹。