QT集成CEF08-多进程通信

通过上一个章节理解了CEF3 的多进程,即一个主进程,一般主进程是Browser进程,其他的分别是渲染进程(Renderer),GPU加速进程(GPU),插件进程(NPAPI或者PPAPI)。

  • Browser进程:负责窗口管理,界面绘制和网络交互。

  • Renderer进程:负责JavaScript的执行与DOM节点维护

  • NPAPI插件进程:按需创建,每种类型的插件只会有一个进程,每个插件进程可以被多个Render进程共享;

  • GPU进程:按需创建,最多只有一个,当且仅当GPU硬件加速打开的时候才会被创建,主要用于对3D加速调用的实现。

多进程的好处很多,在浏览器中最主要的好处是当一个页面或者插件崩溃或假死,不会给其他页面带来影响。各个进程之间还可以通过发送消息来进行通信。这里借用网络上的一张图说明:

在这里插入图片描述

方框代表进程,连接线代表IPC进程间通信(Inter-Process Communication)

CEF3的进程之间可以通过IPC进行通信。Browser和Renderer进程可以通过发送异步消息进行双向通信。这里我们主要讨论的是 Browser进程和Renderer进程之间的通信。

网络上找到了Chromium 的browser进程和Renderer进程的模型图:

  • Browser进程
    在这里插入图片描述

  • Renderer 进程
    在这里插入图片描述

    在CEF3中,每个进程都会运行多个线程

    Browser进程中包含如下主要的线程

    • TID_UI 线程是Browser进程的主线程。如果应用程序在调用CefInitialize()时,传递CefSettings.multi_threaded_message_loop=false,这个线程也是应用程序的主线程。 因为要与QT继承,所以我们传递的是 CefSettings.multi_threaded_message_loop=true
    • TID_IO 线程主要负责处理IPC消息以及网络通信。
    • TID_FILE 线程负责与文件系统交互。

    CEF中browser进程和Renderer 进程是通过管道通信的。CEF做了一层封装所有行为都以接口的形式对外提供,通过继承接口,重写接口函数,处理不同的消息。

    CefBrowserCefFrame对象在Borwser和Renderer 进程里都会存在,都有一个唯一ID值绑定,便于在两个进程间定位匹配。也就是说在我们的示例工程:QyCefVS和QyRender 这两个项目中都能使用CefBrowserCefFrame ,但它们是在不同的进程中运行的,通过相互发送消息进行通信。

1. 消息发送与接收

现在我们需要搞清楚以下几个问题:

  1. 消息的发送者
  2. 发送什么样的消息
  3. 怎么发送消息
  4. 消息的接受者
  5. 怎么接收消息

这里我设计了这样的一个场景: Renderer进程的 JavaScript 引擎V8 Context上下文被创建以后,向Browser进程发送一个消息,发送这个消息的目的是让 Browser进程创建一个窗口并显示出来,所以消息包含三个参数: 窗口标题,窗口宽度,窗口高度。Browser进程收到消息后解析出消息内容,然后发射一个QT 信号给 主窗体,主窗体中的槽函数连接这个信号,在槽函数中创建窗体并显示出来。
在这里插入图片描述

这个场景并没有什么实际意义,但是通过它我们可以了解进程间是如何通信的。

  1. 消息的发送者:Render进程,在 QyRender这个项目中。 QyAppRenderer类实现了CefApp和CefRenderProcessHandler接口,CefRenderProcessHandler 中定义了 OnContextCreated和 OnProcessMessageReceived, 我们需要重写这两个方法。消息就在 OnContextCreated中发出,OnProcessMessageReceived 是用来接收消息的,这里做简单日志输出。这个例子中暂时还用不到。

  2. 发送什么样的消息:CefProcessMessage 表示一个消息,通过它的静态方法CefProcessMessage::Create 可以创建一个消息

  3. 怎么发送消息: 参考文档 中说:

    在进程生命周期内,任何时候你都可以通过CefProcessMessage类传递进程间消息。这些信息和特定的CefBrowser实例绑定在一起,用户可以通过CefBrowser::SendProcessMessage()方法发送。进程间消息可以包含任意的状态信息,用户可以通过CefProcessMessage::GetArgumentList()获取。

    但是在我使用的版本:cef_binary_91.1.22+gc67b5dd+chromium-91.0.4472.124_windows32 中,CefBrowser 类并没有 SendProcessMessage 方法,这个方法被定义到了CefFrame 类上。所以我这里是使用 CefFrame 来发送消息的。

  4. 消息的接收者: Browser进程,在QyCefVS项目中,SimpleHandler 实现了CefClient, 在 CefClient 中定义了 OnProcessMessageReceived 方法,可以用它来接收消息

  5. 怎么接收消息: 在SimpleHandler 类中重写 OnProcessMessageReceived 方法。

