多进程
0. 单线程和多进程
多线程服务模型在很多web服务器上得到使用(如Apache),一个线程服务一个请求。这种模型带来的问题是较大的内存和上下文切换开销。
一些web服务器采用单线程、事件驱动的方式,如Nginx、node,减少了内存和上下文切换开销。
单线程的问题是,不能很好地利用多核cpu。因此node提供了多进程的能力,通过child_process
模块管理子进程。
1. node进程和子进程
使用node执行一个脚本就会启动一个进程,脚本及其依赖的模块可以在process
全局变量中访问进程相关信息
child_process
模块用于操作子进程,比如spawn()
、exec()
、execFile()
、fork()
,每个方法对应不同参数。可以执行shell或启动一个可执行文件或者执行js脚本
使用多进程架构的node web服务器通常使用master-worker的主从架构,即一个主进程和受主进程管理的多个子进程。
2. 进程间通信
使用send
和on('message')
通信,示例如下
// parent.js
var child_process = require('child_process');
var child = child_process.fork('./child.js');
child.on('message', function(m){
console.log('message from child: ' + JSON.stringify(m));
});
child.send({from: 'parent'});
// child.js
process.on('message', function(m){
console.log('message from parent: ' + JSON.stringify(m));
});
process.send({from: 'child'});
3. 集群稳定
进程其它操作
除了”message”,进程还有其它事件可以被监听:
- error
- exit
- close
disconnect()
方法用于关闭IPC通道
kill()
方法用来给进程发送一个SIGTERM信号,进程通过process.on('SIGTERM', callback)
来监听
另外进程上还有stdin
、stdout
对象,标识进程的标准输入输出
自动重启的最佳实践
- 进程触发
exit
事件时,父进程将此进程从子进程集合中删除,并重新创建一个进程,加入到子进程集合中 - 子进程监听到未被捕获的异常时候,停止接收新连接,等已有连接断开后退出进程,这样父进程监听到子进程退出就会自动重启了
- “自杀信号”:基于第二步改进,子进程监听到未被捕获的异常时候,停止接收新连接,并发送一个“自杀信号”给父进程,父进程重新创建一个子进程,子进程等已有连接断开后退出进程
- 断开连接的操作设置一个超时时间,超时强制退出
- 限制单位时间重启的子进程个数
负载均衡
负载均衡是服务器用于保证多个处理单元工作量公平的策略
负载均衡可以让服务对系统资源利用率更高
具体实现时master进程根据一定的策略将任务分配给worker进程
node提供了一种策略Round-Robin
,轮叫调度,即任务依次分发。cluster中通过cluster.schedulingPolicy
来进行设置
状态共享
node不允许在多个进程间共享数据,因此需要一种方案来实现多进程间数据共享
多个进程之间共享数据的方式是用第三方数据存储,每个进程都可以对其进行访问。对第三方数据存储进行监听有不同方式
- 每个进程定时轮询
- 设置一个通知服务,定时轮询第三方存储,并实时通知各个子进程。当然子进程需要做一些工作:第一次启动时候读取一次第三方存储,然后将自己的信息注册到通知服务
4. cluster
cluster模块是child_process和net模块的组合应用。cluster模式有个限制,是每个子进程只能是node进程,使用child_process则可以更灵活地开启子进程,可以是其他类型的程序。
两种创建集群的方式
- 每个子进程监听不同端口,主进程监听主端口(80)再由主进程向子进程分发请求
- 主进程监听端口,并将请求转发给子进程,子进程监听同一端口
cluster使用第二种方式实现,cluster使用示例如下
// server.js
var cluster = require('cluster');
var cpuNums = require('os').cpus().length;
var http = require('http');
if(cluster.isMaster){
for(var i = 0; i < cpuNums; i++){
cluster.fork();
}
}
else{
http.createServer(function(req, res){
res.end(`response from worker ${process.pid}`);
}).listen(3000);
console.log(`Worker ${process.pid} started`);
}
master和worker的通信
cluster.fork()方法是通过child_process.fork实现的,因此可以通过message实现进程间通信
如何实现端口共享
通过上面示例代码可以看到,多个子进程监听了相同的端口。通常如果不同的进程监听相同的端口会报错。那么cluster是如何实现多个子进程监听相同端口的呢?
net模块中的listen会进行判断,如果是在主进程中,则监听相应端口,如果实在子进程中,则只是建立IPC管道,等待父进程传递socket句柄然后进行处理。父进程接收到请求后,会将socket句柄传递给子进程,由于子进程使用父进程传递的句柄,对应同样的文件描述符,因此不会有冲突
如何实现请求转发
每当worker进程创建server实例来监听请求,都会通过IPC通道,在master上进行注册。当客户端请求到达,master会负责将请求转发给对应的worker。
参考
模块
Node模块
0. 前言
commonjs是JavaScript的社区规范,目的是为了让JavaScript可以弥补JavaScript缺乏标准的缺陷。commonjs规范内容包括模块、二进制、buffer、字符集编码、io流、进程环境、文件系统、套接字、单元测试、包管理等。
Node借鉴commonjs的Modules规范实现了一套模块系统。
对于使用者来说,我们关注的是模块如何使用,即如何定义模块、如何引用模块。
而当涉及到了原理层面,我们希望知道Node模块的分类以及如何实现模块的引用
1. 模块语法
- 模块定义 require
- 模块引用 exports.attr = ‘’; module.exports = {};
- 模块标识 字符串、相对路径、绝对路径
2. 模块类型
- 核心模块 nodejs内置模块,如http、fs等,这些已经编译为二进制代码,加载速度最快
- 文件模块 用户自定义模块
- 路径形式的文件模块
- 自定义模块
3. 模块引入步骤
首先,Node对加载过的模块都有缓存,二次加载时候会更快
- 路径分析,首先根据模块类型(核心模块、路径形式的模块、自定义模块)确定引入方式
- 文件定位,对于核心模块和路径形式的文件模块,比较容易定位,对于自定义模块,会查找这个模块对应的目录数组,然后逐个尝试各个路径,最终定位模块。如果最终没有定位到模块,会抛出异常
- 模块编译,Node编译模块时候进行头尾包装,隔离作用域
4. 兼容多种模块规范
;(function (name, definition) {
// 检查上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function',
// 检查上下文环境是否为Node
hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) {
define(definition);
}
else if (hasExports) {
module.exports = definition();
}
else {
this[name] = definition();
}
})('hello', function () {
var hello = function () {};
return hello;
});
网络编程
Node 网络编程
0. 前言
Node提供了网络编程的能力。让开发者可以实现网络收发的功能。利用Node提供的模块,我们可以很轻松地实现发起网络请求和接受请求并返回响应(server)的功能。
Node提供了3个主要的用于网络编程的模块:net、dgram和http。其中,net用来实现TCP数据传输;dgram用来实现UDP数据传输;http则用来实现http协议的数据传输。每个模块都提供了API用来发送请求和接受请求并响应。
对于网络请求的发送,我们关注的API是
- 如何发送请求
- 如何接受响应
而对于server的实现,我们关注的API是
- 如何监听端口
- 如何接受请求
- 如何返回响应
下面我们通过代码示例来看下我们关注的API
1. net
// server.js
var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
// tcp服务端
var server = net.createServer(function(socket) {
console.log('客户端已连接');
socket.on('data', function(data) {
console.log('服务端:收到客户端数据,内容为{'+ data +'}');
// 给客户端返回数据
socket.write('你好,我是服务端');
});
});
server.listen(PORT, HOST, function() {
console.log('服务端:开始监听来自客户端的请求');
});
// client.js
const net = require('net');
const client = net.createConnection({ port: 8124 }, () => {
//'connect' listener
console.log('connected to server!');
client.write('world!\r\n');
});
client.on('data', (data) => {
console.log(data.toString());
client.end();
});
client.on('end', () => {
console.log('disconnected from server');
});
我们可以看到使用net模块需要调用其方法创建client或者server
client.write()用来发送请求;client.on(‘data’)用来接收响应
server.listen用来监听端口;创建server的回调中的socket实例可以用来接受请求数据(socket.on('data')
);socket.write()用来返回响应
2. dgram
server.js
// 例子:UDP服务端
var PORT = 33333;
var HOST = '127.0.0.1';
var dgram = require('dgram');
var server = dgram.createSocket('udp4');
server.on('listening', function () {
var address = server.address();
console.log('UDP Server listening on ' + address.address + ":" + address.port);
});
server.on('message', function (message, remote) {
console.log(remote.address + ':' + remote.port +' - ' + message);
});
server.bind(PORT, HOST);
然后,创建UDP socket,向端口33333发送请求。
client.js
// 例子:UDP客户端
var PORT = 33333;
var HOST = '127.0.0.1';
var dgram = require('dgram');
var message = Buffer.from('My KungFu is Good!');
var client = dgram.createSocket('udp4');
client.send(message, PORT, HOST, function(err, bytes) {
if (err) throw err;
console.log('UDP message sent to ' + HOST +':'+ PORT);
client.close();
});
diagram.createSocket()用来创建一个client或者serve
client.send()用来发送请求
server.bind()用来监听端口;server.on(‘message’)用来接受请求
3. http
var http = require('http');
// http server 例子
var server = http.createServer(function(serverReq, serverRes){
var url = serverReq.url;
serverRes.end( '您访问的地址是:' + url );
});
server.listen(3000);
// http client 例子
var client = http.get('http://127.0.0.1:3000', function(clientRes){
clientRes.pipe(process.stdout);
});
http.get()方法直接发送请求,http.createServer()用来创建一个server(回调中的req和res都是Stream类型的实例,使用Stream相关的接口可以处理数据)
http.get()方法用来发送请求;回调参数clientRes用来接收响应
server.listen()用来监听端口;createServer创建server的回调中的req参数用来接受请求(req.url
);res.end()方法用来返回响应
我们web server最常用的是http模块。目前主流的Node web server 框架express和koa就是对http模块的封装。这些框架的封装主要屏蔽了请求解析的细节,比如通常我们使用的路由功能,都是框架对请求的url进行解析,然后找到使用框架的开发者注册的路由对应的响应方法调用,就实现了相应的功能
4. https
https用法与http很像
https客户端:
var https = require('https');
https.get('https://www.baidu.com', function(res){
res.on('data', function(data){
process.stdout.write(data);
});
});
可以看到,https客户端用法与http类似。需要考虑的问题是,如果访问的网站安全证书不受信任,https模块会报错。有两中方法可以访问证书不受信任的网站
代码执行方式是:
https.get(options, callback)
options中包括访问的url和其他配置信息信息
- options中配置忽略安全警告
- options中配置证书(需要提前下载)
https server端:
创建https server需要证书,执行如下:
https.createServer(options, callback)
options中配置了私钥和证书文件路径。证书和私钥可以通过购买或者使用openssl
工具生成。
文件操作
Node 文件操作(fs)
0. 前言
Node提供了文件操作的能力,具体是通过fs模块来提供的。通过对文件操作,前端可以实现很多功能。最常用的就是构建阶段的自动化,通过操作文件,我们可以对项目进行依赖分析、编译、压缩、图片等静态资源处理等等。
fs默认异步操作,通过回调获知操作后的结果,每个API都有对应的同步版本
1. fs模块API
1. 文件操作
- readFile、createReadStream 读文件(对于大文件可以用createReadStream流式读取,也可以使用第三方的
readline
模块) - writeFile、createWriteStream 写文件
- unlink 删除文件
- appendFile 文件追加内容
- rename 重命名
- access 判断文件权限、是否存在
- stat 获取文件信息
- watchFile 监听文件变化
2. 目录操作
- 创建目录 mkdir
- 遍历目录 readdir
- 删除目录 rmdir
stream
流的概念
计算机处理数据时候,可能有不同的情况:可能拿到需要的全部数据进行处理,再输出结果;也可能拿到数据的一部分,然后处理这部分并输出结果,然后再取数据、处理、输出结果,直到处理完所有的数据。
对于第二种情况,有一些场景
比如,直播时候的流媒体,就是等到网络中的音视频数据传递过来时候存入buffer,然后不断播放buffer中的数据
比如,如果需要读取一个较大文件进行处理,就需要一点一点读入数据到内存,处理完后释放内存,否则内存无法容纳这么多的数据
计算机中流的概念就是对这种场景下这种数据操作方式的抽象
那么流这种抽象应该包含哪些接口呢?
因为读写流的具体操作对于不同应用(比如文件读写和网络收发等)都是不同的,因此流只是分块读入或者分块写入时候抛出一系列事件让应用层代码处理。比如读数据流ReadStream应该读入一块数据之后就抛出’data’事件,将读入的数据抛出;读入数据完成后抛出’end’事件等等
另外,可能有需求是将一个流里的数据导入另一个流中,Node中Stream通过pipe()方法定义这种操作
Node stream
流是一个抽象接口,被Node中很多对象实现
- http模块的request、response
- process.stdin、process.stdout
- net模块的socket
- fs.createReadStream()
- zlib.createDeflate()
Node中有4种基本的流类型
- Readable:用来读取数据,比如 fs.createReadStream()
- Writable:用来写数据,比如 fs.createWriteStream()
- Duplex:可读+可写,比如 net.Socket()
- Transform:在读写的过程中,可以对数据进行修改,比如 zlib.createDeflate()(数据压缩/解压)
每种类都有自己相应的方法
所有的Stream对象都是EventEmitter的实例,常用事件有
- data 当有数据可读时触发。
- end 没有更多的数据可读时触发。
- error 在接收和写入过程中发生错误时触发。
- finish 所有数据已被写入到底层系统时触发。
常用的方法有
- read() 读入流
- write() 写入流
- on() 监听事件
- pipe() 输出到另一个流
下面是代码示例
// read()
const fs = require('fs');
var readable = fs.createReadStream('./test.js');
readable.setEncoding('UTF8');
readable.on('data', () => {
let chunk;
while (null !== (chunk = readable.read(100))) {
console.log(`接收到 ${chunk.length} 字节的数据`);
}
});
// write()
const fs = require('fs');
const writeStram = fs.createWriteStream(./test.js);
writeStram.write('hello world');
writeStram.end();
// on()
const fs = require('fs');
const readStream = fs.createReadStream('./test.js');
const content = '';
readStream.setEncoding('utf8');
readStream.on('data', function(chunk){
content += chunk;
});
readStream.on('end', function(chunk){
// 文件读取完成
console.log('文件读取完成,文件内容是 [%s]', content);
});
// pipe()
// 将input.txt内容复制到output.txt中
const fs = require('fs');
const readStream = fs.createReadStream('./input.txt');
const writeStream = fs.createWriteStream('./output.txt');
readStream.pipe(writeStream);
console.log('执行完毕');
// 压缩
const fs = require('fs');
const zlib = require('zlib');
fs.createReadStream('./input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));
console.log('执行完毕');
// 解压
const fs = require('fs');
const zlib = require('zlib');
fs.createReadStream('./input.txt.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('input.txt'));
console.log('执行完毕');
pm2
pm2使用
守护进程管理
我们运行node程序时候,希望以守护进程方式运行,这样就不会每个程序占用一个终端窗口并且终端窗口终止后我们的程序也会停止运行。
pm2是一个守护进程管理工具,它可以让我们的程序以守护进程方式运行,并且提供了很多很强大的功能来保证我们的程序稳定运行。
pm2的使用
1. 运行方式:命令行和配置文件
使用pm2运行node程序有两种方式:命令行和配置文件
我们可以使用pm2命令来管理进程,进行开启或者停止等操作
// 运行node程序
pm2 start app.js
// 运行其他程序
pm2 start bashscript.sh
pm2 start python-app.py --watch
pm2 start binary-file -- --port 1520
// 其他操作
pm2 restart <app_name|process_id>
pm2 reload <app_name|process_id>
pm2 stop <app_name|process_id>
pm2 delete <app_name|process_id>
也可以使用配置文件:ecosystem file
生成配置文件模板 pm2 ecosystem
,这个命令会生成一个ecosystem.config.js:
接下来我们就可以通过配置文件操作我们的程序了
pm2 [start|restart|stop|delete] ecosystem.config.js
配置文件有两个主要字段:app和deploy
其中app中配置了程序运行相关参数,deploy配置了程序部署相关参数
2. 进程管理
pm2提供了一些api来管理进程
- start,启动一个进程
- stop,停止一个进程
- delete,停止一个进程,并从pm2 list中删除
- restart,重启一个进程
- reload,0秒停机重载进程(主要用于网络进程),只在cluster模式下生效
- list,获取pm2管理的运行中的进程列表
3. cluster负载均衡
使用pm2启动程序有两种模式:’cluster’和’fork’,默认为fork。
两者的区别在于,cluster模式使用node的cluster模块管理进程,子进程只能是node程序,提供了端口复用和负载均衡、集群稳定的一些机制;fork模式使用node的child_process的fork方法管理子进程,子进程可以是其他程序(如php、Python),需要开发者自己实现端口分配和负载均衡的子进程业务逻辑
4. 扩展集群
// 指定子进程数量为2
pm2 scale app 2
// 增加3个子进程
pm2 scale app +3
5. 日志
pm2 logs
命令用来输出pm2的日志
也可以在ecosystem.config.js中配置log_file字段来指定日志输出
6. 监控
我们可以通过pm2 monitor
或者PM2.io
监控应用程序,提进程运行情况的信息
pm2 web
api则会启动一个叫pm2-http-interface的web sever,监听9615端口。我们访问相应主机上的端口即可获取CPU、内存运行情况等信息
7. watch模式
—watch参数用来让pm2监控文件改动自动重启
pm2 start app.js --watch
pm2 stop app.js --watch
eggjs简介
eggjs简述
0. 说明
koa是一个node server框架,封装了http模块。使用koa开发node server,其实是实现一系列的中间件,框架接收到请求经过一系列中间件处理,最终输出响应。
eggjs是一个企业级web server开发框架,它封装了koa,增强了koa开发体验,并提供了应用开发的一些约定和支持和工程化支持。
使用eggjs开发有以下特点:
- 工程化支持完善,开发环境、代码检查、部署等
- 面向未来,扩展方便:支持自定义或使用第三方中间件、插件
- 目录约定,多人开发时候模式统一,大型项目方便管理
- 将应用合理分层:router、controller、service、model,应用可维护、可读性有保证
- 提供定时任务支持
- 通过载入代码-自动挂载代码到对象的方式解决了到处写import/require的问题, 不再需要手动维护模块之间的依赖关系
1. 目录结构
├── app
| ├── router.js // 路由
│ ├── controller // 控制器
│ | └── home.js
│ ├── service (可选) // 服务层
│ | └── user.js
│ ├── middleware (可选) // 中间件
│ | └── response_time.js
│ ├── schedule (可选) // 定时任务
│ | └── my_task.js
│ ├── public (可选) // 静态资源
│ | └── reset.css
│ ├── view (可选) // 模板
│ | └── home.nj
│ ├── model(可选)// 数据模型、表的映射
│ | └── user.js
│ └── extend (可选) // 扩展
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
请求的处理主要逻辑写在router、controller、service,而不需要像使用koa开发时候那样,通过实现路由中间件来实现请求处理。
基本处理流程是:请求->router->controller->service。
router负责转发路由,controller负责解析和处理请求参数,service负责具体的业务逻辑。
2. 工程化
- 代码检查
- 部署多进程管理
- 本地开发与线上部署
3. 开发步骤
可以使用egg-init命令行工具快速生成一个eggjs项目模板。
注意使用vscode的话,可以使用egg插件,这样输入egg时候可以根据提示快速生成代码模板。