深入浅出Nodejs
模块机制
Commonjs规范
-
node的模块实现
步骤:
路径分析
文件定位
-
编译执行
核心模块在node进程启动时,会直接加载进内存中,所以文件定位和编译执行两步可以省略,加载速度快
文件模块则是运行时动态加载,需要经过上面3个步骤
-
优先从缓存加载
会对引入过的模块进行缓存
-
路径分析和文件定位
核心模块/ 以./ 等开头的相对路径文件/以/开头的绝对路径
模块的路径转为真实路径,作为索引
module.paths,依次向上查找node_modules目录
-
模块编译
// 针对js模块进行一层包装 (function(exports, require, module, __filename, __dirname) { var math = require('math') exports.area = function(radius) { return radius } })
exports 和 module.exports的区别
exports是module.exports的引用
exports === module.exports
exports返回的是模块函数,使用点运算符
module.exports返回的是模块对象本身,返回的是一个类
exports.age = 18 moudle.exports = {name: 18} require('.a.js') // 引入的是 module.exports
-
-
npm包规范
-
包结构
Package.json / bin / lib / doc / test
常用功能
-
局域npm
-
AMD / CMD
是解决common js 在客户端中异步加载的场景
-
amd 依赖前置,声明时指定依赖
definde(['dep1, dep2'], function(dep1, dep2) { })
-
cmd, 依赖前置,支持动态引入
define(function(require, exports, module){ ... require('./xx.js') })
-
-
umd
兼容多种模块规范
;(function(root, factory){ if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("vue")); else if(typeof define === 'function' && define.amd) //// AMD环境 CMD环境 define("components", ["vue"], factory); else if(typeof exports === 'object') //// 定义为 通Node模块 exports["components"] = factory(require("vue")); else // 将模块的执行结 在window 量中 在 器中this window对象 root["components"] = factory(root["Vue"]); })(window, function(){...})
异步I/O
-
单线程
无法利用多核cpu的优势和阻塞带来的延迟
-
多进程
进程创建和上下文切换的开销,状态同步和锁的问题
node的方案:
单线程 + 非阻塞异步 I/O
child_process / cluster模块提供多进程,可以利用多核cpu
非阻塞I/O,为了获取i/o响应的数据,需要重复调用i/o是否完成 — 轮询
-
read
重复检查i/o状态是否完成数据的读取
-
select
在read基础上的改进
通过对文件描述符上的事件状态来判断
-
poll
效率最高的i/o事件通知机制
事件订阅,事件通知,执行回调的方式
理想的非阻塞异步I/O
事件循环 event loop
观察者
请求对象
node event loop
process.nextTick 会优于其他microtask执行
-
timer
执行定时器
i/o
Idle /prepare
-
poll
回到timer阶段,执行到时的回调
-
执行poll队列中的事件
poll 中没有定时器的情况下
- poll队列不为空, 便利回调队列并同步执行
- poll队列为空,
- 有setImmediate需要执行, 进入check阶段执行setImmediate
- 没有setImmediate,会等待回调被加入到队列中并立即执行
fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('setImmediate') }) }) // 执行顺序 setImmediate -》 timeout // readFile的回调在poll阶段执行 // 发现setImmediate,去执行 setImmediate // 再到timer阶段执行回调
check
Close
异步编程
函数式编程
高阶函数
-
偏函数,将多参数的函数,返回一个预设部分参数的函数。可接受剩下参赛的函数
柯里化,是偏函数的一种,将多参数的函数,变为接受单一参数的函数
function curry(fn) { var content = this var args = [...arguments].slice(1) return function(){ var finalArgs = args.concat([...arguments]) fn.apply(content, finalArgs) } }
多线程:缺点,上下文切换开销,死锁,同步问题
-
node的优势
基于事件驱动的非阻塞I/O模型
借用v8高性能引擎
-
异步编程难点
1.**异常处理 **
try/catch/final不适用异步编程错误捕获
-
第一参数异常
node在处理异常形成的约定,回调函数的一个参数为err
fs.readFile('dd.js', function(err, data) { // err就是捕获的异常信息 })
2.函数嵌套过深
3.阻塞代码
需要阻塞代码,不能达到真正的线程沉睡,cpu资源会一直为其服务
4.多线程编程
发挥多核cpu的优势,借助web worker模式,node提出child_process,cluster 模块,用于多线程编程
5.异步转同步
-
异步编程的解决方案
-
事件发布/订阅模式
- node中event模块就是发布/订阅模式的实现案例
- 解耦业务逻辑,添加侦听器,当emit事件时,添加在这个事件的侦听器进行变化
- 事件发布/订阅模式的特点为执行流程需要被预先设定
-
promise/deferred模式
-
执行流程不需要被预先设定
$.get('/api').then(res=>{...})
-
promise/deferred模式发布在commonjs规范中,已经抽象出promise/A,promise/B等模型规范
Promise/A规范
定义里3中状态(pendding, resolve,rejected)
promise对象具备then方法即可
状态不可逆
- Promise 用于外部,通过then方法收集事件,
- Deferred 延迟对象,用于内部,维护状态修改,触发相应状态事件
then链式调用
-
-
-
流程控制库
中间件middleware
尾触发与next
-
异步并发控制
避免高并发
- 通过队列来控制并发量,设定阀值,超出部分放入队列中,等调用结束后,从队列中取出并执行
- 超时控制,设置时间阀值
内存控制
V8的垃圾回收机制和内存限制
-
v8的内存限制
64位约1.4GB,32位约0.7GB,导致node无法直接操作大内存对象
-
v8的对象分配
process.memoryUsage() / 查看v8堆内存信息
调整内存限制的大小 --max-old-space-size --max-new-space-size=1024
-
v8垃圾回收机制
-
v8主要的垃圾回收算法
分代式垃圾回收机制
按对象的存活时间分为老生代和新生代
-
Scavenge算法(新生代)
新生代中的对象主要通过scavenge算法进行垃圾回收
采用了cheney算法,一种采用复制的方式实现的垃圾回收算法,将内存一分位二,from和to空间,把from空间中存活的对象复制到to空间,from中非存活的对象被回收,让后将to空间中的对象复制到from空间,to空间清空
缺点是,将内存一分为二,牺牲了空间换时间,所以非常适合新生代中
回收过程中,新生代的对象会晋升为老生代中(满足已经被scavenge过等等条件)
-
标记清除 && 标记整理(老生代)
遍历对象,标记活着的对象,最后清除未被标记的对象
垃圾回收后,有可能造成内存空间不连续的状态(空间碎片),Mark-Compact(标记整理)来解决
Mark-Compact:回收过程中,将活着的对象向一端移动,形成连续新存储
-
incremental marking(增量标记)
垃圾回收会造成js运行停顿
增量标记将垃圾回收拆分为许多小的‘步进’,每完成一段,将执行栈交换回js运行应用,然后再执行垃圾回收。。。,直到垃圾回收完成
-
-
查看垃圾回收日志
node --trace_gc xxxxxxx
高效实用内存
-
作用域(scope)
全局作用域,函数作用域,with作用域
作用域链查找,一直向上查找,直到全局作用域
全局作用域直到进程退出才能释放变量
-
闭包(closure)
实现外部作用域可以访问内部作用域中变量的函数或方法叫做闭包
作用域一直在内存中占用,不会释放
buffer对象不经过v8的内存分配机制,不会有堆内存的大小限制,利用堆外内存可以突破内存限制的问题
内存泄漏
-
缓存
缓存的对象会长驻内存中,由于js对象没有过期策略,会导致缓存长期存在
解决方法:限度缓存对象的大小,加上完善的过期策略(设置过期时间或设定对象大小的阀值)
模块也有缓存机制
node的解决方法:
- 将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效
- 进程之间可以共享缓存
- redis
-
队列消费不及时
消费速度跟不上生成速度,造成队列的累积
- 监控队列的长度,一旦堆积,触发报警系统
- 任意异步调用都包含超时机制
-
作用域未释放
闭包。。。全局变量
内存泄漏排查
- node-heapdump插件
- node-memwatch插件
大内存应用
不可避免还是会操作大文件的场景,使用stream模块,继承EventEmitter,具备事件功能
由于v8的内存限制,使用fs.createReadStream/createWriteStream 替代fs.readFile/writeFile
不考虑字符串的情况下,使用buffer, buffer不受v8堆内存的限制
Buffer
- 类似于数组的二进制数据,他的元素为16进制的两位数,即0到255的数值
内存分配
buffer对象不占用v8的堆内存中,便于处理大量的字节数据,在c++层申请内存,在js中分配内存的策略
slab分配机制
buffer的转换
- 字符串转buffer __new Buffer(str, [encoding])
- buffer转字符串 —— buf.toString([encoding], [start], [end])
- 判读是否支持的编码类型 —— Buffer.isEncoding(encoding)
buffer的拼接
setEncoding(encoding)
buffer与性能
通过预先将静态内容转为buffer对象,可以有效减少cpu的重复使用,节省服务器资源
网络编程
node提供了net,dgram,http,https模块用于搭建服务器
-
tcp
传输层控制协议
面向连接的协议,需要3次握手建立连接, 在创建回话的过程中,服务端和客户端分别提供一个套接字,这两个套接字共同形成一个连接
开启keepalive,一个tcp会话可以用于多次请求和响应
var net = require('net') var server = net.createServer(function(socket) { socket.on('data', function(data) { socket.write('hello word') }) socket.on('end', function(){ console.log('connect abort') }) socket.write('sdjfsdjfs') }) server.listen(8124, function(){ console.log(serve bound) })
// 客户端 var net = require('net') var client = net.connect({port: 8124}, function() { console.log('client connected') client.write('word\r\n') }) client.on('data', function(data){ console.log(data) }) client.on('end', function() { console.log('client disconnected') })
-
服务器事件
- listen事件
- connection事件
- close事件
- error事件
-
连接事件
服务器可以同时与多个客户端保持连接,对每个连接而言是典型的可写可读stream对象,stream对象用于服务器端和客户端之间的通信
-
data
当一端调用了write()发送数据时,另一端会触发data事件
end
connect 用于客户端连接
drain 当一端调用write()发送数据时,当前这端会触发drain事件
error 异常事件
close 当套接字完全关闭时,会触发
timeout 当一定时间后连接不再活跃时,该事件将会被触发,通知用户当前该连接已经被闲置
tcp 中的Nagle算法
针对小数据包,采用延迟,合并数据包,达到一定数量或时间后发出,以此优化网络
tcp默认开启nagle算法,可以调用socket.setNoDelay(true)去掉
-
-
-
UDP
用户数据包协议
不是面向连接的,无需连接
一对多,多对多
安全性,可靠性低
适用于现在的直播,视频
// 创建udp套接字 var dgram = require('dgram') var server = dgram.createSocket('upd4') server.on('message', function(msg, rinfo) { console.log('server got:' + msg + 'from' + rinfo.address + rinfo.port) }) server.on('listening', function() { var address = server.address() console.log('server listening' + address.address + address.port) }) server.bind(41234)
// 客户端 var dgram = require('dgram') var message = Buffer.from('深入浅出node.js') var client = dgram.createSocket('upd4') client.send(message, 0, message.length, 41234, 'localhost', function(err, bytes){ client.close() })
send方法将信息发送到服务端
socket.send(buf, offset, length, port, address, [callback])
- 套接字事件
- message
- listening
- close
- error
- 套接字事件
-
Http
应用层协议,基于tcp协议
基于请求响应式,一问一答实现服务
http服务端的事件
- connection事件 tcp连接后触发
- request事件
- close事件
- checkContinue事件
- connect事件 客户端发起请求时触发
- upgrade事件 客户端升级连接的协议时,服务端接受到时触发
- clientError事件 连接的客户端触发error事件,服务端接受到错误时触发
http客户端
http.request(options, connect) 构造http客户端
var options = { hostname: '127.0.0.1', port: 1234, path: '/', method: 'GET', headers: {}, // auth basic认证, 这个值将被计算成请求头中的authorization部分 } var req = http.request(options, function(res) { res.setEncoding('utf-8') res.on('data', function(chunk) { console.log(chunk) }) }) req.end()
http代理
new http.Agent({ maxSockets: 10, //当前连接池中使用的连接数 requests: 5 // 处于等待状态的请求数 })
http客户端事件
- response
- socket 服务端响应了200状态码,客户端将会触发该事件
- upgrade
- continue 服务端响应100 continue,客户端将触发该事件
构建websocket服务
- 客户端和服务端只要建立一个tcp连接
- server push,双向通信
- 更轻量的协议头,减少数据传送量
// websocket在客户端中应用 var socket = new WebSocket('ws://127.0.0.1:1200/updates') socket.onopen = function() { } socket.onmessage = function(event){ }
websocket的握手
- Sec-WebSocket-Key 用于安全校验, 值为base64编码的字符串
Comet 长轮询
网络服务与安全
- SSL(secure sockets layer)安全套接层,一种安全协议,在传输层对网络连接加密
- 最初ssl应用在web上,后期标准化,称为TLS(transport layer security)安全传输层协议
node提供的3个模块
-
crypto 加密解密
SHA1, MD5
-
tls
提供了与net模块类似的功能,区别在与它建立在TLS/SSL加密的TCP连接上
TLS/SSL
是一个公钥/私钥的结构,它是一个非对称的结构
每个客户端和服务端都有自己的公私钥,公钥用来加密,私钥用来解密
建立安全传输之前,客户端和服务器端之间需要互换公钥,客户端发送数据需要服务端的公钥加密,服务端收到后用自己的私钥解密,反之亦然
node在底层采用的是openssl实现TLS/SSL的,为此生成公钥私钥可以通过openssl完成
// 生成服务器端私钥 > openssl genrsa -out server.key 1024 // 生成客户端私钥 > openssl genrsa -out client.key 1024 //生成公钥 > openssl rsa -in server.key -pubout -out server.pem > openssl rsa -in client.key -pubout -out client.pem
中间人攻击
在客户端和服务器端在交换公钥的过程中,有可能受到中间人攻击,中间人对客户端扮演服务端的角色,对服务端扮演客户端角色,分别获取相应的公钥,存在安全威胁
解决方法:
需要对公钥进行认证,确认得到的公钥出自目标服务器
数字证书,其中包含了服务器的名称和主机名,服务器公钥,签发机构的信息和签名
CA(数字证书认证中心),作用为站点颁发证书,且这个证书中具有ca通过自己公钥和私钥实现的签名,服务器需要通过自己的私钥生成CSR(证书签名请求文件)
中小企业多半采用自签名证书,就是自己扮演CA机构
// 扮演ca机构 生成私钥,生成csr文件,通过私钥自签名生成证书的过程 > openssl genrsa -out ca.key 1024 > openssl req -new -key ca.key -out ca.csr > openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt
服务器申请签名证书之前创建自己的csr文件
> openssl req -new -key server.key -out server.csr
申请签名,需要ca的证书和私钥参与,最后颁发一个带有ca签名的证书
> openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt
客户端发送请求前去获取服务器的证书,并通过ca的证书验证真伪
-
Https
https就是工作在TLS/SSL上的Http
需要私钥和签名证书
// 创建https var https =require('https') var fs = require('fs') var options = { key: fs.readFileSync('./keys/server.key'), cert: fs.readFileSync('./keys/server.crt') } https.createServer(options, function(req, res) { res.writeHead(200) res.end('hello word\n') }).listen(8000)
// https 客户端 var https = require('https') var fs = require('fs') var options = { hostname: 'localhost', port: 8000, path: '/', method: 'GET', key: fs.readFileSync('./keys/client.key'), cert: fs.readFileSync('./keys/client.crt'), ca: [fs.readFileSync('./keys/ca.crt')] } options.agent = new https.Agent(options) var req = https.request(options, function(res) { res.setEncoding('utf-8') res.on('data', function(d) { console.log(d) }) }) req.end() req.on('error', function(e) { console.log(e) })
构建web应用
-
请求方法
req.method
-
路径解析
req.url
var pathname = url.parse(req.url).pathname
-
查询字符串
var url = require('url') var querystring = require('querystring') var query = querystring.parse(url.parse(req.url).query) // 更简洁实现 // var query = url.parse(req.url, true).query // {foo: 'bar', baz: 'val'} // 如果键值出现多次,会解析为数组 // foo=bar&foo=baz // {foo: ['bar', 'baz']}
-
Cookie
http是无状态协议,cookie用来保存状态
服务器生成cookie,发送给浏览器,浏览器保存本地,每次发送请求,携带cookie
// 生成cookie res.setHeader('Set-Cookie': '') Set-Cookie: name=value; Path=/;Expires=Sun,23-Apr-23 09:01:35 GMT; Domain=.domain.com; // 设置HttpOnly, 防止cookie篡改 // 设置Secure 为true,只能在https中有效
Path: cookie影响到的路径
-
expires 和Max-age 设置过期时间,如果没设置,默认cookie只存活在会话器,关闭浏览器,cookie丢失
expires UTC时间格式, 当服务器时间和浏览器时间不一致时,会有偏差
max-age:多久过期
Cookie 性能影响
一旦cookie过多,造成请求头部体积过大,造成待宽的浪费
设置domain,限定使用的域
为静态文件使用不同的域名,不需要发送cookie,缺点是,多个域名就多一次dns查询,好在dns可以缓存
前端可以通过document.cookie修改cookie
目前的应用场景:广告和在线统计领域最为依赖cookie
-
Session
解决cookie的缺点:1请求携带cookie,造成请求头过大,2.前后端都可以篡改cookie, 有安全隐患
session保留在服务器端,客户端无法修改
如何将每个客户和服务器中的数据一一对应起来
-
基于cookie来实现用户和数据的映射
服务器生成session,将sessionId放在cookie中,发送客户端
session是有有效期设置的一般20分钟
客户端每次发送请求携带sessionId,通过cookie发送
服务端接收后校验,如果过期,则重新生成
-
通过查询字符串来实现浏览器和服务器数据的对应
检查请求的查询字符串,如果没有值,会先生成新的带值的url
然后跳转,让客户端重新发起请求
session与内存
统一集中存储在redis中
session与安全
将口令进行签名
-
xss
跨站脚本攻击
反射型 诱导用户点击恶意链接,注入恶意脚本
存储型
在页面中注入恶意脚本代码,发送给服务器,其他用户访问时,接受到恶意脚本,产生安全隐患,比如在评论区注入脚本
解决方法:
-
开启浏览器csp(内容安全策略),本质是建立白名单,规定浏览器只能执行特定来源的代码
通过Content-Security-Policyhttp来开启
cookie设置httpOnly
输入输出字符进行转译过滤
-
-
-
缓存
get请求缓存
检查本地文件是否有缓存
强缓存
-
expires
utc格式时间字符串
缺陷:浏览器时间和服务器时间可能不一致
-
Cache-Control
public/private/no-cache/no-store/max-age
检查服务端文件是否有缓存
协商缓存 发送请求,服务器检查请求头部是否有缓存标识,命中返回304
-
Last-modified / if-modified-since
时间搓改动但内容未必修改
时间搓只能到秒,更新频繁,无法生效
-
Etag / If-None-Match
唯一标识符
var getHash = function(str) { var shasum = crypto.createHash('sha1') return shasum.update(str).digest('base64') } // 服务端 var handle = function(req, res) { fs.readFile(filename, function(err, file) { var hash = getHash(file) var noneMatch = req.headers['if-none-match'] if(hash == noneMatch) { res.writeHead(304, 'not Modified') res.end() }else{ res.setHeader('Etag', hash) res.writeHead(200, 'OK') res.end(file) } }) }
正常链接进入: 匹配强缓存,再匹配协商缓存
页面刷新:跳过强缓存,匹配协商缓存
强制刷新:跳过缓存策略
勾选disable cache 或请求头中设置no-cache,跳过缓存策略
清除缓存
Url
url中跟随版本号
url中跟随hash ,hash发生变化,发送新的请求,推荐
http://url.com/?hash=adjfisdfsd
-
-
Basic认证
当客户端和服务端进行请求时,允许通过用户名和密码实现的一种身份认证方式
检查请求头中的Authorization字段,该字段由认证方式和加密值构成
Authorization: Basic dXNlcjpwYXNz // dXNlcjpwYXNz 是由用户名和密码结合并base64编码的值
如果首次访问网页,请求头中没有携带认证内容,那么浏览器会响应401未授权的状态码
缺点:虽然经过base64加密传送,但是安全系数低
优点:兼容性好,几乎所有的浏览器都支持
-
数据上传
contetn-length代表报文的长度
-
表单数据
Content-Type: application/x-www.form-urlencoded
-
附件上传
content-Type: multipart/form-data; boundary=AaBo3x
boundary=AaBo3x指定每部分的分界符,AaBo3x是随机生成的一段字符串
<form action='/upload' enctype='multipart/form-data'></form>
-
数据上传与安全
-
内存限制
避免大体积数据上传,内存被占光
限制上传内容的大小,超过限制,停止接受
通过流式解析,将数据流导向磁盘中,node中只保留文件路径信息
-
csrf
跨站请求伪造,利用已经登录的用户信息,发送恶意的请求,获取用户信息的一种攻击方式
用户验证
服务器refrere check
token验证
-
-
路由解析
-
文件路径型
- 静态文件
- 动态文件
-
MVC 将业务逻辑按职责分离
- 控制器(Controller),行为的集合
- 模型(Model),数据相关的操作和封装
- 视图(View),视图的渲染
路由解析-》对应的控制器中的行为-》调用相关的模型,进行数据操作-》视图更新
RESTful
-
-
中间件
var middleware = function(req, res, next) { ... next() }
-
异常处理
由于异步方法中的异常不能直接捕获,需要通过next(err) 向外传出
-
中间件与性能
编写高效的中间件
-
合理使用路由
配置路径,app.use('/public', staticFile)
页面渲染
-
内容响应
响应报头中的Content-*字段
Content-Encoding: gzip
Content-length: 22217
Content-Type: text/javascript;charset=utf-8
客户端接收这个报文后,通过gzip来解码报文体的内容,用长度校验内容是否正确,让后以字符集utf-8解码后内容插入到文档中
MIME
-
附件下载
客户端不用打开它,之需弹出下载,
Content-Disposition: inline// 代表内容只需要即时查看; attachment//代码数据可以存为可下载的附件
res.sendfile = function(filePath) { fs.stat(filepath, function(err, stat) { var stream = fs.createReadStream(filepath) res.setHeader('Content-Type', mime.lookup(filepath)) res.setHeader('Content-Length', stat.size) //设置长度 // 设置为附件 res.setHeader('Content-Disposition', 'attachment; filename="'+path.basename(filepath) + "'") res.writeHead(200) stream.pipe(res) }) }
响应json
-
响应跳转
res.redirect = function(url) { res.setHeader('Location', url) res.writeHead(302) res.end('Redirect to' + url) }
-
视图渲染
将数据和模版文件结合,通过模版引擎渲染成最终的html页面
早期的模版语言 jsp, asp, php
破局者: Mustache, 以{{ }}为标志的一套模版语言
//简易模版函数,主要是正则匹配
var render = function(str, data) {
var tpl = str.replace(/<%=(.*)&>/g, function(str, code) {
return " '+ obj." + code + "+ '"
})
tpl = "var tpl = '" + tpl + "'\nreturn tpl;"
// Function中tpl为模版, obj为参数
var complied = new Function('obj', tpl)
return complied(data)
}
1.with的应用
var complie = function(str, data) {
var tpl = str.replace(/<%(.*)%>/g, function(all, code) {
return "'+" + code + "+ '"
})
tpl = "tpl = '" + tpl + "'"
tpl = 'var tpl = "";\nwith(obj) {' + tpl + '}\nreturn top;'
return new Function('obj', tpl)
}
new Function ([arg1[, arg2 [,...argN]]], fnBody)
2.模版安全
转译函数
var escape = function(html) {
return String(html).replace(/&(?!\w+;)/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''')
}
3.模版逻辑
4. 集成文件系统, 引入缓存,避免多次重复编译
5. 子模版 include
-
Bigpipe
翻译为风笛, 是用于调用限流的
用于将页面分割为多个部分,先向用户输出没有数据的布局,将每个部分逐步输出到前端,再最终渲染填充框架,完成整个页面渲染
- 页面布局框架(无数据)
- 后端持续性的数据输出
- 前端渲染
玩转进程
进程: cpu资源分配的最小单位 (工厂)
线程: cpu调度的最小单位 (工人)
nodejs : v8引擎,单线程
单线程的缺点:不能发挥多核cpu的优势,抛出的异常未被捕获处理,会造成进程退出,健壮性和稳定性低
优点: 没有多线程上下文切换的问题,提高cpu的使用率,没有锁,线程同步问题
服务模型的变迁
石器时代: 同步,阻塞,已淘汰
-
青铜时代: 复制进程
通过进程的复制同时服务更多的请求和用户,每个连接需要一个进程服务
-
白银时代: 多线程
线程之间共享数据, 建立线程池,减少创建和销毁线程的开销
多线程上下文切换问题,大并发量时,会暴露一些问题
-
黄金时代: 单线程+事件驱动
node/nginx
解决高并发问题
单线程避免了不必要的内存开销和上下文切换开销
-
多进程架构
解决单线程对多核cpu使用不足的问题,每个进程利用一个cpu
node中提供了child_process /curster模块
主从模式(Master-Worker模式):主进程和工作进程, 典型的分布式架构中用于处理并行业务的模式,主进程不负责具体的业务处理,复制调度和管理工作进程, 工程进程负责具体的业务处理
-
创建子进程
Child_process spawn()/exec()/fork()/execFile()
-
进程间的通信
主线程和工作线程之间通过
onmessage()
postMessage()
子进程对象则通过send() 和message()事件
-
IPC通道
进程间通信(ipc),让不同的进程之间通信
node中实现ipc的事pipe技术
-
句柄传递
多个进程监听同一个端口,会报端口被占用的错误,
现在采用的是主进程监听主端口(如80),主进程对外接收所有的网络请求,再将这些请求分别代理到不同端口的进程上
通过代理,解决端口不能重复被监听,可以适当的负载均衡
child.send(message, [sendHandle]) //第二个可选参数就是句柄
句柄是一种可以用来标识资源的引用,内部包含了指向对象的文件描述符
可以去掉代理这种方案,使主进程接收到socket请求后,将这个socket直接发送给工作进程
var child = require('child_process').fork('child.js') var server = require('net').createServer() server.on('connection', function(socket) { socket.end('handle by parent\n') }) server.listent(1337, function(){ child.send('server', server) //将server传递给子进程 }) // 子进程 process.on('message', function(m, server) { if(m === 'server') { server.on('connection', function(socket) { socket.end('handle by children \n') }) } })
- 句柄的发送与还原
- 端口共同监听
-
-
集群稳定之路
-
进程事件
send() /message()
error
-
exit 子进程退出时触发,正常退出,第一个参数为退出码,被kill()杀死,会得到第二个参数,代表杀死进程的信号
process.exit(1)
process.kill(process.pid, 'SIGTERM')
close 子进程的标准输入输出流中止时触发
-
自动重启
// master.js var fork = require('child_process').fork var cpus = require('os').cpus var server = require('net').createServer() server.listen(1337) var workers = {} var createWorker = function() { // 限量重启 if(xxx){ // 发送giveup事件,不再重启 process.emit('giveup') return } var worker = fork(__dirname + '/worker.js') // 接收到自杀信号后,启动新的进程, 保持总是有新的工作进程存在,可以处理请求 worker.on('message', function(message) { if(message.act === 'suicide') { createWorker() } }) // 退出时重新启动新的进程 worker.on('exit', function() { delete workers[worker.pid] createWorker() }) // 句柄转发 worker.send('server', server) worker[worker.pid] = worker } for(var i=0;i<cpus.length; i++) { createWorker() } // 进程自己退出时, 让所有工作进程退出 process.on('exit', function() { for(var pid in workers) { workers[pid].kill() } })
// worker.js var http = require('http') var server = http.createServer(function(req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}) res.end('handled by child') }) var worker process.on('message', function(m, tcp) { if(m === 'server') { worker = tcp worker.on('connection', function(socket) { server.emit('connection', socket) }) } }) //报错处理 process.on('uncaughtException', function(err) { // 记录日志 logger.error(err) process.send({act: 'suicide'}) //向主进程发送‘自杀’信号 // 停止接收新的连接 worker.close(function() { process.exit(1) // 退出进程 }) // 5秒后退出进程 setTimeout(function() { process.exit(1) }, 5000) })
-
负载均衡
多进程之间监听相同的端口,使用户请求能够分散到多个进程上进行处理,保证多个进程处理的工作量公平的策略就叫负载均衡
将cpu资源都调用起来
node默认提供的机制是采用操作系统的抢占式策略,就是闲置的进程对请求进行抢夺,谁抢到谁服务,它的繁忙由cpu和i/o构成,影响抢占的是cpu的繁忙度,有可能存在cpu空闲,但是i/o忙的情况,这样去抢占服务,会形成负载不均衡
node v0.11提供了新的策略, Round-Robin(轮叫调度),由主进程接收请求服务,依次发给工作进程
//启用round-robin
cluster.schedulingPolicy = cluster.SCHED_RR
// 不启用
cluster.schedulingPolicy = cluster.SCHED_NONE
状态共享
第三方数据存储(数据库, 磁盘, redis等)
主动通知, 进程通知
cluster模块
创建单机node集群
//cluster.js
var cluster = require('cluster')
cluster.setupMaster({
exec: 'worker.js'
})
var cpus = require('os').cpus()
for(var i = 0; i< cpus.length; i++) {
cluster.fork()
}
执行node cluster.js, 和上面用child_process创建子进程集群效果相同
Cluster.setupMaster() /cluster.fork()创建子进程
cluster原理
- cluster模块是child_process 和net模块的组合应用
- 内部隐式创建tcp服务
cluster 事件
- fork 复制一个工作进程后触发该事件
- online
- listening
- disconnect 主进程和工作进程之间ipc通道断开后会触发
- exit 进程退出
- setup cluster.setupMaster()执行后触发
测试
-
单元测试
代码规范:
- 单一职责
- 接口抽象
- 层次分离
单元测试介绍
-
断言
assert模块
Should.js 断言库
var assert = require('assert') assert.equal(Math.max(1, 100), 100) // 如果不满足期望,则会抛出AssertionError异常,整个程序会停止执行
ok() 判断结果是否为真
equal 是否相等
notEqual() 是否不相等
deepEqual() 是否深度相等
strictEqual() 是否严格相等
throws() 判断代码块是否抛出异常
doesNotThrow() 判断代码块是否没有抛出异常
ifError() 判断实际值是否为一个假值(null, undefined, 0, '', false)
-
测试框架, 用来管理测试用咧和生成测试报告
mocha
测试风格
tdd(测试驱动开发)
-
bdd(行为驱动开发)
describe("#indexOf()", function() { it('should return -1 when not present', function() { [1,2,3].indexOf(4).should.equal(-1); }) it('shoudl return index when present', function(){ [1,2,3].indexOf(1).should.equal(0); [1,2,3].indexOf(2).should.equal(1); [1,2,3].indexOf(3).should.equal(2); }) })
异步测试
产品化
项目工程化
项目的组织能力
目录结构
-
构建工具
合并,压缩文件,打包应用,编译模块等
grunt,webpack
-
编码规范
文档式约定——靠自觉
代码提交时强制检查——考工具
jsLint/EsLint
-
代码审查
代码托管平台gitlab/github
git拉取分支,完成编程,合并到主支
-
部署流程
开发-》审查-》合并-》部署
-
性能
-
动静分离
node处理静态文件的能力不算突出,将图片,脚本样式等静态文件托管到nginx或cdn静态服务器上,node只处理动态请求即可
单独部署到静态服务器的好
-
启用缓存
避免重复的请求和计算
redis
-
多进程架构
利用多核cpu的优势,保障服务更健壮,持续化的服务
cluster模块,child_process
社区中提供了pm,forever,pm2模块
-
读写分离
针对数据库,读取速度远远高于写入速度,写入的时候会锁表,会影响读取速度
将数据库读写分离,主从设计
-
日志
建立健全的排查和跟踪机制
还原问题现场,定位问题
分割日志
-
监控报警
日志监控
响应时间
进程监控
磁盘监控
内存监控
cpu占用监控
网络流量监控
应用状态监控
dns监控
-
报警系统
邮件报警/短信或电话报警
稳定性
-
多机器
更多的硬件资源
-
多机房
解决地理位置带来的网络延迟问题,在容灾方面,机房之间互为备份
容灾备份至少4台服务器来构建稳定的服务集群
-
-