【译】Node.js子进程:你需要知道的一切

原文地址
如何使用 spawn(), exec(), execFile(), and fork()

Node.js中的单线程,非阻特性非常适合单个进程。但是,一个CPU中的一个进程终归不足以应付应用程序日益增加的工作量。无论您的服务器可能有多强大,单个线程只能支持有限的负载。Node.js在单个线程中运行的事实并不意味着我们不能利用多个进程,当然也可以利用多个机器。

使用多个进程是扩展Node应用程序的最佳方式。 Node.js设计用于构建具有许多节点的分布式应用程序。这就是为什么它被命名为Node。可扩展性被放入平台,而不是在应用程序的生命周期中开始考虑的事情。

请注意,在阅读本文之前,您需要对Node.js事件和流的理解很好。如果你还没有,我建议你阅读这两篇文章之前阅读这篇文章:
了解Node.js事件驱动架构
Node.js Streams:你需要知道的一切

子过程模块

我们可以使用Node的child_process模块​​轻松地转换子进程,这些子进程可以通过消息传递系统轻松地进行通信。我们可以控制子进程输入流,并监听其输出流。我们还可以控制要传递给底层操作系统命令的参数,我们可以使用该命令的输出来做任何我们想要的操作。例如,我们可以将一个命令的输出管道作为另一个命令的输入(就像我们在Linux中一样),因为这些命令的所有输入和输出都可以使用Node.js流呈现给我们。

请注意,本文中将使用的示例都是基于Linux的。在Windows上,您需要使用Windows替代方案切换我使用的命令。

在Node中创建子进程有四种不同的方法:spawn(), exec(), execFile(), and fork()。我们将看到这四个功能之间的差异以及何时使用它们。

Spawned Child Processes

spawn函数在新进程中启动命令,我们可以使用它来传递该命令任何参数。例如,这里是生成一个将执行pwd命令的新进程的代码。

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

我们简单地从child_process模块​​中重新构建spawn函数,并使用OS命令作为第一个参数来执行。
执行spawn函数(上面的子对象)的结果是一个ChildProcess实例,它实现EventEmitter API。这意味着我们可以直接在此子对象上注册事件的处理程序。例如,当子进程退出时,可以通过注册退出事件的处理程序来执行某些操作:

child.on('exit', function (code, signal) {
  console.log('child process exited with ' +
              `code ${code} and signal ${signal}`);
});

上面的处理程序给出了子进程的退出代码以及用于终止子进程的信号(如果有的话)。当子进程正常退出时,该信号变量为空。我们可以使用ChildProcess实例注册处理程序的其他事件是 disconnect, error, close, 和 message。

  • 当父进程手动调用child.disconnect函数时,将触发 disconnect 事件。
  • 如果进程无法生成或被杀死,则会发出error 事件。
  • 当子进程的stdio流关闭时,关闭事件被发出。
  • message 事件是最重要的事件。当子进程使用process.send()函数发送消息时,它被发出。这是父/子进程可以相互通信的方式。我们将在下面看到一个例子。

每个子进程也可以获取三个标准的stdio流,我们可以使用child.stdin,child.stdout和child.stderr访问它们。

当这些流被关闭时,正在使用它们的子进程将发出关闭事件。此关闭事件与退出事件不同,因为多个子进程可能共享相同的stdio流,因此一个子进程退出并不意味着流已关闭。

由于所有流都是事件发射器,因此我们可以收听附加到每个子进程的stdio流上的不同事件。与正常进程不同的是,在子进程中,stdout / stderr流是可读流,而stdin流是可写的。这基本上是在主流程中发现的那些类型的倒数。我们可以使用这些流的事件是标准的。我们可以使用这些流的事件是标准的。最重要的是,在可读流中,我们可以监听数据事件,这将在命令的输出或执行命令时遇到任何错误:

child.stdout.on('data', (data) => {
  console.log(`child stdout:\n${data}`);
});

child.stderr.on('data', (data) => {
  console.error(`child stderr:\n${data}`);
});

上面的两个处理程序将会将两个案例记录到主进程stdout和stderr。当我们执行上面的spawn函数时,pwd命令的输出被打印,子进程退出代码0,这意味着没有发生错误。

