深入了解Node.js和Electron是如何做进程通信的


本文摘自PHP中文网,作者青灯夜游,侵删。

本篇文章给大家探究一下Node.js 和 Electron 的进程通信原理,介绍一下electron 如何做进程通信、nodejs 的 child_process 和 cluster 如何做进程通信,了解进程通信的本质。

为什么前端要了解进程通信:

前端领域已经不是单纯写在浏览器里跑的页面就可以了,还要会 electron、nodejs 等,而这俩技术都需要掌握进程通信。

nodejs 是 js 的一个运行时,和浏览器不同,它扩展了很多封装操作系统能力的 api,其中就包括进程、线程相关 api,而学习进程 api 就要学习进程之间的通信机制。

electron 是基于 chromium 和 nodejs 的桌面端开发方案,它的架构是一个主进程,多个渲染进程,这两种进程之间也需要通信,要学习 electron 的进程通信机制。【推荐学习:《nodejs 教程》】

这篇文章我们就来深入了解一下进程通信。

本文会讲解以下知识点:

  • 进程是什么
  • 本地进程通信的四种方式
  • ipc、lpc、rpc 都是什么
  • electron 如何做进程通信
  • nodejs 的 child_process 和 cluster 如何做进程通信
  • 进程通信的本质

进程

我们写完的代码要在操作系统之上跑,操作系统为了更好的利用硬件资源,支持了多个程序的并发和硬件资源的分配,分配的单位就是进程,这个进程就是程序的执行过程。比如记录程序执行到哪一步了,申请了哪些硬件资源、占用了什么端口等。

进程包括要执行的代码、代码操作的数据,以及进程控制块 PCB(Processing Control Block),因为程序就是代码在数据集上的执行过程,而执行过程的状态和申请的资源需要记录在一个数据结构(PCB)里。所以进程由代码、数据、PCB 组成。

1.png

pcb 中记录着 pid、执行到的代码地址、进程的状态(阻塞、运行、就绪等)以及用于通信的信号量、管道、消息队列等数据结构。

2.png

进程从创建到代码不断的执行,到申请硬件资源(内存、硬盘文件、网络等),中间还可能会阻塞,最终执行完会销毁进程。这是一个进程的生命周期。

进程对申请来的资源是独占式的,每个进程都只能访问自己的资源,那进程之间怎么通信呢?

进程通信

不同进程之间因为可用的内存不同,所以要通过一个中间介质通信。

信号量

如果是简单的标记,通过一个数字来表示,放在 PCB 的一个属性里,这叫做信号量,比如锁的实现就可以通过信号量。

这种信号量的思想我们写前端代码也经常用,比如实现节流的时候,也要加一个标记变量。

管道

但是信号量不能传递具体的数据啊,传递具体数据还得用别的方式。比如我们可以通过读写文件的方式来通信,这就是管道,如果是在内存中的文件,叫做匿名管道,没有文件名,如果是真实的硬盘的文件,是有文件名的,叫做命名管道。

文件需要先打开,然后再读和写,之后再关闭,这也是管道的特点。管道是基于文件的思想封装的,之所以叫管道,是因为只能一个进程读、一个进程写,是单向的(半双工)。而且还需要目标进程同步的消费数据,不然就会阻塞住。

这种管道的方式实现起来很简单,就是一个文件读写,但是只能用在两个进程之间通信,只能同步的通信。其实管道的同步通信也挺常见的,就是 stream 的 pipe 方法。

消息队列

管道实现简单,但是同步的通信比较受限制,那如果想做成异步通信呢?加个队列做缓冲(buffer)不就行了,这就是消息队列

消息队列也是两个进程之间的通信,但是不是基于文件那一套思路,虽然也是单向的,但是有了一定的异步性,可以放很多消息,之后一次性消费。

共享内存

管道、消息队列都是两个进程之间的,如果多个进程之间呢?

我们可以通过申请一段多进程都可以操作的内存,叫做共享内存,用这种方式来通信。各进程都可以向该内存读写数据,效率比较高。

共享内存虽然效率高、也能用于多个进程的通信,但也不全是好处,因为多个进程都可以读写,那么就很容易乱,要自己控制顺序,比如通过进程的信号量(标记变量)来控制。

共享内存适用于多个进程之间的通信,不需要通过中间介质,所以效率更高,但是使用起来也更复杂。

上面说的这些几乎就是本地进程通信的全部方式了,为什么要加个本地呢?

ipc、rpc、lpc

进程通信就是 ipc(Inter-Process Communication),两个进程可能是一台计算机的,也可能网络上的不同计算机的进程,所以进程通信方式分为两种:

本地过程调用 LPC(local procedure call)、远程过程调用 RPC(remote procedure call)。

