CefSharp 手动执行CDP(Chrome DevTools Protocol)和监听执行CDP 方法消息(messageId)返回结果
CefSharp 提供了多种执行CDP(Chrome DevTools Protocol)方式,有高度封装的DevToolsClient.Page、DevToolsClient.DOM等等,也有完全手动执行的IBrowserHost下的SendDevToolsMessage,这里我们只讨论手动执行方式。
手动执行CDP方式目前我知道的有两种:
只传入CDP方法名称、参数,返回结果(Cefsharp维护 发送消息ID、接收消息ID; 有些方法也提供了消息ID入参),使用方便,但是由于消息id是Cefsharp维护,频繁发送时有时会抛出消息ID不匹配异常;
手动控制发送json,监听返回结果(完全控制,就剩下websocket链接等基本信息cefsharp维护),但是操作比较麻烦
手动执行CDP方法
DevToolsClient
Cefsharp提供的CDP封装类,封装了CDP各种方法模块直接调用方法,但是使用姿势不对可能会执行后程序卡死,具体各种卡死情况请跳转stackoverflow。
可以通过chromiumWebBrowser.GetBrowser().GetDevToolsClient() 获得DevToolsClient实例。
最好不要频繁调用GetDevToolsClient() 获取DevToolsClient,因为据说每次获取会重置消息ID,频繁获取可能会导致 发送/接收消息ID冲突,所以最好声明全局变量在ChromiumWebBrowser实例初始化完成时获取一次:
DevToolsClient devTool = null; private void Form1_Load(object sender, EventArgs e){ //.... ChromiumWebBrowser chromiumWebBrowser1 = new ChromiumWebBrowser(); chromiumWebBrowser1.IsBrowserInitializedChanged+= new EventHandler(delegate { devTool = chromiumWebBrowser1.GetBrowser().GetDevToolsClient(); }); }
DevToolsClient.ExecuteDevToolsMethodAsync
我感觉相对比较简单的手动调用CDP方式,CefSharp维护发送消息ID,Cefsharp已经简单封装了消息结果类型
方法原型:
public class DevToolsClient : IDevToolsMessageObserver, IDisposable, IDevToolsClient { //.... public Task<DevToolsMethodResponse> ExecuteDevToolsMethodAsync(string method, IDictionary<string, object> parameters = null); }
method: CDP 方法名称
parameters: 方法参数
返回结果DevToolsMethodResponse:
public class DevToolsMethodResponse { public DevToolsMethodResponse(); public int MessageId { get; set; } public string ResponseAsJsonString { get; set; } public bool Success { get; set; } }
MessageId: 消息ID
ResponseAsJsonString: 返回消息内容(消息的result内容)
Success: 是否执行成功
比如执行刷新页面:
private void button8_Click(object sender, EventArgs e) { devTool.ExecuteDevToolsMethodAsync("Page.reload").ContinueWith(delegate(Task<DevToolsMethodResponse> result) { Console.WriteLine(result.Result.ResponseAsJsonString); }); }
获取页面结构:
private void button8_Click(object sender, EventArgs e) { devTool.ExecuteDevToolsMethodAsync("DOM.enable").ContinueWith(delegate(Task<DevToolsMethodResponse> result) { Dictionary<string, object> param = new Dictionary<string, object>() { { "depth", 10 }, { "pierce", true } }; devTool.ExecuteDevToolsMethodAsync("DOM.getDocument", param).ContinueWith(delegate(Task<DevToolsMethodResponse> resultA) { Console.WriteLine(resultA.Result.ResponseAsJsonString); }); }); }
DOM.enable: 开启DOM代理
DOM.getDocument: 获取页面结构(包含嵌套的iframe内容),有两个可选参数(depth: 获取结构深度,pierce: 是否递归向下查询iframes)
但是别使用Wait()奥- -,像这样:
private void button8_Click(object sender, EventArgs e) { devTool.ExecuteDevToolsMethodAsync("DOM.enable").Wait(); }
会发现程序卡死了...当初这个问题困扰好久,上边的overflow上的问题就是我提出的,截止到现在,还没有大佬关注...o(╥﹏╥)o
DevToolsExtensions
另一个执行CDP方法的静态类,主要用来扩展实现IBrowserHost、IWebBrowser、IBrowser接口的实例可以直接执行CDP方法,因为ChromiumWebBrowser实现了IWebBrowser和IBrowser,所以可以在ChromiumWebBrowser实例中直接调用ExecuteDevToolsMethodAsync方法。
上边chromiumWebBrowser1.GetBrowser().GetDevToolsClient()中GetDevToolsClient方法就是使用的此类中的扩展函数。
DevToolsExtensions.ExecuteDevToolsMethod
执行CDP方法,执行成功返回消息id,失败则返回0。
和上边不同的是,上边执行CDP方法后,会异步返回方法执行结果,此方法没有异步执行,而是返回了传入的消息id,并且此方法必须在cefsharp线程中调用
public static class DevToolsExtensions { public static int ExecuteDevToolsMethod(this IBrowserHost browserHost, int messageId, string method, JsonString parameters); }
browserHost: 此处传入 chromiumWebBrowser1.GetBrowserHost();
messageId: 消息ID
method: 方法名称
parameters: 方法参数,传入方法参数json字符串
比如获取页面结构:
int i = 1;
Cef.UIThreadTaskFactory.StartNew(delegate { chromiumWebBrowser1.GetBrowserHost().ExecuteDevToolsMethod(i, "DOM.enable"); i++; Console.WriteLine(chromiumWebBrowser1.GetBrowserHost().ExecuteDevToolsMethod(i, "DOM.getDocument", new JsonString("{\"pierce\": true, \"depth\": 1}"))); });
上边方法只是发送指令,如果想要拿到指令对应的结果,就需要实现IDevToolsMessageObserver接口,相当于添加了一个监听(官方取名叫观察者),监听websocket发送过来的消息:
class DevToolsMessageObserverHandler : IDevToolsMessageObserver { public void Dispose() { } public void OnDevToolsAgentAttached(IBrowser browser) { } public void OnDevToolsAgentDetached(IBrowser browser) { } public void OnDevToolsEvent(IBrowser browser, string method, Stream parameters) { } public bool OnDevToolsMessage(IBrowser browser, Stream message) { return false; } public void OnDevToolsMethodResult(IBrowser browser, int messageId, bool success, Stream result) { byte[] bytes = new byte[result.Length]; result.Read(bytes, 0, bytes.Length); StringBuilder sb = new StringBuilder(); foreach (byte item in bytes) { sb.Append((char)item); } Console.WriteLine(sb.ToString()); } }
这里主要关注OnDevToolsMessage和OnDevToolsMethodResult方法,服务器发送一条消息时,先到OnDevToolsMessage方法,在到OnDevToolsMethodResult方法。
如果OnDevToolsMessage方法返回true,表示消息已处理,不会在执行后续的OnDevToolsMethodResult.
OnDevToolsMethodResult方法的messageId就是发送指令时传入的消息ID.
把监听类注册到BrowserHost中:
//全局变量
IRegistration reg = null;
chromiumWebBrowser1.IsBrowserInitializedChanged += new EventHandler(delegate { reg = chromiumWebBrowser1.GetBrowserHost().AddDevToolsMessageObserver(new DevToolsMessageObserverHandler()); });
这样每一条浏览器端的发送过来的消息,都会监听到,但是这时就需要我们自己来实现根据消息ID匹配CDP方法的返回结果了。
这里需要注意AddDevToolsMessageObserver方法返回的IRegistration对象,这个对象控制监听器是否继续监听,如果调用reg.Dispose(),监听器将再也不起作用。
不要写成这样:
chromiumWebBrowser1.IsBrowserInitializedChanged += new EventHandler(delegate { chromiumWebBrowser1.GetBrowserHost().AddDevToolsMessageObserver(new DevToolsMessageObserverHandler()); });
如果写成这样,将在第一次监听到消息后,会随着GC空闲时,把AddDevToolsMessageObserver返回的IRegistration对象回收,监听器也就无效了。
IBrowserHost.SendDevToolsMessage
完全手动控制发送json,虽然自由度高但使用起来跟上边比起来确实有些繁琐
方法原型很简单,只传入一个json,CefSharp会直接给浏览器端发送这个json字符串,返回true表示执行成功,false表示执行失败
bool SendDevToolsMessage(string messageAsJson);
发前需要我们记一下消息id,发送后需要使用上边监听浏览器端消息内容方式匹配每一条消息ID。
和DevToolsExtensions.ExecuteDevToolsMethod函数一样,需要在cefsharp线程中使用:
Cef.UIThreadTaskFactory.StartNew(delegate { IBrowserHost browserHose = chromiumWebBrowser1.GetBrowserHost(); browserHose.SendDevToolsMessage("{\"id\": 1, \"method\": \"DOM.enable\"}"); browserHose.SendDevToolsMessage("{\"id\": 2, \"method\": \"DOM.getDocument\", \"params\": {\"pierce\": true, \"depth\": 40}}"); });
以上根据个人理解总结,如有错误的地方,欢迎前辈指出,非常感谢!