我们可以使用spawn函数的第二个参数传递由spawn函数执行的命令的参数,该函数是要传递给命令的所有参数的数组。 例如,要使用-type f参数在当前目录下执行find命令(仅列出文件),我们可以做:

const child = spawn('find', ['.', '-type', 'f']);

如果在命令执行期间发生错误,例如,如果我们给出上面找到一个无效的目标,则会触发child.stderr data 事件处理程序,并且exit 事件处理程序将报告1的退出代码,这表示一个 发生错误 错误值实际上取决于主机操作系统和错误类型。

子进程stdin是一个可写的流。 我们可以使用它来发送命令一些输入。 就像任何可写的流一样,消耗它的最简单的方法是使用管道(pipe)功能。 我们只需将一条可读的流管理到一个可写的流中。 因为主进程stdin是一个可读的流,我们可以将它管道到一个子进程stdin流中。 例如:

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

const child = spawn('wc');

process.stdin.pipe(child.stdin)

child.stdout.on('data', (data) => {
  console.log(`child stdout:\n${data}`);
});

在上面的例子中,子进程调用wc命令,它在Linux中排列行,字和字符。 然后我们将主进程stdin(这是一个可读流)管道进入子进程stdin(这是一个可写的流)。 这种组合的结果是我们得到一个标准的输入模式,我们可以输入一些东西,当我们按Ctrl + D时,我们输入的内容将被用作wc命令的输入。

我们也可以管理多个进程的标准输入/输出,就像我们可以用Linux命令一样。 例如,我们可以将find命令的stdout引导到wc命令的stdin来计数当前目录中的所有文件:

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

const find = spawn('find', ['.', '-type', 'f']);
const wc = spawn('wc', ['-l']);

find.stdout.pipe(wc.stdin);

wc.stdout.on('data', (data) => {
  console.log(`Number of files ${data}`);
});

我添加了-l参数到wc命令,使它只计算行。执行时,上述代码将输出当前所有目录下所有文件的计数。

Shell语法和exec函数

默认情况下,spawn函数不会创建一个shell来执行我们传入的命令。 这使得它比exec函数稍微高效一点,它创建一个shell。 exec函数有另外一个主要区别。 它缓冲命令的生成输出,并将整个输出值传递给一个回调函数(而不是使用流,这是什么spawn)。
这是以前的find | wc示例使用exec函数实现。

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

exec('find . -type f | wc -l', (err, stdout, stderr) => {
  if (err) {
    console.error(`exec error: ${err}`);
    return;
  }

  console.log(`Number of files ${stdout}`);
});

由于exec函数使用shell来执行命令,所以我们可以直接使用shell语法来利用shell管道功能。
请注意,如果您正在执行外部提供的任何种类的动态输入,那么使用shell语法会带来安全隐患。 用户可以使用shell语法字符简单地执行命令注入攻击,如 ; 和 $(例如command + ’; rm -rf ~’

exec函数缓冲输出,并将其传递给回调函数(exec的第二个参数)作为stdout参数。 这个stdout参数是我们要打印输出的命令的输出。
如果需要使用shell语法,并且从命令中预期的数据的大小很小,那么exec函数是一个很好的选择。 (记住,exec将在返回之前缓冲内存中的整个数据。)
当命令的预期数据大小时,生成函数是一个更好的选择,因为该数据将与标准IO对象一起流式传输。

如果我们想要,我们可以使生成的子进程继承其父进程的标准IO对象,更重要的是,我们可以使spawn函数也使用shell语法。这是使用spawn函数实现的相同的find | wc命令:

const child = spawn('find . -type f', {
  stdio: 'inherit',
  shell: true
});

由于上述stdio:'inherit'选项,当我们执行代码时,子进程继承主进程stdin,stdout和stderr。这将导致子进程数据事件处理程序在主process.stdout流中被触发,从而使脚本立即输出结果。
由于上面的shell:true选项,我们可以在传递的命令中使用shell语法,就像我们用exec一样。但是使用这段代码,我们仍然可以获得产生函数给我们的数据流的优势。这真的是两个世界最好的。
除了shell和stdio之外,还有一些其他很好的选择,我们可以在child_process函数的最后一个参数中使用。例如,我们可以使用cwd选项来更改脚本的工作目录。例如,这里使用一个使用外壳程序的spawn函数,并将工作目录设置为“我的下载”文件夹,这是完全相同的计数全文件示例。这个cwd选项将使脚本计数我在~/Downloads中的所有文件:

const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true,
  cwd: '/Users/samer/Downloads'
});

