Webkit远程调试协议实战

482 查看

Webkit远程调试协议实战

上一篇文章 介绍了 DevTools 和 Webkit Debug Protocol 这两个 Web 开发利器的内部原理。本篇主要讲解 iOS 的 Safari 远程调试。

iOS 的 Safari 远程调试,是 iOS7 引入的新功能。它允许开发者通过桌面端 Safari 的调试工具远程检视移动端浏览器打开的页面。这套调试工具,彻底解决了无线开发纯靠 “alert” 的调试困境。Safari 的远程调试中使用到的调试协议,与 Google 开放的 Chrome Debug Protocol 有牵丝万缕的关系,体现了这两家互联网巨头相爱相杀的本质:

  1. 这两套协议本质都是 Webkit Debug Protocol 的衍生产物。大部分的实际功能是一模一样的,例如 DOM 检视、网络请求监控、Console 等。
  2. 自从 Google 与 Webkit 项目撇清关系,分道扬镳后生下亲儿子 blink 后,自己的调试功能越来越强大,并逐渐产生了一些和 Webkit 调试协议不一样的功能,故自成一套 Chrome Debug Protocol。
  3. Apple Safari 这个养在深闺里小女子,虽然主导 Webkit,但是在实际产品使用中,却并没有直接使用 Webkit Debug Protocol,不仅仅使用了 binary plist 来作为序列化方法,还抛弃了 WebSocket 的通讯手段。没有文档、没有代码,我们姑且叫这个不舍得露面的东西叫做 Safari 调试协议吧。

webinspectord 和 lockdown

iOS7 以后,每个 iOS 都有一个 webinspectord 守护进程,负责远程调试的通讯。这个进程暴露了一个服务接口,供外部应用(例如桌面端的 Safari 调试工具)使用。

iOS 上所有的服务(文件浏览、消息推送、app 安装等)都是通过一个 lockdown 服务管理连接上的。

自然的,调试工具也需要透过 USB 接口,通过 lockdown 界面,连接到 webinspector 服务。

由于 iOS 的各种系统组件都极为神秘,没有更多可以解释的了。伟大的开源社区,通过各种手段实现了这些服务的接口,大家可以前去膜拜

Safari 远程调试服务概述

抛开 USB 通讯、lockdown 接口不谈,Safari 远程调试服务所使用的协议本身其实就是 Webkit 调试协议的二次包装。也就是共享了 Webkit 调试协议的大部分功能。

先分析这个协议里面的主体:

  • iOS 设备,iPad、iPhone 等物理设备
    • UDID: 以 40 位 UDID 字符串唯一识别一个设备
  • Application:iOS 设备上运行的开启了 WebView 应用程序,设备上可以同时运行多个 Application
    • Identifier:应用标示符
    • BundleIdentifier:应用的 main bundle 标示符
  • Page:每个 Application 可以打开多个页面
    • Identifier:页面标示符
    • Title
    • URL

还有一些概念字段:

  • ConnectionId, 标示当前连接到 webinspector 服务的连接
  • SenderId, 标示请求方(例如 DevTools )实体

大体可以看出,这个调试服务的接口是有状态的。设备和 DevTools 建立连接后,拥有可以复用的链接作为后续通讯的通道。

假设我们需要发送一条调试指令到 iOS 上的某个 Webkit 内核,它的整个编码流程应该是是这样:

  1. Webkit 能识别的消息对象进行 JSON 序列化为字符串
  2. 构建 Safari 调试协议中使用的 bplist 消息体,来包装之前得到的字符串。这里用到的 selector 就是 _rpc_forwardSocketData
  3. 将消息题通过 Socket 传输到 iOS 上的调试服务
  4. iOS 上调试服务识别消息,并解析 bplist,得倒 Webkit 能识别的消息对象
  5. 将上一步得倒的消息对象传输给 Webkit

