浅谈ipc通讯
因为大部分功能都会涉及主进程和渲染进程通讯,所以首先了解一下主进程是怎么和渲染进程通讯的。
渲染进程通过 ipcRenderer
模块给主进程发送消息,官方提供了三个方法:
ipcRenderer.send(eventName, ...args)
ipcRenderer.invoke(eventName, ...args)
ipcRenderer.sendSync(eventName, ...args)
参数包括eventName
事件名称,args
传递的参数。
下面介绍方这三个方式如何给主进程发送消息和主进程如何消息的具体示例。
ipcRenderer.send
渲染进程
通过 ipcRenderer.send
发送消息给主进程,使用ipcRenderer.on
监听主进程返回的数据。
const { ipcRenderer } = require("electron");
// 发送给主进程的消息
ipcRenderer.send('render-send-event', '发给主进程的事件参数');
// 主进程通过 reply 返回的数据
ipcRenderer.on('main-reply-answer', (event, args) => { console.log('主进程返回值', args);
主进程
通过ipcMain.on
监接收渲染进程发送的消息,使用event.reply
发送另一个事件给渲染进程。
const { ipcMain } = require('electron');
ipcMain.on('render-send-event', (event, data) => {
console.log('接受到的事件参数:', data);
// 接收到render-send-event消息并返回数据
event.reply('main-reply-answer', '主进程给渲染进程的返回值')
})
send
方法的返回值是 undefined
,所以如果需要主进程返回数据给渲染进程需要通过 event.reply
发送另一个事件,渲染进程监听这个事件得到回复结果。
ipcRenderer.invoke
渲染进程
通过 ipcRenderer.invoke
发送消息给主进程并传递事件参数,其invoke
返回值就是主进程返回的Promise
格式的数据。
const { ipcRenderer } = require('electron');
const invokeInvokeToMain = async () => {
const replyMsg = await ipcRenderer.invoke('render-invoke-event', '发给主进程的事件参数');
console.log('主进程接收到消息后的返回值', replyMessage);
}
主进程
使用ipcMain.handle
接收渲进程发送的消息,并把异步获取到的数据,通过return
返回给渲染进程。
const { ipcMain } = require('electron');
ipcMain.handle("render-invoke-event", async (event, data) => {
console.log(`接收到渲染进程发送的事件参数: ${data}`);
// 异步获取数据
const result = await asyncAnswer();
// 返回数据
return result;
});
// 异步数据请求
const asyncAnswer = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("等待 6 秒后返回给渲染进程的数据");
}, 6000);
});
};
ipcRenderer.sendSync
渲染进程
使用ipcRenderer.sendSync
发送消息给主进程,sendSync
的返回值就是主进程发回来的数据。
const { ipcRenderer } = require('electron');
const replyMessage = ipcRenderer.sendSync('render-sendSync-event', '发给主进程事件参数');
console.log('主进程回复的消息:', replyMessage);
主进程
const { ipcMain } = require('electron');
ipcMain.on('render-sendSync-event', async (event, data) => {
console.log(`渲染进程发送过来的事件参数: ${data}`)
// 通过rvent.returnValue返回数据给渲染进程
event.returnValue = await asyncAnswer();
})
// 异步数据请求
const asyncAnswer = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("等待 6 秒后返回给渲染进程的数据");
}, 6000);
});
};
注意:
- 如果
event.returnValue
不为undefined
的话,渲染进程会等待sendSync
的返回值才执行后面的代码。 - 请保证
event.returnValue
是有值的,否则会造成非预期的影响。
因为不论渲染进程在接收
sendSync
结果的时候,是不是用await
等待,都会等待结果返回后才向下执行。所以如果你已经确定你的请求是一个异步的话,建议还是使用invoke
去发送消息
总结
- 通过上面三种方式,主进程都可以有相应的方法给予渲染进程应答,只是应答的方法不同。
- 同步请求建议使用
sendSync
,异步请求建议使用invoke
,普通的消息通知建议使用send
。
http协议无法同时播放6路以上视频
问题:
浏览器为了避免网络拥塞和提高网络传输效率,同一个域名下最多只能同时建立6个TCP连接,这个限制被称为”同源策略”。
解决方法:
- 使用HTTPS、HTTP/2.0或WebSocket协议可以在一定程度上缓解同一个域名下最多只能并发6个TCP连接的限制
- 如果不改变协议,可以使用node创建服务对访问的http协议进行转发从而调过同源策略的限制
这里介绍一下使用第二种方法的解决思路
由于满足同源策略的条件是协议、域名、端口都要一致,所以即使协议域名相同但端口不同就不会触发同源策略。所以解决思路就是,如果要在本地使用http协议播放六路以上视频时,我们下本地使用node创建代理转发的http服务每当超过6路视频时,就增加一个端口号不同的http服务。
主要逻辑代码
以下代码为实现功能的主要逻辑代码,不重要的逻辑已进行省略
主进程中利用node创建http服务
主要步骤
- 声明创建服务的函数,函数中主要做的事情有
- 通过函数传参传入的端口号来创建对应的http代理服务,在创建的http服务中再去请求最终要请求的视频地址
- 在http代理服务中需要解析通过地址传参传递进来的参数来判断具体要请求的视频地址
- 返回创建的http代理服务的实例,用以关闭服务等操作
- 通过ipc通信,通过监听渲染进程发送的创建服务消息来创建http代理服务
- 将需要播放的视频列表传递给主进程,通过判断每6条视频创建一个不同端口的http代理服务
- 通过ipc通信,通过监听渲染进程发送的关闭服务消息来关闭http代理服务
const http = require("http");
const url = require("url");
const { ipcMain } = require("electron");
const createServer = (port) => {
const hostname = "127.0.0.1";
const onRequest = (req, res) => {
// 设置响应头中的跨域访问控制
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
// 解析请求参数
const requestUrl = url.parse(req.url, true);
// 视频地址
const flvUrl = requestUrl.query.flvUrl;
if (!flvUrl) {
res.statusCode = 400;
res.end("Missing FLV URL parameter");
return;
}
// flv地址
let targetUrl =
"http://12.3.456.789:1234/live" + flvUrl;
// 解析目标 URL
const options = url.parse(targetUrl);
// 将请求中的所有 headers 复制到 options 对象中
options.headers = req.headers;
// 代理请求
const targetReq = http.request(options, (targetRes) => {
res.writeHead(
targetRes.statusCode,
targetRes.statusMessage,
targetRes.headers
);
targetRes.on("data", (chunk) => {
res.write(chunk);
});
targetRes.on("end", () => {
res.end();
});
});
req.on("data", (chunk) => {
targetReq.write(chunk);
});
req.on("end", () => {
targetReq.end();
});
targetReq.on("error", (err) => {
console.error(`代理请求时出错 ${targetUrl}: ${err.message}`);
res.writeHead(500);
res.end(`代理请求时出错 ${targetUrl}: ${err.message}`);
});
};
const server = http.createServer(onRequest);
server.listen(port, hostname, () => {
console.log(`服务启动在 http://${hostname}:${port}/`);
});
return server;
};
const serverList = [];
// 创建http服务
ipcMain.on("create-node-services", (event, videoData) => {
console.log("create-node-services", videoData);
let videoList = JSON.parse(videoData);
for (let i = 6; i <= videoList.length; i++) {
if (i % 6 === 0) {
serverList[(i - 6) / 6] = createServer(9000 + (i - 6) / 6, videoList[i]);
}
}
});
// 关闭http服务
ipcMain.on("server-close", (event, videoData) => {
serverList.forEach((t) => t.close(() => console.log("server close")));
});
渲染进程播放视频并通知主进程创建http服务
// 因为返回的媒体流为flv格式,这里就以基于第三方库mpegts.js为例
import mpegts from "mpegts.js";
const { ipcRenderer } = require("electron");
// 视频播放编号列表
const videoList = ref([ "/124.flv", "/125.flv","/126.flv","/127.flv","/128.flv"]);
// 通知主进程创建http服务
ipcRenderer.send("create-node-services", JSON.stringify(videoList.value));
// 视频播放实例列表
const playerList = ref([]);
onMounted(() => {
if (mpegts.getFeatureList().mseLivePlayback) {
var videoElement = document.querySelectorAll("#videoElement");
// 创建播放实例并保存致实例列表
videoElement.forEach((item, index) => {
playerList.value[index] = mpegts.createPlayer({
type: "mse",
isLive: true,
url:
"http://127.0.0.1:" +
(9000 + Math.floor(index / 6)) +
"?flvUrl=" +
videoList.value[index] +
"&key=" +
key.value,
});
});
// 播放实例绑定video标签
playerList.value.forEach((item, index) => {
item.attachMediaElement(videoElement[index]);
item.load();
item.play();
});
}
});
onUnmounted(() => {
// 销毁播放器实例
playerList.value.forEach((item, index) => {
item.destroy();
});
// 关闭node服务
ipcRenderer.send("server-close");
});
调用系统关联程序打开文件
使用electron
中的shell
模块,使用默认应用程序打开文件,shell
模块提供与桌面集成相关的功能。
const { shell } = require("electron");
// 主进程
ipcMain.on("open-file", async () => {
//调取对话框并配置筛选选项
const result = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [
{ name: "word文件", extensions: ["docx"] },
{ name: "所有文件", extensions: ["*"] },
],
});
if (!result.canceled && result.filePaths.length > 0) {
// 使用openPath api打开文件
shell.openPath(result.filePaths[0]);
}
});
文件的读取和写入
文件读取
主进程代码示例
- 使用
ipc
通信handle
监听渲染进程的get-file
事件 - 调取系统对话框选择文件,并配置文件筛选条件
- 使用
fs
模块中的readFileSync
api读取文件内容,通过path模块获取文件名称、类型等数据 - 将读取的数据返回给渲染进进程,这里值得注意的是要将读取后的文件数据转换一下数据格式,直接将读取的
Buffer
数据返回给渲染进程会导致转换数据时出现转换异常的问题
Buffer数据问题:
因为通过readFileSync
读取出来的文件数据为Buffer
格式,而Buffer
在ipc传递数据的过程中会转换成Uint8Array
,导致在渲染进程中使用toString
转换数据的时候出现异常,所以要在传输时对Buffer
数据进行转换后再传输。
const { ipcMain, dialog } = require("electron");
const fs = require("fs");
ipcMain.handle("get-file", async () => {
const result = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [
{ name: "text文件", extensions: ["txt"] },
{ name: "word文件", extensions: ["docx"] },
{ name: "所有文件", extensions: ["*"] },
],
});
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0];
// 读取文件内容
const file = fs.readFileSync(result.filePaths[0]);
return {
name: path.basename(filePath),// 文件名称
type: path.extname(filePath), // 文件类型
data: file.toString("base64"),// 文件数据
};
}
});
渲染进程代码实例
const { ipcRenderer } = require("electron");
// 发送获取文件消息给主进程并接收返回的文件数据
const getFile = async () => {
const res = await ipcRenderer.invoke("get-file");
console.log('选择的文件数据:'+ res)
};
文件写入
主进程代码示例:
通过ipcMain.on
接收渲染进程发送的消息和数据,选择路径和文件名称后使用fs
模块的writeFileSync
写入文件
const { ipcMain, dialog } = require("electron");
ipcMain.on("write-file-txt", async (event, data) => {
// 打开保存对话框选择路径和文件名
const result = await dialog.showSaveDialog({
/ 设置文件类型
filters: [
{ name: ".txt", extensions: ["txt"] },
{ name: "所有文件", extensions: ["*"]},
],
});
if (!result.canceled && result.filePath) {
// 将传入的数据写入
fs.writeFileSync(result.filePath, data.toString());
}
});
渲染进程
const { ipcRenderer } = require("electron");
// 发送消息给主进程并传递数据
ipcRenderer.send("write-file-txt", "通知主进程要写入的数据");