我们可以使用的另一个选项是env选项来指定新子进程可见的环境变量。 该选项的默认值为process.env,它允许任何命令访问当前进程环境。 如果我们想重写这个行为,我们可以简单地传递一个空对象作为env选项或者新的值被认为是唯一的环境变量:

const child = spawn('echo $ANSWER', {
  stdio: 'inherit',
  shell: true,
  env: { ANSWER: 42 },
});

上面的echo命令无法访问父进程的环境变量。 例如,它不能访问$HOME,但它可以访问$ANSWER,因为它通过env选项作为自定义环境变量传递。
这里解释的最后一个重要的子进程选项是分离选项,这使得子进程独立于其父进程运行。
假设我们有一个保持事件循环繁忙的timer.js文件:

setTimeout(() => {  
  // keep the event loop busy
}, 20000);

We can execute it in the background using the detached option:

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

const child = spawn('node', ['timer.js'], {
  detached: true,
  stdio: 'ignore'
});

child.unref();

The exact behavior of detached child processes depends on the OS. On Windows, the detached child process will have its own console window while on Linux the detached child process will be made the leader of a new process group and session.
If the unref function is called on the detached process, the parent process can exit independently of the child. This can be useful if the child is executing a long-running process, but to keep it running in the background the child’s stdio configurations also have to be independent of the parent.
The example above will run a node script (timer.js) in the background by detaching and also ignoring its parent stdio file descriptors so that the parent can terminate while the child keeps running in the background.

Gif captured from my Pluralsight course — Advanced Node.js
The execFile function
If you need to execute a file without using a shell, the execFile function is what you need. It behaves exactly like the exec function, but does not use a shell, which makes it a bit more efficient. On Windows, some files cannot be executed on their own, like .bat or .cmd files. Those files cannot be executed with execFile and either exec or spawn with shell set to true is required to execute them.
The *Sync function
The functions spawn, exec, and execFile from the child_process module also have synchronous blocking versions that will wait until the child process exits.

const { 
  spawnSync, 
  execSync, 
  execFileSync,
} = require('child_process');

Those synchronous versions are potentially useful when trying to simplify scripting tasks or any startup processing tasks, but they should be avoided otherwise.
The fork() function
The fork function is a variation of the spawn function for spawning node processes. The biggest difference between spawn and fork is that a communication channel is established to the child process when using fork, so we can use the send function on the forked process along with the global process object itself to exchange messages between the parent and forked processes. We do this through the EventEmitter module interface. Here’s an example:
The parent file, parent.js:

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

const forked = fork('child.js');

forked.on('message', (msg) => {
  console.log('Message from child', msg);
});

forked.send({ hello: 'world' });
The child file, child.js:
process.on('message', (msg) => {
  console.log('Message from parent:', msg);
});

let counter = 0;

setInterval(() => {
  process.send({ counter: counter++ });
}, 1000);

In the parent file above, we fork child.js (which will execute the file with the node command) and then we listen for the message event. The message event will be emitted whenever the child uses process.send, which we’re doing every second.
To pass down messages from the parent to the child, we can execute the send function on the forked object itself, and then, in the child script, we can listen to the message event on the global process object.
When executing the parent.js file above, it’ll first send down the { hello: 'world' } object to be printed by the forked child process and then the forked child process will send an incremented counter value every second to be printed by the parent process.

Screenshot captured from my Pluralsight course — Advanced Node.js
Let’s do a more practical example about the fork function.
Let’s say we have an http server that handles two endpoints. One of these endpoints (/compute below) is computationally expensive and will take a few seconds to complete. We can use a long for loop to simulate that:

const http = require('http');
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const sum = longComputation();
    return res.end(`Sum is ${sum}`);
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