所有的消息,都是以 Apple 自己定义的 RPC 消息提格式进行的。但它实际传输的有效数据,还是 Webkit
能够识别的指令。另外,Safari 没有如同 Chrome 那样,使用了 WebSocket 作为暴露出去的应用层协议。它选择了最基本的 Socket 通讯方式和 bplist 作为传输格式。

调试消息的大冒险

下面以一个具体的消息作为例子,来说说这整个过程。

JSON 消息和 Safari 的 RPC 协议

假设我们要开启网络监控这个面板,需要发送一个 JSON 指令。指令序列化之后我们的到:

就像之前说的,Safari 不直接使用 JSON 字符串作为传输的序列化方案。Safari 远程调试协议有自己的 RPC 规范,所有的消息都都有 __selector__arguments 两个字段。前者说明调用的方法,后者说明调用时的参数。

常见的一些方法(其实是 ObjC selector 的字符串表达)如下:

  • _rpc_reportIdentifier::向 webinspector 服务注册当前链接 (传输 connectionId )
  • _rpc_getConnectedApplications::要求获取连接到 webinspector 的 iOS 应用列表
  • _rpc_forwardGetListing::获取某个应用的页面列表(传输 connectionId, appId )
  • _rpc_forwardSocketSetup::注册当前会话 (传输 connectionId、senderId )
  • _rpc_forwardSocketData::利用某个会话传输数据(传输 connectionId、senderId、data )。Webkit 调试协议所传输的 JSON 就是通过这个方法传递的,JSON 字符串的二进制表达被通过这个接口传递到 iOS 设备上的调试服务。

另一方面,iOS 端也会传过来很多消息,同样遵循基本消息提的格式,常见的 __selector 有:

  • _rpc_reportConnectedApplicationList::回报连接到 webinspector 的应用列表
  • _rpc_applicationSentListing::回报某个应用的页面列表
  • _rpc_applicationConnected::某个 iOS 应用连接到了调试服务
  • _rpc_applicationDisconnected::某个 iOS 应用从调试服务断开

Safari 不选择 WebSocket 作为传输协议应该是从安全性、复杂性的角度去考虑。但选择 bplist 作为传输格式,应该没有太多理由,大概因为 Apple 体系内部都是用 bplist 的。

JSON 到 plist 的转换

plist 和 bplist 都是 Apple 的通讯格式。其中 plist 非常常见。加入你做过 iOS 或者 Mac 开发,你一定写过不少 plist。plist 就是一种拥有自有 DTD 的 XML 文档类型。说白了,它就是 XML 文档。

例如之前的 JSON 指令,转换为 Safari 调试协议能够理解的 plist 文档:

可以看到 plist 拥有多种标签来定义数据类型,例如 dict、string、data 等;同时节点的顺序,都是遵循 key、value 的顺序编写。也就是说 JSON 是可以和 plist 互相转换的。

这个转换过程中,唯一麻烦的是 data 类型。这个标签是用来存储二进制数据的,JSON 中没有定义。但是在 Node.js 中,可以无缝转换为一个 Buffer。

细心的你一定注意到上面 plist 中的两个问题:

  1. 没发现任何 JSON 字符串的内容
  2. WIRSocketDataKey 里面的竟然是 base64 编码的字符串

事实上,Safari 的调试协议中,要求 JSON 字符串是被当作 payload data 传输的。而 plist 标准中,data 数据类型,就是进行 base64 编码的。

plist 到 bplist 的转换

bplist 是 binary plist 的简称。它以二进制编码为基础,可以用来存储 plist 格式中同样的内容。这在 Socket 通讯中十分有用。

要知道 Safari 调试协议只接受 bplist 格式。具体客户端的开发中,没有规定一定要像本文中将一个指令先转换为 plist,再转换为 bplist。安排这样的转换,只是方便大家理解。你完全可以直接将一个 JSON 构造为 bplist。

前文的那段 plist,转换为 binary plist 就是: