CEF3 - 多进程模式消息传递

上一篇讲了如何构建可调试的多进程CEF应用,这篇讲一下各个进程之间的通信,包括C++ 调JS, js 调C++,以及browser和render进程的消息流动。为什么先从多进程入手,个人试了一下,单进程模式,各个线程切换以及执行的时机不是很清楚,也没找到调试的正确姿势。多进程把线程分解出来变成独立应用,也算是以空间换时间的一种策略。这一篇中个人遇到好几个问题,虽然最终解决了,但是原因一直不是特别清楚。由于个人对于CEF理解有限,好多看法仅供参考。

  • browser和render进程

  1. browser 进程姑且叫UI进程,主要负责一些UI交互,比如页面加载,窗口弹出、菜单、前一页后一页、上传下载等;

  2. render进程姑且叫Web进程,主要做JS的解释和执行,以及DOM结构的解析等;

  • 明白各个进程的作用或者角色,然后分析一下,几种消息的流动(以下流程,基于自己当前理解,可能会有错误)

  1. 运行浏览器 - >browser进程根据url拉取服务端web数据 -> render进程解析DOM/JS ->GPU进程完成渲染 -> browser进程显示页面;

  2. 触发JS搞事情 -> render进程处理js->browser进程收保护费->然后再给render开一张发票;

  • 第一种方式的通信

现在最常用的,打开浏览器,输入地址或者关键字,从服务器拉取数据到本地浏览器做解释,然后渲染显示。

// Create the first browser window.
           //CefString url = "http://172.16.2.159";
           CefString url = strURL;//"file:///E://cef//index.html";
           CefBrowserHost::CreateBrowser(window_info, client, url, browser_settings, NULL);

创建浏览器进程,然后传入url地址,拉取数据解析和渲染过程没做进一步研究,这里就不瞎说了。

  • 第二种方式,是重点介绍的部分

 先把大的问题说下,最后说一下简单的实现。进程之间通信,无非就那几种方式,信号、共享内存,邮件槽,管道,windows平台的剪切板。CEF中browser进程和render进程是通过管道通信的。CEF做了一层封装所有行为都以接口的形式对外提供,通过继承接口,重写接口函数,处理不同的消息。