2. 简单日志和中文处理

因为这是个多进程应用,调试比较麻烦,所以这里通过在程序中输出日志到文件的方式来Debug。这里使用了QT的 QDebug来输出到文件中。

QT 中 QDebug的输出默认是输出到控制台上的,可以使用 qInstallMessageHandler 函数来定制 QDebug的输出。

QT中输出中文也比较麻烦,经常会出现乱码,所以也要配置一下,让我们能够在日志文件中正常的显示中文信息。

2.1 日志输出

在QyCefVS项目和 QyRender项目的源码目录中创建一个头文件:log.h

// log.h
#pragma once
#include <QtCore/qlogging.h>
#include <QFile>
#include <QDateTime>
#include <QMutex>
#include <QTextStream>
#include <QtCore/QCoreApplication>
void outputMessage(QtMsgType type, const QMessageLogContext& context, const QString& msg)
{
	static QMutex mutex;
	mutex.lock();

	QString text;
	switch (type) {
	case QtDebugMsg:
		text = QString("debug:");
		break;
	case QtWarningMsg:
		text = QString("warning:");
		break;
	}

	QString context_info = QString("File:(%1) Line:(%2)").arg(QString(context.file)).arg(context.line);
	QString current_date_time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss ddd");
	QString current_date = QString("(%1)").arg(current_date_time);
	QString message = QString("%1 %2 %3 %4").arg(text).arg(context_info).arg(msg).arg(current_date);
	// 在QyRender项目中,输出为 QyRenderLog.txt
	//QFile file(QCoreApplication::applicationDirPath().append("\\QyRenderLog.txt"));
    // 在QQyCefVS项目中,输出为 QyCefVSLog.txt
    QFile file(QCoreApplication::applicationDirPath().append("\\QyCefVSLog.txt"));
	file.open(QIODevice::WriteOnly | QIODevice::Append);
	QTextStream text_stream(&file);
	text_stream << message << "\r\n";
	file.flush();
	file.close();

	mutex.unlock();
}

注意在不同的项目中,这里输出的日志文件名字不一样。

在两个项目的main函数中引入头文件,调用 qInstallMessageHandler:

  • QyRender项目 main函数:
//... 省略
#include "log.h"
//... 省略
int main(int argc, char* argv[])
{
    QCoreApplication a(argc, argv);
	qInstallMessageHandler(outputMessage);
	//... 省略
}
  • QyCefVS项目 main函数:
//... 省略
#include "log.h"
//... 省略
int main(int argc, char* argv[])
{
    //... 省略
   QApplication a(argc, argv);
    qInstallMessageHandler(outputMessage); // 日志
	//... 省略
}

2.2 中文乱码处理

为 vs 2019 安装一个 FileEncoding 插件,使用它能方便更改源代码所使用的字符集。

在这里插入图片描述

然后修改源代码文件的编码格式为 “UTF-8(BOM)”:
在这里插入图片描述

在需要输出中文的代码文件的第一行添加一个预编译指令:

#pragma execution_character_set("UTF-8")

这样就能使用 qDebug直接输出中文了。

3. 实现步骤

这里只给出关键代码,其它代码省略掉了。

3.1 Renderer进程发送消息

QyRender项目的 QyAppRenderer类实现,重点是实现 OnContextCreated 方法中的发送消息

//QyAppRenderer.h 
#include "include/cef_app.h"
class QyAppRenderer :public CefApp, public CefRenderProcessHandler {
public:
	QyAppRenderer();

	//重写CefApp 中的GetRenderProcessHandler方法
	CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() OVERRIDE {
		return this;
	}
	//实现 CefRenderProcessHandler 接口中的方法
	void OnBrowserCreated(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefDictionaryValue> extra_info) OVERRIDE;

	// 在这里发送消息
	void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefFrame> frame,
		CefRefPtr<CefV8Context> context) OVERRIDE;
	// 接收消息,暂时没哟用到
	bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefFrame> frame,
		CefProcessId source_process,
		CefRefPtr<CefProcessMessage> message);

private:
	// Include the default reference counting implementation.
	IMPLEMENT_REFCOUNTING(QyAppRenderer);
};


