github地址:戳这里javascript
目标:写一个基于事件驱动 ,非阻塞i/o 的web服务器,以达到更高的性能。构建快速,可伸缩的网络应用平台php
js开发性能低,事件驱动应用html
node强制不共享任何资源的 单线程 ,单进程系统,包含十分适宜网络的库前端
两个readFile的操做最终时间为最慢的那一个java
事件编程方式:轻量级,轻耦合,只关注事务点等优点node
单线程nginx
前言:c++
web 1.0 : JavaScript用于表单校验和网页特效,只有对bom,dom的支持git
web 2.0 : 提高了网页的用户体验,bs应用展示出了比cs(须要装客户端)应用优越的地方。h5崭露头角github
此过程经历了工具-组件-框架-应用的变迁
js的规范缺陷:
require()
同步,为后端js指定的规范,并不彻底适合前端的应用场景
已被编译进了二进制执行文件,node启动时就被加载进内存,因此1.2步骤能够省略。且加载速度最快
动态加载,速度比核心模块慢
路径分析
..
或者 .
相对路劲模块/
开头的绝对路径模块connect
模块若是想加载与核心模块标识符相同的模块,必须选择 不一样的标识符 或者 换用路径 的方法
以 .
,..
,/
开头的标识符,会将路径转换成真实路径
自定义模块是最费时的
module.paths
模仿搜索路径
规则以下:
文件定位
文件扩展名
.js
.node
.json
顺序补齐
fs
模块同步阻塞式的判断文件是否存在,若是是.node
和.json
文件,带上扩展名再配合缓存能够加快速度
目录和包的处理
package.json
,取出 main
属性指定的文件名定位。package.json
, 会将 index
做为默认文件名编译执行
node会新建一个模块对象,而后根据路径载入并编译,对应不一样扩展名,载入方法不一样:
.js
经过 fs
同步读取.node
经过 dlopen()
加载.json
经过fs读取,再 JSON.parse
.js
每个编译成功的模块都会被绑定在 Module._cache
上
编译过程对文件内容进行头尾包装
// 经过vm原生模块runInThisContext方法执行,不污染全局
(function (exports, require, module, __filename, __dirname) {
})
复制代码
另外,这样会出错
exports = function () {
// My class
}
复制代码
缘由在于,exports对象是经过形参的方式传入的,直接赋值会改变形参的做用,但并不能改变做用域外的值。
c++模块主内完成核心,js主外实现封装
性能优于脚本语言
被编译成二进制文件,一旦node开始执行,就直接加载进缓存
依赖关系:文件模块 <--
核心模 块<--
内建模块
package.json
包描述文件
bin
存放可执行二进制文件的目录
lib
存放js的代码目录
doc
存放文档
test
存放单元测试用例
查看帮助npm help
安装依赖包npm install --save/--save-dev express
-g是讲一个包安装到全局可用的可执行命令。它根据包描述文件中的bin字段配置,将实际脚本链接到与node可执行文件相同的路径下
若是node可执行文件的位置是/usr/local/bin/node
,那么模块目录就是/usr/local/lib/node_modules
。最后经过软连接方式将bin字段配置的可执行文件连接到node的可执行目录下
本地安装
换源:
npm install underscore --registry=http:registry.url
npm config set registry http:registry.url
npm钩子
发布包
npm adduser
npm publish<folder>
npm owner ls <package_name>
npm owner add <user> <package_name>
npm owner rm <user> <package_name>
6. 分析包 npm ls
node模块引入几乎都是同步的,但若是前端模块也采用同步的方式来引入,用户体验会形成问题
须要用define来明肯定义一个模块,而在node实现中是隐式包装的。
全部的依赖,经过形参传递到依赖模块内容中
define(['dep1', 'dep2'], function (dep1, dep2) {
return function () {}
})
复制代码
目的是做用域隔离
内容须要返回的方式实现导出
define(function () {
var exports = {};
exports.sayHello = function () {
...
}
return exports
})
复制代码
更接近commonjs规范
define(function (require, exports, module) {
// ...
})
复制代码
require,exports, module经过形参传递给模块。
;(function (name, definition) {
var hasDefine = typeof define === 'function';
var hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) { // AMD或者CMD
define(definition);
} else if(hasExports) { // 定义为普通模块
module.exports = definition()
} else {
this[name] = definition()
}
})('hello', function () {
var hello = function () {}
return hello
})
复制代码
node面向网络而设计
利用单线程,原理多线程死锁,状态同步问题
利用异步i/o,让单线程原理阻塞,更好的利用cpu
内核在进行文件i/o的操做时,经过文件描述符进行管理,文件描述符相似于应用程序与系统内核之间的凭证。
阻塞i/o形成cpu等待浪费,非阻塞却要 轮询 去确认是否彻底完成数据获取
理想非阻塞异步i/o:发起非阻塞调用后,能够直接处理下一个任务,只需i/o完成后经过信号或回调将数据传递给应用程序
显示的异步i/o:经过让部分线程进行阻塞i/p或者非阻塞i/o加轮询技术来完成数据获取,让一个线程进行计算处理,经过线程之间的通讯将i/o获得的数据进行传递
用户体验
若是是同步,js执行ui渲染和响应将处于停滞状态
采用异步,在下载资源期间,js和ui的执行都不会处于等待状态
采用异步方式所花时间为max(m, n)
资源分配
缺点:
单线程同步编程模型会由于阻塞i/o致使性能差,
缺点:
代价在于建立线程和执行期线程上下文切换的开销较大
多线程常面临锁,状态同步问题
优势:
可是能有效提高cpu利用率
模型基本要素:事件循环,观察者,请求对象,i/o线程池
node自身实际上是多线程的,只是i/o线程使用的cpu较少
每一个事件循环中有一个或者多个观察者
异步i/o过程当中的重要中间产物,全部的状态都保存在这个对象中,包括送入线程池等待执行以及i/o操做完毕后的回调处理
建立的定时器会被插入到定时器观察者内部的一个红黑树中
每次Tick执行时,会从红黑树中迭代取出定时器对象,检查是否超过定时时间。若是超过,就造成一个时间,它的回调函数将当即执行
时间复杂度O(lg(n)) 2. process.nextTick
将回调函数放入队列,在下一轮Tick时取出执行
时间复杂度 0(1)
服务器模型:
node高性能:
var toString = Object.prototype.toString;
var isType = function (type) {
return function (obj) {
return toString.call(obj) == '[object' + type + ']'
}
}
var isFunction = isType('Function')
复制代码
异步i/o提交请求和处理结果两个阶段中间,有事件循环的调度。异步方法则一般在提交请求后当即返回,由于一场并不必定发生在这个阶段,因此try/catch在这里无效
try/catch对于callback执行时抛出的异常无能为力
事件发布/订阅模式
var events = require('events');
function Stream () {
events.EventEmitter.call(this)
}
util.inherits(Stream, events.EventEmitter)
复制代码
利用事件队列解决雪崩问题,once方法
多异步之间的写做方案
Promise/Deferred
Promise/A
只有三种状态:rejected,fullfiled, rejected
只能未完成到完成,或者失败,不能逆反
状态不能更改
流程控制库
js在浏览器的应用场景,因为运行时间短,随着进程的推出,内存会释放,几乎没有内存管理的额必要
内存控制正式在海量请求和长时间运行的前提下进行探讨的。
在服务器端,资源寸土寸金
对于性能敏感的服务器端程序,内存管理的好坏,垃圾回收情况的优良,影响很大
在node中经过js使用内存时,只能使用部分,没法直接操做大内存对象
64位系统下约为1.4GB,32位系统下约为0.7GB
node中使用js对象,都是经过V8来进行分配和管理的
js对象经过堆来分配
当在代码中生命变量并赋值时,所使用对象的内存就分配在堆中。若是已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆得大小超过V8的限制为止
V8为什么限制堆得大小:表层缘由是起初为浏览器而设计,限制值已经绰绰有余。深层缘由是V8的垃圾回收机制的限制,作一次非增量式的垃圾回收时间花销大
V8垃圾回收策略主要基 分代式垃圾回收机制
垃圾回收算法:
将内存分为 新生代 和 老生代
新生代中的对象为存活时间较短的对象,老生代的对象为存活时间较长或常驻内存的对象
Scavenge算法
具体实现主要采用Cheney算法
采用复制的方式实现垃圾回收算法。
将堆内存一分为二。每一份空间成为semispace。处于闲置状态的称为To空间,处于使用状态的称为From空间。
当开始进行垃圾回收时,会检查From空间的存活对象,这些存活对象会被复制到To空间。非存活对象占用空间会被释放
缺点:用空间换时间
当一个对象通过屡次复制依然存活时,被认为是生命周期较长的对象。被移到老生代中。称为晋升
对象晋升的条件:
经过检查它的内存地址来判断。若是经历过了,从From复制到老生代
缺点:1. 存活对象较多时,复制存活对象的效率低。 2. 浪费通常空间
Mark-Sweep(标记清除)
Mark-Compat(标记整理)
对象在标记为死亡后,整理过程当中,将活着的对象往一端移动。完成后,直接清理掉边界外的内存
在空间不足以对重新生代晋升过来的对象进行分配时才使用
Incremental Marking
延迟清理和增量清理
并行标记和并行清理
小结:
node --trace_gc -e "..."
能够了解垃圾回收的运行情况,找出哪些阶段比较费时
node --prof xx.js
会在该目录下生成v8.log文件,获得性能分析数据
node --prof-process isolate-0x103001200-v8.log
因为日志文件不具有可读性,故这样能够统计日志信息
做用域
var foo = function () {
var local = {};
}
foo();
复制代码
内存回收过程:只被局部变量引用的对象存活周期较短,会被分配在新生代的From空间,在做用域释放后,局部变量local失效,引用的对象会在下次垃圾回收时被释放
标识符查找:
js在执行时回去找该变量在哪里定义,在当前做用域没有查到,将会向上级的做用域里查找,直到查到为止
做用域链:
根据在内部函数能够访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。
执行环境:
js为每个执行环境关联了一个变量对象。环境中定义的全部变量和函数都保存在这个对象中。
变量的主动释放:
全局变量,直到进程退出才释放。引用的对象常驻内存(老生代)。
能够用delete操做和从新赋值(null或者undefined)
实现外部做用域访问内部做用域中变量的方法
做用域中产生的内存占用不会获得释放。除非再也不有引用,才会逐步释放
进程的内存一部分是rss,其他部分在交换区或者文件系统中
$ node
> process.memoryUsage()
{
rss: // 常驻内存
heapTotal: // 总申请的内存量
heapUsed: // 使用中的内存量
}
> os.totalmem() // 总内存
> os.freemem() // 闲置内存
复制代码
Buffer对象并不是经过V8分配,没有堆内存的大小闲置
小结:受V8的垃圾回收限制的主要是V8堆内存
哪怕一字节的内存泄漏也会形成堆积,垃圾回收过程当中将会耗费更多时间进行对象描述,应用响应缓慢,直到进程内存溢出,应用奔溃
缘由:
缓存中存储的键越多,长期存活对象也就越多,常驻在老生代
普通对象无过时策略
var cached = {};
function get (key) {
if (cached[key]) {
return cached[key]
} else {
}
}
function set (key, value) {
cached[key] = value;
}
复制代码
解决:
缓存限制策略
超过数量,先进先出的方式进行淘汰
设计模块时,应添加清空队列的相应接口
缓存的解决方案
进程间没法共享内存
队列消费速度低于生产速度,将会造成堆积。而js相关做用域也不会获得释放,内存占用不会回落,从而出现内存泄漏
解决方案:
node中大多数模块都有stream应用。因为V8内存限制,采用流实现对大文件的操做
若是不须要进行字符串层面的操做,则不须要V8来处理,尝试进行纯粹的Buffer操做
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
var data = ''
reader.on('data', function (chunk) {
data += chunk
})
reader.on('end', function () {
console.log(data)
})
复制代码
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
render.setEncoding('utf8')
复制代码
setEncoding的时候,可读流对象在内部设置了一个decoder对象。每次data事件都经过该decoder对象进行Buffer到字符串的解码。
decoder的对象会暂时存储,buffer读取的剩余字节
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
var chunks = [];
var size = 0;
reader.on('data', function (chunk) {
chunks.push(chunk);
size += chunk.length;
})
reader.on('end', function () {
var buf = Buffer.concat(chunks, size);
console.log(buf.toString())
})
复制代码
在web领域,大多数的编程语言须要专门的web服务器做为容器,如ASP、ASP.NET须要IIS做为服务器,PHP须要打在Apache或Nginx环境等,JSP须要Tomcat服务器等。但对于Node而言,只须要几行代码便可构建服务器,无需额外的容器。
TCP
建立TCP服务器端
const net = require('net');
let server = net.createServer();
server.on('connection', function (socket) {
console.log('connection')
})
server.listen(8000)
复制代码
UDP不是面向链接的。
一个套接字能够与多个UDP服务通讯,它虽然提供面向事务的简单不可靠信息传输服务,在网络差的状况下存在丢包严重的问题
优势:无链接,资源消耗低,处理快速且灵活
应用:音频,视频,dns服务
const dgram = require('dgram');
const server = dgram.createSocket('udp4')
server.on('error', (err) => {
console.log(`服务器异常:\n${err.stack}`);
server.close();
});
server.on('message', (msg, rinfo) => {
console.log(`服务器收到:${msg} 来自 ${rinfo.address}:${rinfo.port}`);
});
server.on('listening', () => {
const address = server.address();
console.log(`服务器监听 ${address.address}:${address.port}`);
});
server.bind(1000)
复制代码
特色:
res.writeHead(()
res.write() // 发送数据
res.end()
复制代码
http服务端事件
http客户端
示例:
var req = http.request(options, function (res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log(chunk)
})
})
复制代码
在keepalive的状况下,一个底层会话链接能够屡次用于请求。为了重用tcp链接,能够用http.globalAgent客户端代理对象
默认状况下,经过ClientRequest对象对同一个服务器发起的http请求最多能够建立五个链接
如需改变,可在options中传递agent选项
var agent = new http.Agent({
maxSockets: 10
})
var options = {
hostname: '127.0.0.1',
port: 1334,
path: '/',
method: 'GET',
agent: agent
}
复制代码
特色:
好处:
握手完成后,再也不进行http交互,客户端的onopen将会触发执行
当客户端调用send发送数据时,服务端触发onmessage事件;当服务端调用send发送数据时,客户端触发message事件。
当send发送一条数据时,协议可能将这个数据封装为一帧或多帧数据,而后逐帧发送
交换公钥过程当中,可能遇到中间人攻击,因此应引入数字证书来认证。
建立私钥:
openssl genrsa -out ryans-key.pem 2048
生成csr
openssl req -new -sha256 -key ryans-key.pem -out ryans-csr.pem
生成自签名证书
openssl x509 -req -in ryans-csr.pem -signkey ryans-key.pem -out ryans-cert.pem
验证:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('./ryans-key.pem'),
cert: fs.readFileSync('./ryans-cert.pem')
}
https.createServer(options, function (req, res) {
res.writeHead(200);
res.end('hello world')
}).listen(2000)
复制代码
-k忽略掉证书的验证
curl -k https://localhost:2000
HTTP_Parser在解析请求报文的时候,将报文头抽取出来,设置为req.method。有诸如:GET, POST, HEAD, PUT, DELETE, OPTIONS, TRACE, CONNECT
路径部分存在于报文的第一行的第二部分,如:
GET /path?foo=bar HTTP/1.1
HTTP_Parser将其解析为req.url, 通常而言,完整的url地址以下
http://user:pass@host.com:8080/p/a/t/h?query=string#hash
这里hash部分会被丢弃,不会存在于报文的任何地方, 下列的url对象不是报文中的,故有hash
解析出来的url对象
Url {
protocol: 'https:',
slashes: true,
auth: 'user:pass',
host: 'sub.host.com:8080',
port: '8080',
hostname: 'sub.host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash' }
复制代码
查询字符串,若是键出现屡次,那么它的值会是一个数组
foo=bar&foo=baz
复制代码
var query = url.parse(req.url, true).query;
{
foo: ['bar', 'baz']
}
复制代码
cookie处理:
Set-Cookie: name=vale; Path=/;Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
path表示cookie影响路径,表示服务器目录下的子html都能访问
expires和max-age表示过时时间,一个是绝对时间,一个是相对时间
httpOnly告知浏览器不能经过document.cookie获取
secure为true表示在https才有效
domain:子域名访问父域名
**性能影响:**大多数cookie并不须要每次都用上,由于这会形成带宽的部分浪费
解决:
session的数据只保留在服务器端,客户端没法修改。
应用:
将口令放在cookie中,口令一旦被褚昂爱,就丢失映射关系。一般session的有效期一般短,过时就将数据删除
一旦服务器检查到用户请求cookie中没有携带session_id,它会为之生成一个值,这个值是惟一且不重复的值,并设定超时时间。若是过时就从新生成,若是没有过时,就更新超时时间
var sessions = {};
var key = 'session_id';
var EXPIRES = 20*60*1000;
var generate = function () {
var session = {};
session.id = (new Date().getTime()) + Math.random();
session.cookie = {
expire: (new Date()).getTime() + EXPIRES
}
sessions[session.id] = session
}
function (req, res) {
var id = req.cookies[key];
if (!id) {
req.session = generate();
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > new Date().getTime()) {
session.cookie.expire = new Date().getTime() + EXPIRES;
req.session = session;
} else {
delete sessions[id];
req.session = generate();
}
} else {
req.session = generate();
}
}
}
复制代码
原理:检查查询字符串,若是没有值,会生成新的带值的url
var getURL = function (_url, key, value) {
var obj = url.parse(_url, true);
obj.query[key] = value;
return url.format(obj);
}
function (req, res) {
var redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(302);
res.end();
}
var id = req.query[key];
if (!id) {
var session = generate();
redirect(getURL(req.url), key, session.id);
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > new Date().getTime()) {
session.cookie.expire = new Date().getTime() + EXPIRES;
req.session = session;
handle(req, res);
} else {
delete sessions[id];
var session = generate();
redirect(getURL(req.url), key, session.id)
}
} else {
var session = generate();
redirect(getURL(req.url), key, session.id)
}
}
}
复制代码
因为session存储在sessions对象中,故在内存中,若数据量加大,会引发垃圾回收的频繁扫描,引发性能问题。
为了利用多核cpu而启动多个进程,用户请求的链接将可能随意分配到各个进程中,node的进程与进程之间不能直接共享内存,用户的session可能会引发错乱
将session集中化,将可能分散在多个进程里的数据,统一转移到集中数据存储中。目前经常使用工具是redis,memcached。node无需在内部维护数据对象。
问题: 会引发网络访问
设置last-modified
var handle = function (req, res) {
fs.stat(filename, function (err, stat) {
var lastModified = stat.mtime.toUTCString();
if (lastModified === req.headers['if-modified-since']) {
res.writeHead(304, 'Not Modified');
res.end()
} else {
fs.readFile(filename, function (err, file) {
var lastModified = stat.mtime.toUTCString();
res.setHeader('Last-modified', lastModified);
res.writeHead(200, 'ok');
res.end(file);
})
}
})
}
复制代码
缺陷:
设置etag
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['if-none-match'];
if (hash === noneMath) {
res.writeHead(304, "Not Modified");
res.end()
} else {
res.setHeader("ETag", hash);
res.writeHead(200, "ok");
res.end(file);
}
})
}
复制代码
强制缓存
var handle = function (req, res) {
fs.readFile(filename, function (err, file) {
res.setHeader("Cache-Control", "max-age=" + 10*365*24*60*60*1000);
res.writeHead(200, "ok");
res.end(file);
})
}
复制代码
用expires可能致使浏览器端与服务器端时间不一样步带来的不一致性问题
清除缓存
浏览器是根据url进行缓存,那么一旦内容有所更新时,咱们就让浏览器发起新的url请求,使得新内容可以被客户端更新。
var hasBody = function (req) {
return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
}
function (req, res) {
if (hasBody(req)) {
var buffers = [];
req.on('data', functino (chunk) {
buffers.push(chunk);
})
req.on('end', function () {
req.rawBody = Buffer.concat(buffers).toString(); // 拼接buffer
handle(req, res);
})
} else {
handle(req, res);
}
}
复制代码
处理json格式
// application/json;charset=utf-8;
var mime = function (req) {
var str = req.headers['content-type'] || '';
return str.split(';')[0]
}
var handle = function (req, res) {
if (mime(req) === 'application/json') {
try {
req.body = JSON.parse(req.rawBody);
} catch(e) {
res.writeHead(400);
res.end("Invalid JSON");
return
}
}
todo(req, res)
}
复制代码
处理xml文件
var xml2js = require('xml2.js');
var handle = function (req, res) {
if (mime(req) === 'appliction/xml') {
xml2js.parseString(req.rawBody, function (err, xml) {
if (err) {
res.writeHead(400);
res.end('Invalid XML');
return;
}
req.body = xml;
todo(req, res);
})
}
}
复制代码
图片上传
var formidable = require('formidable'),
http = require('http'),
util = require('util'),
fs = require('fs');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
fs.renameSync(files.upload.path,"./tmp/text.jpeg"); // 另存图片
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:\n\n');
res.end(util.inspect({fields: fields, files: files}));
});
return;
}
if (req.url == '/')
// show a file upload form
res.writeHead(200, {'content-type': 'text/html'});
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload" multiple="multiple"><br>'+
'<input type="submit" value="Upload">'+
'</form>'
);
}).listen(8080);
复制代码
在解析表单,json和xml部分,咱们采起的策略是先保存用户提交的全部数据,而后再解析处理,最后才传递给业务逻辑。
弊端:数据量大,占内存
解决方案:
限制大小方案代码:
var bytes = 1024;
function (req, res) {
var received = 0;
var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
if (len && len > bytes) {
res.writeHead(413);
res.end();
return;
}
req.on('data', function (chunk) {
received += chunk.length;
if (received > bytes) {
req.destroy();
}
})
handle(req, res);
}
复制代码
var generateRandom = function (len) {
return crypto.randomBytes(Math.ceil(len*3/4)).toString('base64').slice(0, len);
}
var token = req.session._csrf || (req.session._crsf = generateRandom(24));
// 作页面渲染的时候服务器端渲染这个_csrf
复制代码
function (req, res) {
var token = req.session._csrf || (req.session._csrf = generateRandom(24));
var _csrf = req.body._csrf;
if (token !== _csrf) {
res.writeHead(413);
res.end("禁止访问");
} else {
handle(req, res);
}
}
复制代码
mvc工做模式
手工映射
自由映射,从入口程序中判断url,而后执行对应的逻辑。
匹配的时候,可以正则匹配
天然映射
/controller/action/param1/param2/param3
按约定去找controllers目录下的user文件,将其require出来,调用这个文件模块的setting方法,其他的参数直接传递到这个方法中
RESTful(representational state transfer)
须要区分请求方法
一个地址表明了一个资源,对这个资源的操做,主要体如今http请求方法上,不是体如今url上
设计:
POST,GET,PUT,DELETE
POST /user/add?username=jack
GET /user/remove?username=jack
复制代码
中间件
含义:指底层封装细节,为上层提供更方便服务的意义,为咱们封装全部http请求细节处理的中间件
中间件性能
缓存须要重复计算的结果,避免没必要要的计算。
响应头中的content-*字段十分重要。
示例
Content-Encoding:gzip
Content-Length:21170
Content-Type:text/javascript;charfset=utf-8
复制代码
客户端在接收到后,经过gzip来解码报文体重的内容,用长度校验报文体内容是否正确,而后在以字符集utf-8将解码后的脚本插入到文档节点中
application/json, application/xml, application/pdf
背景:不管响应的内容是什么MIME,只须要弹出并下载它
Content-Disposition
判断是应该将报文数据当作及时浏览的内容,仍是可下载的附件。
inline // 内容只需查看
attachment // 数据能够存为附件
复制代码
还能指定保存时使用的文件名
Content-Disposition:attachment;filename="filename.txt"
响应附件api
res.sendfile = (filepath) => {
fs.stat(filepath, (err, stat) => {
let 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);
})
}
复制代码
res.json = function (json) {
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(JSON.stringify(json))
}
复制代码
res.redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(200);
res.end('redirect to' + url)
}
复制代码
res.render = function (view, data) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
var html = render(view, data);
res.end(html)
}
复制代码
模板要素:
function render (str, data) {
var tpl = str.replace(/<%=([\s\S]+?)%>/g, function (match, code) {
return "' + obj." + code + "+ '";
})
tpl = "var tpl = '" + tpl + "'\nreturn tpl;";
var compiled = new Function('obj', tpl);
return compiled(data);
}
复制代码
集成文件系统
fs.readFile('file/path', 'utf8', function (err, txt) {
if(err) {
res.writeHead(500, {'Content-Type': 'text/html'});
res.end('模板文件错误');
return;
}
res.writeHead(200, {"Content-Type": "text/html"});
var html = render(compile(text), data);
res.end(html);
})
复制代码
这样作每次都须要读取模板文件,所以可设置cache={}
模板性能
一个进程只能利用一个核,如何充分利用多核cpu服务器
单线程上抛出的异常没有被捕获,如何保证进程的健壮性和稳定性
一次只为一个请求服务
经过进程的赋值同时服务更多的请求和用户。进程赋值会致使内存浪费
一个线程服务一个请求,线程相对于进程的开销要小,线程之间能够共享数据,内存浪费问题获得解决
可是线程上线文切换会产生时间消耗
解决高并发问题
单线程避免没必要要的内存开销和上下文切换
php为每一个请求都简历独立的上下文
master.js实现进程的复制
let fork = require('child_process').fork;
let cpus = require('os').cpus();
for (let i = 0; i < cpus.length; i++) {
fork('./worker.js');
}
复制代码
worker.js
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {"Content-Type": "text/plain"});
res.end('hello')
}).listen(parseInt(Math.random()*10000), '127.0.0.1')
复制代码
ps aux | grep worker.js
查看进程的数量
lejunjie 3306 0.0 0.0 4267752 868 s001 S+ 11:18上午 0:00.00 grep worker.js
lejunjie 3171 0.0 0.3 4893888 21656 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3170 0.0 0.3 4893888 21632 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3169 0.0 0.3 4893888 21708 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3168 0.0 0.3 4893888 21664 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
复制代码
经过fork复制的进程都是一个独立的进程,启动多个进程只是为了充分将cpu资源利用起来,而不是为了解决并发问题
cp.spawn('node', ['worker.js']);
sp.exec('node worker.js', () => {})
启动一个子进程来执行可执行文件
建立node子进程只须要指定要执行的javascript文件模块
主线程与工做线程之间经过onmessage和postMessage进行通讯,子进程对象则由send方法实现主进程向子进程发送数据
parent.js
var cp = require('child_process');
var n = cp.fork('./child.js');
n.on('message', function (data) {
console.log('parent data: ' + data.name);
})
n.send({name: 'parent'})
复制代码
child.js
process.on('message', function (data) {
console.log('child: ' + data.name);
})
process.send({name: 'child'})
复制代码
结果
child: parent
parent data: child
复制代码
node中实现ipc通道的是管道技术,具体由libuv提供
父进程在实际建立子进程以前,会建立ipc通道并监听它,而后才真正建立子进程,并经过环境变量告诉子进程这个ipc通道的文件描述符。
双向通讯,在系统内核中完成通讯,不用通过实际的网络层
多个进程监听经过端口会抛出EADDRINUSE异常,这是端口被占用的状况。能够经过代理,在代理进程上作适当的负载均衡,使得每一个子进程能够较为均衡地执行任务。可是代理进程链接到工做进程的过程须要用掉两个文件描述符
句柄是一种能够用来标识资源的应用,他的内部包含了只想对象的文件描述符。好比句柄能够用来表示一个服务器端socket对象,一个客户端socket对象,一个udp套接字,一个管道等。
发送句柄使得主进程接收到socket请求后,将这个socket直接发给工做进程,而不是从新与工做进程之间创建新的socket链接来转发数据。解决文件描述符的浪费问题
parent.js
const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');
var server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('handled by parent');
})
server.listen(1338, () => {
child1.send('server', server);
child2.send('server', server);
})
复制代码
child.js
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', function (socket) {
socket.end('handled by child , pid is' + process.pid);
})
}
})
复制代码
让请求都由子进程处理
parent
const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');
var server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('handled by parent');
})
server.listen(1338, () => {
child1.send('server', server);
child2.send('server', server);
server.close();
})
复制代码
child
var http = require('http');
var server = http.createServer((req, res) => {
res.writeHead(200, {"Content-Type": "text/plain"});
res.end("handled by child, pid is" + process.pid);
})
process.on('message', (m, tcp) => {
if (m === 'server') {
tcp.on('connection', function (socket) {
server.emit('connection', socket);
})
}
})
复制代码
多个子进程能够同时监听相同端口,再没有EADDRINUSE异常发生
总结:
独立启动的进程中,tcp服务器端socket套接字的文件描述符并不相同,致使监听到相同的端口时会抛出异常
多个应用监听相同端口时,文件描述符同一时间只能被某一个进程所用,因此是抢占式的
进程退出时,让全部工做进程退出。子进程退出时从新create
const cp = require('child_process');
var server = require('net').createServer();
var cpus = require('os').cpus();
var workers = {};
function create () {
var worker = cp.fork('./child.js');
worker.on('exit', function () {
console.log('worker: ' + worker.pid + 'exited');
})
worker.send('server', server);
workers[worker.pid] = worker;
console.log('create worker pid: ' + worker.pid);
}
for (var i = 0; i < cpus.length; i++) {
create();
}
process.on('exit', function () {
for (var pid in workers) {
workers[pid].kill();
}
})
复制代码
在极端状况下,全部工做进程都中止接受新的链接,全出在等待退出的状态。但在等进程彻底退出才重启的过程当中,全部新来的请求可能存在没有工做进程为新用户服务的情景,这会丢掉大部分请求
所以可在子进程中监听uncaughtException,而后发送自杀信号
process.on('uncaughtException', function (err) {
process.send({act: 'suicide'});
worker.close(function () {
process.exit(1);
})
})
复制代码
node默认提供的机制是采用操做系统的抢占式策略。
新的策略是轮叫调度。工做方式是由主进程接受链接,将其一次分发给工做进程。
在多个进程之间共享数据
实现同步:子进程向第三方进行定时轮训
主动通知子进程,轮训。
要建立单机node集群,因为有许多细节须要处理,因而引入cluster,解决多核cpu的利用率问题
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工做进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('listening', () => {
console.log('listening')
})
cluster.on('exit', (worker, code, signal) => {
console.log(`工做进程 ${worker.process.pid} 已退出`);
});
} else {
// 工做进程能够共享任何 TCP 链接。
// 在本例子中,共享的是一个 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工做进程 ${process.pid} 已启动`);
}
process.on('exit', () => {
console.log('exit')
})
复制代码
原理:cluster模块就是child_process和net模块的组合应用。在fork子进程时,将socket的文件描述符发送给工做进程。经过so_reuseaddr端口重用,从而实现多个子进程共享端口。
项目的组织能力
代码流程--》stage普通测试环境--》pre-release预发布环境--》product实际生产环境
node file.js以启动应用,会站住一个命令行窗口,窗口退出进程也退出
nohup node app.js &
不挂断进程的方式
bash脚本, 解决进程id不容易查找的问题。重启,中断,启动
动静分离:
让node只处理动态请求,将静态文件引导到专业的静态文件服务器。用nginx或者专业的cdn来处理
cdn缓存,将文件放在离用户尽量近的服务器
对静态请求使用不一样的域名或者多个域名还能消除掉没必要要的cookie传输和浏览器对下载线程数的限制
启用缓存
提高服务速度,避免没必要要的计算
多进程架构
读写分离
对数据库进行主从设计,这样读取数据操做再也不受到写入的影响,下降了性能的影响。
写到磁盘上
数据库写入要经历锁表,日志等操做,若是大量访问会排队,进而内存泄露。
经过监控异常日志文件的变更,将新增的异常按异常类型和数量反应出来。
监控访问日志,体现业务qps值,pv/uv,预知访问高峰
在nginx类的反向代理上监控
经过应用自行产生的访问日志来监控
检查操做系统中运行的应用进程数,对于采用多进程架构的web应用,就须要检查工做进程的数量,若是低于预估值,就应当发出报警
监控磁盘的用量,设置警惕值
健康的内存是有升有降的
cpu分为内核态,用户态,iowait等。
用户态占用高: 服务器上应用大量cpu开销
内核态占用高:服务器花费大量时间进程调度或者系统调用。
描述操做系统当前的繁忙程度
指标太高,在node中可能体如今用子进程模块反复启动新的进程
反应磁盘读写状况
流入流量和流出流量
应用状态监控
dns监控