node 进程
node建立在v8之上,意味着其模型和浏览器相似,javascript是运行在单个进程的单个线程之上,他没有多线程 锁,线程同步的问题,且程序状态单一,没有上下文切换时,对cpu过多的负荷,但是他也有很多致命的问题,比如,单线程的机制无法充分利用多核cpu服务器,且在程序运行的时候,如果程序中途崩溃,整个进程就会崩溃。幸好有解决办法。
1. 服务器模型的变迁
石器时代,同步
早期的服务器,请求执行模式是同步,意味着第二个请求,需要等第一个请求执行完毕才能完成。其QPS(每秒查询率)为1/N
青铜时代,复制进程
为了接入更多的请求,对于每个连接,复制一个进程来处理请求,也就是说对于100个请求,复制100个进程来处理,显然,这是不合理,复制进程的代价很昂贵,无论是进程内相同资源的浪费还是进程复制带来的开销都限制这种模式的发展,其QPS为M(可利用最大进程数)/N
白银时代,多线程
复制进程 带来的开销实在巨大,所以将原来进程处理请求改用线程处理请求会很好多。线程使用开销会小于进程,而且线程之间可以共享数据,意味着可以共同使用的数据就不用重复复制,造成浪费。
线程固然比进程好但是也要不足之处,线程拥有自己的堆栈,这意味着每一个线程都在内存之中分配了一些空间。而且单个cpu一时间只能处理一件事情,当线程过多, cpu不断切换线程上下文 ,会消耗大量时间。当线程开销是进程的1/L倍时。其QPS为M*L/N。
黄金时代,事件驱动
多进程的模式,在大量线程处理上,切换上下文的代价太大,限制了并发的应用。而基于事件驱动服务模型的node以及nginx。利用单线程解决了切换上下文带来的开销,但是基于事件驱动受限于cpu的计算能力,这也是为什么说node不适合处理计算密集的应用场景,但是node处理计算能力也是很不错的。
基于事件驱动的模型也有一些缺点,当线程中有错误发生,整个线程会奔溃,健壮性不适合那么好,无法利用多核CPU,造成资源浪费,还有如果cpu处理计算过多,会导致无法继续调用异步(ps:感觉egg多进程框架解决了其中的一些问题,现在最主要是cpu处理大量计算的问题,虽然不知真假,和朋友在技术群里,谈论利用云计算代替芯片的未来,开玩笑不买电脑,手机不带芯片,这样以后node利用云计算能力,释放cpu的部分能力,node是不是就没缺点,异步回调不算缺点!!!!)
2. 多进程架构
node基于事件驱动存在一些缺点,比如单线程无法充分利用多核cpu的能力,那么就如复制进程 那样,复制与核数一致的进程数,解决单线程无法利用计算机性能的问题。
node提供基础模块“child_process”,该模块有方法fork()可以复制进程,当然fork进程也需要代价,每个复制的进程之中有着穿新的V8实例,启动开销,内存开销。当然这个代价可以说是没有,我们不fork那么多,仅仅为了解决利用多核cpu的资源问题。
创建子进程
创建子进程,可以通过child_process模块的四种方法。
方法 | 是否有回调获知子进程情况 | 进程类型 | 执行类型 | 设置超时 |
---|---|---|---|---|
spawn | 否 | 任意 | 命令 | 否 |
exec | 可以 | 任意 | 命令 | 可以 |
execFile | 可以 | 任意 | 可执行文件 | 可以 |
fork | 否 | node | js脚本 | 可以 |
其中execFile 执行的是可执行文件,使用”chmod +x ./filename”,将文件转为可执行文件,而在文件内部首行需要告诉环境该脚本执行器的类型,添加 “#!/usr/bin/env node”
进程之间通信
Electron 只需要使用浏览器语言就能编写pc端,这是拥抱浏览器开发思想的一种很好体现,其风味主进程和渲染进程,而主进程和渲染进程通信使用事件(EventEmitter)发射来处理进程之间的通信.
在node中,主进程和子进程接收消息靠触发message事件,而发送消息靠send()方法。对此我对下面的代码有些疑问
// 主进程master.js
const childProcess =require("child_process");
const cpf = childProcess.fork(__dirname+'./child.js');
cpf.on("message",(message)=>{
console.log("子进程发送的消息",message);
})
cpf.send(
{send:"主进程发送得消息"}
);
// 子进程 child.js
const process = require("process");
process.on("message",(message)=>{
console.log("父进程发送得消息");
})
process.send(
{send:"子进程发送得消息"}
);
// 启动 node master.js
其结果图如下
我的疑问是:在主进程中,无论是需要接受子进程的消息还是发送消息给子进程,都需要加持在fork的子进程上,我在想能不能直接获得主进程process的对象然后加持接受消息事件,和发送消息。主进程的修改代码如下
// 主进程master.js,修改的意思主要是在当前进程执行send方法,子进程能不能获得消息
const childProcess =require("child_process");
const pr = require("process");
const cpf = childProcess.fork(__dirname+'./child.js');
pr.on("message",(message)=>{
console.log("子进程发送的消息",message);
})
pr.send(
{send:"主进程发送得消息"}
);
结果是send is not function ,message消息也灭有得到,从官方的介绍
如果使用 IPC 通道衍生 Node.js 进程(参阅子进程和集群文档),则只要子进程收到父进程使用
childprocess.send()
发送的消息,就会触发'message'
事件。如果Node.js进程是通过进程间通信产生的,那么,process.send()方法可以用来给父进程发送消息。 接收到的消息被视为父进程的
ChildProcess
对象上的一个'message'
事件。 如果Node.js进程不是通过进程间通信产生的,process.send()
会是undefined
。
child_process.fork()
方法是child_process.spawn()
的一个特例,专门用于衍生新的 Node.js 进程。 与child_process.spawn()
一样返回ChildProcess
对象。 返回的ChildProcess
会内置一个额外的通信通道,允许消息在父进程和子进程之间来回传递
也就是说”使用IPC通道衍生的node.js进程”,”通过进程间通信产生的” 才是一个能send的进程。
IPC(进程间通信)
tcp/ip实现不同主机不同进程进行,而实现同一主机进程间通信可以只用命名管道,匿名管道,socket,信号量,共享内存,消息队列,DomainScoket等技术,而node进程间的通信由IPC管道支持,在window环境下,IPC管道借由命名管道实现,*nix借由Unix Domain Socket实现。
实际上,fork进程之前,父进程会先创建一个IPC通道,然后创建子进程,并且将这个IPC通道的文件描述符告诉子进程。子进程通过文件描述符完成IPC通道的连接 fork返回的子进程实例中就有着通信的管道。
句柄传递
围绕node是单线程的问题来看,启动多进程可以解决node利用电脑资源不充足的问题,但是如何才能让子进程去监听同一个端口,满足同一个网络需求呢?一般使用代理起到内部转发的作用,而使用代理进程会过多消耗文件描述符(linux万物以文件始,文件描述符相当于指针,记录指向打开的文件,有一定限制数量,可修改,超过工作就gg了)。
node句柄传递,在网络请求的场景中实际上将句柄文件描述符传给了工作进程,sendHandle可将一个 TCP server 对象句柄或者socket句柄传给子进程,并不是将整个请求发过去了。
// master.js
const childProcess = require("child_process");
const http = require("http");
const cp1 = childProcess.fork(__dirname + '/child.js');
const cp2 = childProcess.fork(__dirname + '/child.js');
const server = http.createServer();
server.listen(8009,(err)=>{
if(err)return ;
cp1.send("server",server);
});
// child.js
process.on('message',(m,server)=>{
if(m === 'server' ){
server.on('connection',(socket)=>{
socket.end("子进程已经被处理了");
});
}
})
在send通过IPC发送消息的时候,仅仅是传递了一些标志的消息,通过这些标志信息,在接收消息一方通过标志信息来还原具体对应的对象,其中的文件标志符发送给子进程,只有一个子进程才能抢到使用。
文件描述符是一件很重要的资源!!!!