//QyAppRenderer.cpp
#pragma execution_character_set("UTF-8")
#include "QyAppRenderer.h"
#include <QDebug>
void QyAppRenderer::OnBrowserCreated(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefDictionaryValue> extra_info) {
	qDebug() << "=====OnBrowserCreated=======";
}
// 在这里发送消息
void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefRefPtr<CefV8Context> context) {

	// 发送消息给 Browser进程
	CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("showSubWindow");
	CefRefPtr<CefListValue> args = msg->GetArgumentList();
	CefString title("我是一个子窗口");
	int width = 400, height = 300; //窗体的宽和高
	args->SetSize(2); //两个参数
	args->SetString(0, title);
	args->SetInt(1, width);
	args->SetInt(2, height);
	qDebug() << "=====发送消息给Browser进程=======";
	// 发送消息给Browser进程
	frame->SendProcessMessage(PID_BROWSER, msg);
}
// 收到其它进程发送过来的消息,这里仅仅打印了收到的消息
bool QyAppRenderer::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefProcessId source_process,
	CefRefPtr<CefProcessMessage> message) {
	qDebug() << "收到进程:" << source_process << "的消息, 消息名称:"
		<< QString::fromStdString(message.get()->GetName());
	return true;
}

3.2 Browser进程接收消息

QyCefVS项目中 ,SimpleHandler 中接收Renderer进程发送过来的消息然后发射QT 信号,所以它需要继承 QObject,并添加 Q_OBJECT 宏,定义一个信号,名字为:onReceiveRendererProccessMessasge 并实现 CefClient中的OnProcessMessageReceived 方法:

//simple_handler.h
#include "include/cef_client.h"
#include <list>
#include "include/wrapper/cef_helpers.h"
#include "QObject"
class SimpleHandler :public QObject, public CefClient
	, public CefLifeSpanHandler
	, public CefKeyboardHandler {
	Q_OBJECT
public:
	// ... 省略
	virtual bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefFrame> frame,
		CefProcessId source_process,
		CefRefPtr<CefProcessMessage> message);

signals:
	void onReceiveRendererProccessMessasge(QString title, int width, int height);

	// ... 省略
};


//simple_handler.cc
// ... 省略
bool SimpleHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefProcessId source_process,
	CefRefPtr<CefProcessMessage> message) {

	qDebug() << "收到:" << source_process << "进程消息: " << QString::fromStdString(message.get()->GetName());

	CefString title = message.get()->GetArgumentList().get()->GetString(0); //第一个参数 标题
	int width = message.get()->GetArgumentList().get()->GetInt(1); // 第二个参数宽度
	int height = message.get()->GetArgumentList().get()->GetInt(2);// 第三个参数高度
	//发送信号到 主窗口,让主窗口创建一个子窗口
	emit onReceiveRendererProccessMessasge(QString::fromStdString(title), width, height);
	return true;
}

3.3 主进程(Browser进程) 连接信号

MainWindow 中先定义一个槽函数void onReceiveRendererProccessMessasge(QString title, int width, int height);

在SimpleHandler 对象创建完毕之后,关联信号和槽。而SimpleHandler 对象是在 原来的 :void MainWindow::createBrowserWindow() 中创建的,所以在这个函数中做信号连接。

// main_window.h
#include <QtWidgets/QMainWindow>
#include "ui_mainwindow.h"
#include "cef/simple_app.h"
class MainWindow : public QMainWindow
{
// 省略...
private slots:
// 省略...
	void onReceiveRendererProccessMessasge(QString title, int width, int height);
// 省略...
};

// main_window.cpp
void MainWindow::createBrowserWindow() {
	// 省略...
    // 连接信号和槽
	connect(handler.get(), &SimpleHandler::onReceiveRendererProccessMessasge, this, &MainWindow::onReceiveRendererProccessMessasge);
}

// 槽函数,用来创建一个子窗口
void MainWindow::onReceiveRendererProccessMessasge(QString title, int width, int height)
{
	QDialog* subWin = new QDialog(this);
	subWin->setWindowTitle(title);
	subWin->setFixedWidth(width);
	subWin->setFixedHeight(height);
	subWin->show();
}

3.4 编译运行

先编译QyRender项目,在编译QyCefVS项目,或者直接编译整个解决方案后运行:

在这里插入图片描述

项目运行后会发现主窗口出现以后,稍微等待一小会,子窗口出现了,这说明已经成功了。再看看可执行程序目录下多了两个日志文件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JNSkkYNa-1638952929830)(assets/image-20211208162050895.png)]
代码请访问 GitHub qt_cef_08分支

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

paopao_wu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值