首页 > 娱乐前沿 > 热点
Webkit远程调试协议初探
佚名 2015-11-23 09:45:17

任何做过 Web 开发的同学,都避免不了在浏览器内进行调试。而大部分同学的首选工具,就是 Chrome DevTools。DevTools 本身我们无需多说,是一个大家不能再熟悉的工具了。但是埋藏在 DevTools 下面的开放协议以及它赋予的众多可能性,至今仍未见到充分的剖析和应用。

Webkit 的远程调试协议是 Webkit 在2012 年引入的。目前所有 Webkit 内核的浏览器都支持这一特性。但是我们还是以 DevTools 和 Chrome 为出发点,来做讨论。

为什么我们关注 DevTools:

原因 1:DevTools 是开源项目

DevTools 的源码就在Google 的 blink 项目中,高度的开放。目前这么多丰富的功能,正是 Google 和其社区的共同贡献。同时它的License也不拒绝任何的二次开发。

原因 2:它足够简单

DevTools 仅仅是简单的由 HTML、JavaScript、CSS、Images 组成的,本质上就是一个 WebApp,纯粹的前端应用。当你去了解、修改它时,你不需要理解 C++ 和任何编译的知识。

原因 3:它的应用架构足够开放,满足任何形式的功能扩展。

事实上 Devtools 是一个充分模块化的 JavaScript 网页应用。它的每个功能你都可以去扩展(仅需要了解 JavaScript)。

原因 4:大部分前端都已经习惯它并且喜欢它。

Webkit 的远程调试特性

谈到远程调试前,有必要先了解各组件之间的关系。

DevTools 的界面是数据驱动的。数据的来源就是 WebSocket API。Google 对 Webkit 的调试协议做了进一步的封装,提供了以 JSON 为序列化格式的WebSocket界面。

大家在本地电脑上就可以体验这个远程调试是怎样一回事。执行如下步骤:

  1. 彻底关闭当前 Chrome 进程
  2. 在 Chrome 的启动参数上加上–remote-debugging-port=9222,例如 Mac 平台:
    open -a Google/ Chrome –args –remote-debugging-port=9222
  3. 在开启的 Chrome 浏览器里打开任意网页,例如:http://www.taobao.com/
  4. 在其他浏览器或者 Chrome 的新 Tab 打开http://localhost:9222,你会得到这样的界面:

  1. 点击 “淘宝网” 的方框,就进入页面的调试界面了:

注意看地址栏,我们访问的是一个标准的 HTTP 协议下的网页,不是 Chrome 的私有协议。这里,你可以用 DevTools 再次检视这个页面,即按下CMD+OPTION+i。你会发现,这真的就是一个 HTML 应用。

再观察一下这个 URL:

http://localhost:9222/devtools/inspector.html?ws=localhost:9222/devtools/page/06D198AC-907F-430C-999C-16CCD7D2D489

通过 QueryString,我们告诉了 DevTools 的前端应用,它应该连接到哪个 WebSocket 服务。

你可以再你刚打开的检视 DevTools 的 DevTools(好绕口)里面,观察整个调试过程中的 WebSocket 通讯。例如:

以前用 WebSocket 做过 RPC 的同学应该看得出来,Google 实现的的确就是一个远程调用的接口。这个接口里面有两种通讯模式:

  1. request/response:就如同一个异步调用,通过请求的信息,获取相应的返回结果。这样的通讯必然有一个 message id,否则两方都无法正确的判断请求和返回的匹配状况。
  2. notification:和第一种不同,这种模式用于由一方单方面的通知另一方某个信息。和 “事件” 的概念类似。
实验一:通过调试协议获取页面加载的 Timeline 数据

通过调试协议来获取页面加载的所有网络请求并打印。为了简单,我们编写一个 Node.js 的应用来实现。大致步骤如下:

这里拿到的数据足以绘制一个非常准确的页面加载的瀑布图。从调试协议里拿到的数据具有以下特点:

完整代码如下(请先安装好相应的 npm 模块,并且打开 Chrome 本地的 9222 调试端口):

var WebSocketClient = require("websocket").client,
    util = require("util"),
    EE = require("events").EventEmitter,
    request = require("request"),
    chalk = require("chalk"),
    exec = require("child_process").exec;
