提出问题
之前我的一篇博客说的是怎么利用 selenium 来做自动化监控。当出现异常时,我们需要记录页面源码、网络请求数据、截图等信息来方便我们诊断问题,基本上就够用了。但是,这两天遇到一个棘手的异常,时不时页面会弹出:“系统繁忙,请稍候再试!”,这时候我们去看网络请求数据,结果状态码全部都是 200,没有其它信息,这压根没法定位不了问题。
这就说明:网络出现异常的时候,仅靠状态码是不够的。我们最好能拿到 http 所有数据,包括:请求头、响应头、请求体、响应体。其中请求头、响应头,可以通过 PERFORMANCE_LOG 拿到,问题都不大。但是请求体与响应体,我们可以拿到么?
分析过程
这个问题困扰了我整整一天的时间,终于解决了。为什么这么困难?
我们先来看 selenium,它为什么不直接支持这个功能呢?因为开发人员觉得这不是他们目标:
we will not be adding this feature to the WebDriver API as it
falls outside of our current scope (emulating user actions).
we will not be adding this feature to the WebDriver API as it falls outside of our current scope (emulating user actions).
然后我继续翻网络,发现谷歌的devtools-protocol明确是支持的:
那我有没有什么办法能调用这两个方法呢?这就很麻烦了,我根据这篇文章的思路去直连谷歌的 Remote Port。
看这篇文章真的很美,但实际上到我这个项目并不可行,为什么?
原因在于这篇文章所用的PyChromeDevTools是基于 websocket 的,而且是在请求一个链接后,立即去读取 Chrome 吐出来的响应数据。
而在监控这种场景下,是在请求已经完成之后才会收集 PerformanceLog,然后根据其中的请求 ID 去问 Chrome 要数据。一个是推,一个是拉,这是两种模式。所以非常不幸,解决不了我的问题。
但是给我了我一个思路,我去找找有没有类似 Java 的组件。这时候,我从 GitHub 上找到了cdp4j,这是一个跟 Chrome 打交道的包,它有一个很迷人的 API:
// 获取请求返回内容
session.getCommand().getNetwork().getResponseBody("requestIdxxxxx");
// 获取请求返回内容 session.getCommand().getNetwork().getResponseBody("requestIdxxxxx");
这个方法我试验了很久,结果仍然不行,调用时一直返回的是:
No resource with given identifier found
我确认了很久,确认 requestId 是没有问题的,为什么拿不到数据?我试了很久,最后放弃了,因为我发现是这样的:
Java 的 Selenium 通过 chromedriver 开启了一个与 Chrome 的 session,cdp4j 是没有办法直接绑到这个 session 上面的(理论上是可能的,但是 cdp4j 的扩展性太差,我实在懒得去改)。这就意味着 chromdriver 的请求数据无法通过 cdp4j 来获取到。
既然 Java 的 Selenium 其实没并有直连 Chrome,而是通过 chromedriver 去跟 Chrome 打交道的。我们能不能从 chromedriver 上看看有没有直接获取 responseBody 的接口呢?
所以,我开始找 chromedriver 的文档,文档真的非常少。不知道从哪里我了解到 chromedriver 是根据 w3c 的协议开发的,我看看 w3c 的webdriver协议里能不能找到答案。
结果仍然很让人沮丧,我翻了很久,发现 w3c 的 webdriver 协议没有定义 Network 相关的操作。
然后我就开始仔细分析 selenium 的源码,发现了 AbstractHttpCommandCodec 里有与 chromedriver 相关的操作定义。
/**
* A command codec that adheres to the W3C's WebDriver wire protocol.
*
* @see W3C WebDriver spec
*/
public abstract class AbstractHttpCommandCodec implements CommandCodec {
//...
public AbstractHttpCommandCodec() {
defineCommand(STATUS, get("/status"));
defineCommand(GET_ALL_SESSIONS, get("/sessions"));
defineCommand(NEW_SESSION, post("/session"));
defineCommand(GET_CAPABILITIES, get("/session/:sessionId"));
defineCommand(QUIT, delete("/session/:sessionId"));
// ...
// Mobile Spec
defineCommand(GET_NETWORK_CONNECTION, get("/session/:sessionId/network_connection"));
defineCommand(SET_NETWORK_