我们的需求是js触发调用,render拦截到调用,browser进程做一些额外操作,然后把操作结果返回js.

  1. js调用C++,走CEF流程处理,一般有两种方式。第一种render进程往js 全局对象window添加函数供调用,需要继承CefRenderProcessHandler接口,重写OnContextCreated方法
  2. void RenderApp::OnContextCreated(CefRefPtr browser, CefRefPtr frame, CefRefPtr context)
    {
    	OutputDebugString(_T("ClientAppRenderer::OnContextCreated, create window binding\r\n"));
    
    	// Retrieve the context's window object.
    	CefRefPtr object = context->GetGlobal();
    
    	if (nullptr == m_v8Handler){
    		m_v8Handler = new V8HandlerImpl;
    	}
    
    	// Create the "NativeLogin" function.
    	CefRefPtr func = CefV8Value::CreateFunction("NativeLogin", m_v8Handler);
    
    	// Add the "NativeLogin" function to the "window" object.
    	object->SetValue("NativeLogin", func, V8_PROPERTY_ATTRIBUTE_NONE);
    
    	// 
    	CefRefPtr fId = CefV8Value::CreateFunction("getIdInfo", m_v8Handler);
    	CefRefPtr fCm = CefV8Value::CreateFunction("getCameraInfo", m_v8Handler);
    	object->SetValue("getIdInfo", fId, V8_PROPERTY_ATTRIBUTE_NONE);
    	object->SetValue("getCameraInfo", fCm, V8_PROPERTY_ATTRIBUTE_NONE);
    
    	// test
    	/*CefRefPtr fcb = CefV8Value::CreateFunction("callbackId", m_v8Handler);
    	object->SetValue("callbackId", fcb, V8_PROPERTY_ATTRIBUTE_NONE);*/
    
    	// js 注册回调
    	// Create the "register" function.
    	CefRefPtr func1 = CefV8Value::CreateFunction("register", m_v8Handler);//第一个参数和SetValue参数保持一致,否则无法调用
    
    	// Add the "register" function to the "window" object.
    	object->SetValue("register", func1, V8_PROPERTY_ATTRIBUTE_NONE);
    }

    m_v8Handler 是一个类成员,对象初始化的时候就创建了,但是CEF多进程Debug模式下,由当前浏览器页面跳转其他web页面的时候(这时候会重新加载frame,构造新的context )会出现m_v8Handler为空的情况,不知道原因,但release下无法复现,所以这里会判空,然后重新创建v8 handler。

  3. 完成以上这步,js 的window 对象内就注册了几个方法,可以调用。js调用注册函数的时候,render需要拦截调用,需要继承CefV8Handler,然后重写Execute函数

  4. bool V8HandlerImpl::Execute(const CefString& name, /*JavaScript调用的C++方法名字*/ 
    							CefRefPtr object, /*JavaScript调用者对象*/ 
    							const CefV8ValueList& arguments, /*JavaScript传递的参数*/ 
    							CefRefPtr& retval, /*返回给JS的值设置给这个对象*/ 
    							CefString& exception)
    {
    	if (name == "NativeLogin")
    	{//Window Binding
    		if (arguments.size() == 2)
    		{
    			CefString strUser = arguments.at(0)->GetStringValue();
    			CefString strPassword = arguments.at(1)->GetStringValue();
    
    			//TODO: doSomething() in native way
    			CefRefPtr msg = CefProcessMessage::Create("login_msg");
    
    			// Retrieve the argument list object.
    			CefRefPtr args = msg->GetArgumentList();
    
    			// Populate the argument values.
    			args->SetSize(2);
    			args->SetString(0, strUser);
    			args->SetString(1, strPassword);
    
    			// Send the process message to the browser process.
    			CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg);
    			retval = CefV8Value::CreateInt(0);															//函数的返回值 我们可以拿这个返回值做判断或者其他操作
    			//var result = window.NativeLogin(document.getElementById("userName").value, document.getElementById("password").value);
    			//document.getElementById("text").innerHTML = result
    		}
    		else
    		{
    			retval = CefV8Value::CreateInt(2);
    		}
    		return true;
    	}
    	else if (name == "GetId")
    	{//JS Extensions
    		if (arguments.size() == 0)
    		{
    			// execute javascript 
    			// just for test
    			CefRefPtr frame = CefV8Context::GetCurrentContext()->GetBrowser()->GetMainFrame();
    			frame->ExecuteJavaScript("alert('Hello, I came from native world.')", frame->GetURL(), 0);
    
    			// return to JS
    			retval = CefV8Value::CreateString("72395678");
    			return true;
    		}
    	}
    	else if (name == "register")
    	{
    		if (arguments.size() == 1 && arguments[0]->IsFunction())
    		{
    			CefRefPtr callback_func_ = arguments[0];
    			CefRefPtr callback_context_ = CefV8Context::GetCurrentContext();
    
    			callback_func_->ExecuteFunction(NULL, arguments);//执行回调函数
    
    			return true;
    		}
    
    		// 测试回调
    		//if (arguments.size() == 2 && arguments[0]->IsFunction())
    		//{
    		//	CefRefPtr callback_func_ = arguments[0];
    		//	CefRefPtr callback_context_ = CefV8Context::GetCurrentContext();
    
    		//	//callback_func_->ExecuteFunction(NULL, arguments);//执行回调函数
    
    		//	return true;
    		//}
    	}
    	else if (name == "callbackId")
    	{
    		int iSz = arguments.size();
    		if (iSz == 2 && arguments[0]->IsFunction())
    		{
    			CefRefPtr callback_func_ = arguments[0];
    			int strImgType = arguments.at(0)->GetIntValue();
    
    			//TODO: doSomething() in native way
    			CefRefPtr msg = CefProcessMessage::Create("cbId");
    
    			// Retrieve the argument list object.
    			CefRefPtr args = msg->GetArgumentList();
    
    			callback_func_->AddRef();
    
    			// Populate the argument values.
    			args->SetSize(2);
    			args->SetInt(0, strImgType);
    			//args->SetValue(1, callback_func_);
    			CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg);
    		}
    		
    	}
    	else if (name == "getIdInfo")
    	{
    		int iSz = arguments.size();
    		int strImgType = arguments.at(0)->GetIntValue();
    
    		//TODO: doSomething() in native way
    		CefRefPtr msg = CefProcessMessage::Create("getIdInfo");
    
    		// Retrieve the argument list object.
    		CefRefPtr args = msg->GetArgumentList();
    
    		// Populate the argument values.
    		args->SetSize(1);
    		args->SetInt(0, strImgType);
    		CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg);
    	}
    	else if (name == "getCameraInfo")
    	{
    		int iSz = arguments.size();
    		int strImgType = arguments.at(0)->GetIntValue();
    
    		//TODO: doSomething() in native way
    		CefRefPtr msg = CefProcessMessage::Create(name);
    
    		// Retrieve the argument list object.
    		CefRefPtr args = msg->GetArgumentList();
    
    		// Populate the argument values.
    		args->SetSize(1);
    		args->SetInt(0, strImgType);
    		CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg);
    	}
    	// Function does not exist.
    	return false;
    }

    当拦截到getIdInfo后,不要通知browser做一些处理,然后把数据返回js。browser进程需要继承CefClient接口,然后重写 OnProcessMessageReceived方法,用于接收render进程发送的消息

  5. bool CustomClient::OnProcessMessageReceived(CefRefPtr browser, CefProcessId source_process, CefRefPtr message)
    {
    	int iState;
    
    	if (message->GetName() == _T("getIdInfo"))
    	{
    		CefRefPtr argList = message->GetArgumentList();
    		int strImgType = argList->GetInt(0);
    		
    		// getIdCardInfo
    		CefString strContent(_T("Welcode getIdInfo"));
    		getMediaInfo(browser, CefPoint(), strImgType, strContent, iState);
    
    		std::wstring strFunc(_T("whenreadover"));
    		CefRefPtr msg_back = CefProcessMessage::Create(strFunc);
    		msg_back->GetArgumentList()->SetString(0, strFunc);
    		msg_back->GetArgumentList()->SetString(1, strContent);
    		//browser->SendProcessMessage(PID_RENDERER, msg_back);
    
    		std::wstring s2(strContent);
    		CString strReplace= s2.c_str();
    		strReplace.Replace(L"\r\n", L"");
    
    		// 2018.8.4 这里是个坑,传入复合字符包换换行符,无法传递参数
    		CString str;
    		str.Format(L"whenreadover('%s')", strReplace);
    		browser->GetMainFrame()->ExecuteJavaScript((LPCTSTR)str, browser->GetMainFrame()->GetURL(), 0);
    
    		return true;
    	}
    	else if (message->GetName() == _T("getCameraInfo"))
    	{
    		CefRefPtr argList = message->GetArgumentList();
    		int strImgType = argList->GetInt(0);
    		
    		// getCameraInfo
    		CefString strContent(_T("Welcome getCameraInfo"));
    		getMediaInfo(browser, CefPoint(), 2, strContent, iState);
    
    		std::wstring s2(strContent);
    		CString strReplace = s2.c_str();
    		strReplace.Replace(L"\r\n", L"");
    		strReplace.Replace(L"\n\t", L"");
    		strReplace.Replace(L" ", L"");
    		std::wstring s3(strReplace);
    
    		std::wstring strFunc(_T("whenreadover"));
    		CefRefPtr msg_back = CefProcessMessage::Create(strFunc);
    		msg_back->GetArgumentList()->SetString(0, strFunc);
    		msg_back->GetArgumentList()->SetString(1, s3);
    		//browser->SendProcessMessage(PID_RENDERER, msg_back);
    
    		//// 2018.8.4 这里是个坑,传入复合字符包换换行符,无法传递参数
    		CString str;
    		str.Format(L"whenreadover('%s')", strReplace);
    		browser->GetMainFrame()->ExecuteJavaScript((LPCTSTR)str, browser->GetMainFrame()->GetURL(), 0);
    		
    	}
    	return false;   // has handle for the message  
    }

    browser进程拦截消息后,可以做交互,获取数据,然后可以再次发消息给render进程(需要继承CefRenderProcessHandler,然后重写OnProcessMessageReceived方法),或者可以直接调用约定的js函数,不需要返回render进程再调用,这是因为之前render进程已经把对应browser对象的指针传入了(这里姑且理解为一个web页面对应一个browser对象,一个browser对象对应一个frame,也对应一个v8 context, 这个context决定了js对象、函数的作用域和可见性,这个地方可能有误,注意),用程序验证,确实是可以的

  6.         // getIdCardInfo
            CefString strContent(_T("Welcode getIdInfo"));
            getMediaInfo(browser, CefPoint(), strImgType, strContent, iState);

            std::wstring strFunc(_T("whenreadover"));
            CefRefPtr msg_back = CefProcessMessage::Create(strFunc);
            msg_back->GetArgumentList()->SetString(0, strFunc);
            msg_back->GetArgumentList()->SetString(1, strContent);
            //browser->SendProcessMessage(PID_RENDERER, msg_back);

            std::wstring s2(strContent);
            CString strReplace= s2.c_str();
            strReplace.Replace(L"\r\n", L"");

            // 2018.8.4 这里是个坑,传入复合字符包换换行符,无法传递参数
            CString str;
            str.Format(L"whenreadover('%s')", strReplace);
            browser->GetMainFrame()->ExecuteJavaScript((LPCTSTR)str, browser->GetMainFrame()->GetURL(), 0);

 whenreadover是js中定义的方法,不需要提前注册,所以这里的上下文环境很重要。调用的方式是构造字符串,格式为func('params'),URL可有可无。

参数传递的时候不能包含"\r\n" 空格等特殊格式化字符,也不能包含用json第三方库格式化出来的json字符串(c++ jsonCpp这个库格式化出来的字符无法成功传递)。原因推测v8引擎需要分析js的语义,部分格式化字符可能有冲突了,所以产生了异常,导致js调用失败。格式化字符串作为参数传递的时候经常会有这样的问题,尤其是在一些协议里,涉及到二次解析的可能就有问题,这里要注意。

篇幅太长了,下一篇单独讲如何通过js扩展实现js与C++通信,包括但参数(json),回调等特性。

你可能感兴趣的:(CEF)