本地过程调用就是我们上面说的信号量、管道、消息队列、共享内存的通信方式,但是如果是网络上的,那就要通过网络协议来通信了,这个其实我们用的比较多,比如 http、websocket。

所以,当有人提到 ipc 时就是在说进程通信,可以分为本地的和远程的两种来讨论。

远程的都是基于网络协议封装的,而本地的都是基于信号量、管道、消息队列、共享内存封装出来的,比如我们接下来要探讨的 electron 和 nodejs。

electron 进程通信

electron 会先启动主进程,然后通过 BrowserWindow 创建渲染进程,加载 html 页面实现渲染。这两个进程之间的通信是通过 electron 提供的 ipc 的 api。

ipcMain、ipcRenderer

主进程里面通过 ipcMain 的 on 方法监听事件

1

2

3

4

5

import { ipcMain } from 'electron';

 

ipcMain.on('异步事件', (event, arg) => {

  event.sender.send('异步事件返回', 'yyy');

})

渲染进程里面通过 ipcRenderer 的 on 方法监听事件,通过 send 发送消息

1

2

3

4

5

6

7

import { ipcRenderer } from 'electron';

 

ipcRender.on('异步事件返回', function (event, arg) {

  const message = `异步消息: ${arg}`

})

 

ipcRenderer.send('异步事件', 'xxx')

api 使用比较简单,这是经过 c++ 层的封装,然后暴露给 js 的事件形式的 api。

我们可以想一下它是基于哪种机制实现的呢?

很明显有一定的异步性,而且是父子进程之间的通信,所以是消息队列的方式实现的。

remote

除了事件形式的 api 外,electron 还提供了远程方法调用 rmi (remote method invoke)形式的 api。

其实就是对消息的进一步封装,也就是根据传递的消息,调用不同的方法,形式上就像调用本进程的方法一样,但其实是发消息到另一个进程来做的,和 ipcMain、ipcRenderer 的形式本质上一样。

比如在渲染进程里面,通过 remote 来直接调用主进程才有的 BrowserWindow 的 api。

1

2

3

4

const { BrowserWindow } = require('electron').remote;

 

let win = new BrowserWindow({ width: 800, height: 600 });

win.loadURL('https://github.com');

小结一下,electron 的父子进程通信方式是基于消息队列封装的,封装形式有两种,一种是事件的方式,通过 ipcMain、ipcRenderer 的 api 使用,另一种则是进一步封装成了不同方法的调用(rmi),底层也是基于消息,执行远程方法但是看上去像执行本地方法一样。

nodejs

nodejs 提供了创建进程的 api,有两个模块: child_process 和 cluster。很明显,一个是用于父子进程的创建和通信,一个是用于多个进程。

child_process

child_process 提供了 spawn、exec、execFile、fork 的 api,分别用于不同的进程的创建:

spawn、exec

如果想通过 shell 执行命令,那就用 spawn 或者 exec。因为一般执行命令是需要返回值的,这俩 api 在返回值的方式上有所不同。

spawn 返回的是 stream,通过 data 事件来取,exec 进一步分装成了 buffer,使用起来简单一些,但是可能会超过 maxBuffer。

1

2

3

4

5

6

7

8

9

10

11

const { spawn } = require('child_process');

 

var app = spawn('node','main.js' {env:{}});

 

app.stderr.on('data',function(data) {

  console.log('Error:',data);

});

 

app.stdout.on('data',function(data) {

  console.log(data);

});

其实 exec 是基于 spwan 封装出来的,简单场景可以用,有的时候要设置下 maxBuffer。

1

2

3

4

5

6

7

8

const { exec } = require('child_process');

 

exec('find . -type f', { maxBuffer: 1024*1024 }(err, stdout, stderr) => {

    if (err) {

        console.error(`exec error: ${err}`); return;

    }  

    console.log(stdout);

});

execFile

除了执行命令外,如果要执行可执行文件就用 execFile 的 api:

1

2

3

4

5

6

const { execFile } = require('child_process');

 

const child = execFile('node', ['--version'], (error, stdout, stderr) => {

    if (error) { throw error; }

    console.log(stdout);

});

fork

阅读剩余部分

相关阅读 >>

深入了解node.js中的非阻塞 i/o

了解node.js中的模块系统

了解http事务、node模块化规范

electron protocol 模块

nodejs http请求相关的总结介绍

4个使用将html转换为pdf的方法介绍

node.js是什么?有什么优势?有什么用途?

electron browserwindow 模块

node.js中require()是如何工作的?工作原理介绍

electron 构建步骤 (linux)

更多相关阅读请进入《node.js》频道 >>




打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

评论

管理员已关闭评论功能...