This program has a big problem; when the the /compute endpoint is requested, the server will not be able to handle any other requests because the event loop is busy with the long for loop operation.
There are a few ways with which we can solve this problem depending on the nature of the long operation but one solution that works for all operations is to just move the computational operation into another process using fork.
We first move the whole longComputation function into its own file and make it invoke that function when instructed via a message from the main process:
In a new compute.js file:

const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  };
  return sum;
};

process.on('message', (msg) => {
  const sum = longComputation();
  process.send(sum);
});

Now, instead of doing the long operation in the main process event loop, we can fork the compute.js file and use the messages interface to communicate messages between the server and the forked process.

const http = require('http');
const { fork } = require('child_process');

const server = http.createServer();

server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const compute = fork('compute.js');
    compute.send('start');
    compute.on('message', sum => {
      res.end(`Sum is ${sum}`);
    });
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

When a request to /compute happens now with the above code, we simply send a message to the forked process to start executing the long operation. The main process’s event loop will not be blocked.
Once the forked process is done with that long operation, it can send its result back to the parent process using process.send.
In the parent process, we listen to the message event on the forked child process itself. When we get that event, we’ll have a sum value ready for us to send to the requesting user over http.
The code above is, of course, limited by the number of processes we can fork, but when we execute it and request the long computation endpoint over http, the main server is not blocked at all and can take further requests.
Node’s cluster module, which is the topic of my next article, is based on this idea of child process forking and load balancing the requests among the many forks that we can create on any system.
That’s all I have for this topic. Thanks for reading! Until next time!
If you found this article helpful, please click the

虚拟机与Docker 镜像、容器

Docker vs 虚拟机

虚拟机在软件层面上通过模拟硬件的输入和输出,让虚拟机的操作系统得以运行在没有物理硬件的环境中。
而docker和宿主机共享kernel,只需要搬运工kernel的输入输出。
所以虚拟机启动需要几十秒,而Docker容器可以在数毫秒内启动。

容器与镜像

容器(container)的定义和镜像(image)几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。

容器 = 镜像 + 读写层。并且容器的定义并没有提及是否要运行容器。
imgserver.png

构建镜像

  1. docker commit
    可以想象为git,创建一个容器,在容器中做一些修改,最后提交成为一个新的容器。
  2. Dockerfile
    Dockerfile的第一条指令必须是FORM

通过docker build执行Dockerfile中的全部命令。

推荐使用Dockerfile构建,相比第一种方式更加灵活。

从镜像启动容器

docker run

docker run命令类似于git pull命令。git pull命令就是git fetch 和 git merge两个命令的组合,同样的,docker run就是docker create和docker start两个命令的组合。

参考文献:
10张图带你深入理解Docker容器和镜像

Let's Encrypt 实现全站 HTTPS

获取证书

访问 Certbot 获取相应安装命令。(Certbot官方的自动化获取、部署和更新安全证书的工具)
安装完Certbot后执行certbot certonly

之后会提示如下内容:

How would you like to authenticate with the ACME CA?
-------------------------------------------------------------------------------
1: Place files in webroot directory (webroot)
2: Spin up a temporary webserver (standalone)
-------------------------------------------------------------------------------

webroot 模式会要你输入网站根目录(例如 /var/www/example)然后会在其中创建 .well-known 文件夹,这个文件夹里面包含了一些验证文件,certbot 会通过访问 example.com/.well-known/acme-challenge 来验证你的域名是否绑定的这个服务器。

standalone 模式会要你输入域名(例如 vps.zhangchen915.com),然后程序会启动服务器的443端口,来验证域名的归属(注意验证时443端口不能被其它服务占用,如 Nginx)。
 
个人感觉第二种比较方便。

验证成功后,会生成四个证书文件cert.pem  chain.pem  fullchain.pem  privkey.pem,他们会被放在/etc/letsencrypt/live/目录下对应的域名文件夹中。

配置 nginx

如果你不太熟悉nginx,推荐访问Mozilla SSL Configuration Generator生成配置文件。
这里生成的配置文件应还会有两部分(server{...}是一部分),第一部分的作用是将80端口的HTTP服务301重定向到对应的HTTPS地址,第二部分是配置SSL连接。