// `Commander` class is message handler that talks to debug service exposed by Chrome
var Commander = function(conn) {
  EE.call(this);
  this.connection = conn;
  this.sendCommands = [];
  var self = this;
  Object.defineProperty(this, "nextMsgId", {
    get: function() {
      return self.sendCommands.length;
    },
    enumerable: true,
    configurable: false
  });
  conn.on("message", this.onMessage.bind(this));
};
util.inherits(Commander, EE);
// Send message using websocket connection
Commander.prototype.send = function(method, params, callback) {
  this.sendCommands.push([method, params, callback]);
  var msg = JSON.stringify({
    id: this.nextMsgId,
    method: method,
    params: params
  });
  console.log(msg);
  this.connection.send(msg);
};
//handler for receiving a message
Commander.prototype.onMessage = function(msg) {
  var command, data;
  if(msg.type === "utf8") {
    data = JSON.parse(msg.utf8Data);
    if(data.id) {//it's method request/response invocation
      command = this.sendCommands[data.id-1];
      if(command) {
        if(command.callback) {
          command.callback(data.params);
        }
      } else {
        console.warn("unmatched message id %s", data.id);
      }
    } else {//notifications
      this.emit(data.method, data.params);
    }
  } else {
    console.warn("message of unknown encoding");
  }
};
//find tab info
request("http://localhost:9222/json", function(e, res, data) {
  data = JSON.parse(data);
  var url = data[0].webSocketDebuggerUrl;
  if(!url) {
    throw new Error("no url");
  }
  var client = new WebSocketClient();
  //once it's connect, start our actions
  client.on("connect", function(conn) {
    console.log("client connceted");
    var commander = new Commander(conn);
    //Shoud enable this freatures
    commander.send("Network.enable",{});
    commander.send("Page.enable",{});
    //Listen to wanted notifications
    commander.on("Network.requestWillBeSent", function(data) {
      console.log("[%s] %s %s: %s", chalk.green(data.timestamp), chalk.blue("WillSend"), data.requestId, data.request.url);
    });
    commander.on("Network.loadingFailed", function(data) {
      console.log("[%s] %s %s", chalk.green(data.timestamp), chalk.red("LoadFail"), data.requestId);
    });
    commander.on("Network.loadingFinished", function(data) {
      console.log("[%s] %s %s", chalk.green(data.timestamp), chalk.magenta("LoadDone"), data.requestId);
    });
    commander.on("Network.responseReceived", function(data) {
      console.log("[%s] %s %s: %s Status %s %s", chalk.cyan(data.timestamp), chalk.red("RespRecv"), data.requestId, data.type, data.response.status, data.response.headers["Content-Length"]);
    });
    commander.on("Network.requestServedFromCache", function(data) {
      console.log("%s %s", chalk.gray(data.timestamp), chalk.red("RespCache"), data.requestId);
    });
    commander.on("Page.domContentEventFired", function() {
      console.log(chalk.bgGreen("OnDOMContentLoad/t/t/t/t/t/t/t/t"));
    });
    commander.on("Page.loadEventFired", function() {
      console.log(chalk.bgCyan("OnLoad/t/t/t/t/t/t/t/t"));
    });
    //Navigate to target page
    commander.send("Page.navigate", {url: "http://www.taobao.com"});
  });
  client.connect(url);
});

运行后的结果如下:

结语

本篇内容仅仅介绍调试协议这个概念,以及它的通讯原理。并且,我们通过一个实验,来展示这套协议的强大特性。后面,我们还会讨论其他浏览器的调试协议,以及移动设备的调试。

上一篇  下一篇

I 相关 / Other

5天内搞定产品设计是怎样一种体验

产品设计过程是有一个比较固定的周期的。但是,如果你能将整个流程的运作速度提升起来,用更频繁的反馈获得

一起来说Vim语

作为一款古老而具有持久生命力的编辑器,vim 自有它的强大之处。很多人觉得 Vim 的学习曲线太陡峭了,为了能

Java8简明教程

欢迎阅读我编写的 Java 8 介绍。本教程将带领你一步步认识这门语言的所有新特性。通过简单明了的代码示例,

Wireshark基本介绍和学习TCP三次握手

这篇文章介绍另一个好用的抓包工具 Wireshark,用来获取网络数据封包,包括 HTTP、TCP、UDP 等网络协议包。

一站式学习Wireshark(三):应用WiresharkIO图形工具分析数据流

基本IO Graphs:IO graphs是一个非常好用的工具。基本的Wireshark IO graph会显示抓包文件中的整体流量情况,

I 热点 / Hot