Electron 如何使用 IPC 命名管道与其他应用通信

2,035 阅读5分钟

一、前言

本文根据自身实际工作情况,向大家简单的介绍下在 Electron 中如何通过 IPC 命名管道的方式(此技术在类 Unix 系统下被称为域套接字)和其他应用进行相互通信,该通信的核心是基于 Node.js 的 Net 模块来实现的,通过阅读本文你可以学习到如何通过 Node.js 的 Net 模块创建及使用 IPC 命名管道进行进程间的消息通信。

二、进程间通信(IPC)

进程间通信IPCInter-Process Communication),指至少两个进程线程间传送数据或信号的一些技术或方法,其主要的 IPC 通信方式如图一所示:

image.png

由于每个进程创建之后都有自己的独立地址空间,实现 IPC 的目的就是为了进程之间资源共享访问,在上面主要的 IPC 通信方式中,系统内进程之间的通信最常见的方式还是 IPC 命名管道(此技术在 Unix 下被称为套接字),但是 Electron 并没有提供直接创建命名管道的 API(Electron 自身进程之间通信是通过 IPC 来实现的,但是只能处理 Electron 自身的进程通信),我们需要借助 Electron 内置的 Node.js 服务的 Net 模块来实现。

三、使用 Net 模块实现 IPC 服务

Net 模块提供了异步的网络 API,用于创建基于流的 IPC 服务器,其对于 IPC 的支持,在 Windows 系统上采用命名管道的方式,在其他的操作系统上则使用 Unix 域套接字,IPC 命名管道区分客户端和服务端,其中服务端主要用于监听和接收数据,客户端主要用于连接和发送数据,两者都是可以做到持久连续双向通信的。

为了更好的方便大家理解,假设现在有一个大型的混合应用程序,应用 A 是原生应用,应用 B 是 Electron 应用,在应用 B 运行的过程中,需要不断的和应用 A 发送消息交换数据。

在交互的过程中,A 需要提供给 B 一个用于连接和发送数据的命名管道服务,也就是说 A 是命名管道的服务端,B 是命名管道的客户端。

其两者的交互步骤如下图二所示:

image.png

注意:在上图中,应用 A 的 IPC 服务,有最大实例数连接的限制,应用 B 的每一次连接对于应用 A 而言都是一个新的实例(客户端),你需要注意控制应用 B 连接应用 A 的实例数量,必要时可以在恰当时机释放应用 B 的连接。

应用 A 创建管道服务代码如下:

const net = require('net');

const fs = require('fs');

const pipeName = 'xxx_server_pipe';

const pipePath = process.platform === 'win32' ? path.join('\?\pipe', pipeName) : `/tmp/${pipeName}`;

const removeServerPipePath = serverPath => {

  try {

    process.platform !== 'win32' && fs.unlinkSync(serverPath);

  } catch (e) {}

};

const server = net.createServer((connc) /* connc: 监听器 */ => {

  connc.on('data', d => {

    console.log(Date.now(), `服务端接收到数据${d.toString()}`);

    connc.write('hello 应用B');

  });

  connc.on('end', d => console.log(Date.now(), '客户端已关闭连接'));

});

server.on('close', () => {

  removeServerPipePath(pipePath);

  console.log('服务关闭');

});

// 注意:在非 Windows 的操作系统中,需要主动删除套接字文件,具体原因在下文有详细说明

removeServerPipePath(pipePath);

server.listen(pipePath, () => console.log('服务已经启动,正在监听客户端连接'));

在上面代码中,我们通过 net.createServer 来创建了一个服务对象 server,但是此时创建出的服务可以是 TCP 或者 IPC 服务,只有当你使用 server.listen 启动并监听一个命名管道地址时候,这个服务才是 IPC 服务。

当有客户端连接此命名管道的时候,将会触发 net.createServer 的回调函数,这个回调函数有一个 connectionListener 对象,我们可以通过这个对象接收或者发送数据。

当客户端发送一段数据后,会触发 connectionListener 的 'data' 事件。

当客户端连接关闭时,会触发 connectionListener 的 'end' 事件。

服务端可以通过 connectionListener 的 write 方法向客户端发送数据。

当程序关闭时,我们需要手动关闭掉命名管道服务,当服务被关闭时,会触发 server 对象的 close 事件,对于除 Windows 外的操作系统,我们需要主动的删除掉域套接字文件,否则下次启动时就会报错,如下图三所示:

image.png

其具体的原因可以参考这个链接:nodejs.cn/api/net.htm…

到此,我们已经模拟了应用 A 开启并创建命名管道服务,那么应用 B 如何连接到命名管道服务并发送数据给它呢,我们需要在应用 B 中创建一个命名管道客户端,代码如下:

const net = require('net');

const pipePath = process.platform === 'win32' ? path.join('\\?\pipe', 'xxx_server_pipe') : '/tmp/xxx_server_pipe';

const client = net.connect(pipePath);

client.on('connect', () => {

  console.log(Date.now(), '客户端与服务端连接建立成功');

  client.write('hello 应用A');

});

client.on('data', d => {

  console.log(Date.now(), '客户端接收到的数据', d.toString());

  // 接收到应用 A 返回的数据,关闭当前连接

  client.end();

});

在上面的代码中,我们借助 net.connect API 连接已经存在的 IPC 服务,它会返给我们一个实例对象,我们可以通过 write 方法向管道中写入数据;通过监听 'data' 事件接收应用 A 传递的数据。在终端中运行上面两个文件代码,得到的输出结果如下图四所示:

image.png

上面就是 Electron 中使用 Net 模块实现 IPC 命名管道和其他应用通信的核心代码。

除了这种通过命名管道的通信方式外,你还可以通过其他常见的 IPC 通信方式和其他进程及应用通信,都能实现一样的效果。

四、查看 IPC 服务

Windows

直接在浏览器地址栏打开该路径 file://.//pipe// ,你就可以看到当前系统下所有的命名管道,如图五所示:

image.png

其他系统

通过 Net 创建的 IPC 服务,一般都会指定创建到 tmp 目录下,我们可以直接在浏览器地址栏打开该路径 file:///tmp/,就可以看到当前系统下创建的 IPC 服务,如图六所示:

image.png

五、参考

  1. zhuanlan.zhihu.com/p/89174306
  2. zh.wikipedia.org/wiki/行程間通訊
  3. zh.wikipedia.org/wiki/命名管道
  4. Electron 实战:入门、进阶与性能优化 / 刘晓伦 [M] .北京:机械工业出版社,2020.5(2020.11 重印):146~148