nginx 的默认配置在 /etc/nginx/conf.d/文件夹下。如果你之前有HTTP服务的话,你应该修改过default.conf,如一些反向代理,备份一份,然后用第一部分替换掉。

然后在ssl.conf中写入第二部分的配置

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
     ssl_certificate /etc/letsencrypt/live/<你的域名>/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/<你的域名>/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    ....

    # 注意:Let's Encrypt 签发的证书, 可以忽略ssl_stapling_verify 和 ssl_trusted_certificate 两个配置
    ## verify chain of trust of OCSP response using Root CA and Intermediate certs
    # ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;

    # resolver <IP DNS resolver>;

    <之前在default.conf的配置可以放在这里>
}

最后,让nginx重新载入配置文件service nginx reload就能看到效果了。

单IP配置多个https server

在单个IP地址上运行多个HTTPS服务器的更通用的解决方案是TLS服务器名称指示扩展(SNI,RFC 6066),允许浏览器在SSL握手期间传递所请求的服务器名称,因此服务器将知道链接用哪个证书。

但是SNI需要服务端和浏览器都支持。

  • 服务端
     运行nginx -V,如果返回信息里包括TLS SNI support enabled,则表明OpenSSL支持SNI。
  • 浏览器
    IE 7及以上版本(注意:在XP系统中任何版本的IE浏览器都不支持SNI)

nginx的配置也很简单,在对应的配置文件加入server_name www.example.org;即可。

完全向前保密

完全向前保密 (PFS) 是一个 IPSec 属性,用于确保其中一个专用密钥在今后发生泄漏时,派生的会话密钥不会泄漏。

为了防止第三方发现密钥值,IPSec 使用完全向前保密 (PFS)。 PFS 将根据双方在交换时提供的值定期创建新的密钥值。 由于双方都提供了一个只有他们知道的随机值,因此生成的每个新密钥不同于先前创建的密钥。
使用 PFS 表示,即使第三方设法拦截对称密钥,也只能短时间地使用拦截的密钥。 此外,由于新创建的密钥并非基于先前拦截的密钥,因此第三方必须开始新的暴力计算以猜测新的密钥值。 在 IPSec 中启用了 PFS 的情况下,创建新密钥花费的时间长于未使用 PFS 的情况。 但是,使用 PFS 有助于阻止第三方拦截数据和对数据进行解码。

PFS 使用了『迪菲-赫尔曼加密法』,所以需要一些额外的配置。
1、需要生成一个 dhparam.pem 文件。这个文件在 DH 握手时会跟传给客户端。这是“DHE加密法”需要用上的东西。
2、告诉 Nginx 使用不要使用默认的加密算法。

我们执行如下命令:
openssl dhparam -out /etc/letsencrypt/live/<你的域名>/dhparam.pem 2048
生成完成后在nginx加入如下配置:`

# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
    ssl_dhparam /etc/letsencrypt/live/<你的域名>/dhparam.pem;

续签

证书默认90天过期,但是我们可以执行更新操作告诉 Let's Encrypt 我们机器还活着。这件事情可以直接交给crontab定时任务来完成。crontab在ubuntu中服务名为cron。下面这段内容表示每隔1个月的凌晨 2:30执行更新操作,更多关于crontab命令相关内容可以参考这里

30 2 * */1 * certbot renew --pre-hook "service nginx stop" --post-hook "service nginx start"

参考文献:
Configuring HTTPS servers
完全向前保密
Nginx 开启 https 双证书指南

理解TCP与UDP

TCP与UDP属于网络中的运输层,运输层解决的是进程与进程之间的通信

一.UDP(用户数据报协议)

1.发送数据之前不需要建立连接
2.不保证可靠交付
3.面向报文,给UDP多少报文,他就原样发送,既不合并也不拆分。
4.没有拥塞控制,可以一对多,多对一交互通信

二、TCP(传输控制协议)

1.面向连接的字节流,应用程序在使用TCP协议之前,先要建立TCP连接,在发送报文时会根据对方给出窗口值和当前网络拥塞状态自定决定一个报文段的大小。
2.只能一对一通信
3.提供可靠交付
4.提供双工通信

一句话理解:UDP就像发邮件,TCP就像打电话