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}}");
            });


以上根据个人理解总结,如有错误的地方,欢迎前辈指出,非常感谢!
posted @ 2021-02-02 23:46  耿明岩  阅读(1964)  评论(0编辑  收藏  举报
希望能帮助到你,顺利解决问题! ...G(^_−)☆