请求解析

请求方法在报文的第一行,路径在其后

> GET /path?foo=bar HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337 
> Accept: */*

HTTP_Parser在解析时,把报文头的方法取出来设置为req.method,把路径设置为req.url.根据路径进行业务处理的通常有静态文件服务器,选择控制器(/controller/action/a/b/c)

查询字符串:位于路径?之后,可用node提供的querystring处理,也可用url模块,url.parse(req.url,true).query
如果查询字符串的键出现多次,它的值会变成数组

  • 记录状态
  • 服务端先向客户端发送cookie,浏览器保存,之后每次请求都会带上它

构造cookie

1
2
//curl构造cookie字段
curl -v -H "Cookie: foo=bar; baz=val" "http://127.0.0.1:1337/path?foo=bar&foo=baz"

HTTP_Parser会把cookie解析到req.headers.cookie上,格式为key=value;keys=value2

1
2
3
4
5
6
7
8
9
10
11
12
var parseCookie = function(cookie) {
var cookies = {};
if (!cookie) {
return cookies;
}
var list = cookie.split(';');
for (var i = 0; i < list.length; i++) {
var pair = list[i].split('=');
cookies[pair[0].trim()] = pair[1];
}
return cookies;
};

发送cookie通过响应报文:Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;

  • path cookie使用的路径,路径不满足不会发送
  • Expires 过期时间 Max-Age 多久后过期 不设置的话关闭浏览器cookie就丢失
  • HttpOnly 不可过脚本document.cookie修改
  • secure 只对https生效
1
2
3
4
5
6
7
8
9
10
11
12
// cookie序列规范化
var serialize = function(name, val, opt) {
var pairs = [name + '=' + encode(val)];
opt = opt || {};
if (opt.maxAge) pairs.push('Max-Age=' + opt.maxAge);
if (opt.domain) pairs.push('Domain=' + opt.domain);
if (opt.path) pairs.push('Path=' + opt.path);
if (opt.expires) pairs.push('Expires=' + opt.expires.toUTCString());
if (opt.httpOnly) pairs.push('HttpOnly');
if (opt.secure) pairs.push('Secure');
return pairs.join('; ');
};

res.setHeader的第二个参数可以是数组以设置多个字段 res.setHeader(‘Set-Cookie’, [serialize(‘foo’, ‘bar’), serialize(‘baz’, ‘val’)]);并且在头部形参两条Set-Cookie

1
2
Set-Cookie: foo=bar; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
Set-Cookie: baz=val; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;

cookie对性能的影响

  • 客户端每次请求都会带上服务端发给它cookie,只要路径匹配
  1. 减少cookie的大小
  2. 为静态组件使用不同的域名 静态文件一般不关心状态,用不到cookie,不同的域名cookie不匹配就不会发送,同时也可突破浏览器下载线程数限制
  3. 减少DNS查询 上面的修改会增加dns查询,但浏览器有dns缓存

session

因为cookie会被前端修改,不安全并且体积大影响性能。而session的数据只保存在服务端,保障数据安全性,也不用每次传递。session会设置有效期且较短,通常20分钟,即此时间内客户端会与服务端交换,session就删掉了

需要解决将用户和服务器的session对应

基于cookie实现用户和数据映射

  • 将口令(session_id)放在cookie与服务端数据映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//生成session
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;
return session;
};
//检查cookie的口令和服务端数据,过期了就重新生成
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 {
// 如果session过期或口令不对,重新生成
req.session = generate();
}
}
handle(req, res);
}
//将口令注入cookie并响应给客户端
var writeHead = res.writeHead;
res.writeHead = function() {
var cookies = res.getHeader('Set-Cookie');
var session = serialize('Set-Cookie', req.session.id);
cookies = Array.isArray(cookies) ? cookies.concat(session) : [cookies, session]; //array.push()一样
res.setHeader('Set-Cookie', cookies);
return writeHead.apply(this, arguments);
};
  • 通过查询字符串来让客户端和服务端对应(链接带key)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//生成带key的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 {
//如果session过期或者口令不对,重新生成session
var session = generate(); redirect(getURL(req.url, key, session.id));
}
}
}

session与安全

对session用私钥加密
  • 随机产生口令值可能被命中,并用此套用服务端数据

在服务端设定私钥,用私钥对口令加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将值通过私钥签名,由.分割原值和签名
var sign = function (val, secret) {
return val + '.' + crypto.createHmac('sha256', secret).update(val).digest('base64').replace(/\=+$/, '');
};
//在响应时,设置session值到cookie或跳转url
var val = sign(req.sessionID, secret);
res.setHeader('Set-Cookie', cookie.serialize(key, val));
//接受请求时取出口令再签名,经行对比
var unsign = function(val, secret) {
var str = val.slice(0, val.lastIndexOf('.'));
return sign(str, secret) == val ? str : false;
};
xxs漏洞
  • 跨站脚本攻击 Access-Control-Allow-Origin

这个前端脚本会获取页面的url,$(‘#box’).html(location.hash.replace(‘#’, ‘’));攻击者可以把脚本藏在链接后http://a.com/pathname#,再对url压缩。用户打开后脚本会执行,攻击者可以用此获取cookie来伪装用户甚至管理员

缓存

  • 让浏览器缓存静态资源
  • 通常只适应于get请求

Smaller icon

一开始本地没有文件,浏览器直接请求服务端并将文件缓存,二次请求时若不能确定是否可用,会发起条件请求,在get中附带If-Modified-Since字段

使用时间戳

1
2
//get报文附带If-Modified-Since/Last-Modified字段
If-Modified-Since: Sun, 03 Feb 2013 06:01:12 GMT

服务端如果没有新版本就只需要响应304,浏览器就会使用本地文件;若更新就发送新文件,让浏览器更新缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(Entity Tag)

  • 服务端确定生成规则生成
  • 请求字段为If-None-Match/ETag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//生成文件hash
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);
}
});
};

Expires,Cache-Control

  • 让浏览器明确缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Expires 设置一个过期时间,可能出现时间不同步,文件提前更新等问题
var handle = function(req, res) {
fs.readFile(filename, function(err, file) {
var expires = new Date();
expires.setTime(expires.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
res.setHeader("Expires", expires.toUTCString());
res.writeHead(200, "Ok");
res.end(file);
});
};
//Cache-Control 可设置更多选项,使用max-age倒计时
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);
});
};

http1.0不支持max-age,所以要同时设置Expires,Cache-Control,在浏览器中max-age优先级高于Expires

清除缓存

  • 浏览器是根据url进行缓存的,内容更新时可以让浏览器发起新的url缓存

每次发布在路径中跟随文件内容的hash,http://url.com/?hash=afadfadwe.这样能避免无意义的更新

Basic认证

  • 通过用户名和密码实现的身份认证

当页面需要Basic认证时,会检查请求头部的Authorization字段,它的值由认证方式和加密值构成

$ curl -v "http://user:pass@www.baidu.com/"
> GET / HTTP/1.1
> Authorization: Basic dXNlcjpwYXNz
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5 
> Host: www.baidu.com
> Accept: */*

它会将用户名和密码组合,再Base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var encode = function(username, password) {
return new Buffer(username + ':' + password).toString('base64');
};
//每次请求都要带上Authorization,对于没有带认证的请求会返回401
function(req, res) {
var auth = req.headers['authorization'] || '';
var parts = auth.split(' ');
var method = parts[0] || ''; // Basic
var encoded = parts[1] || ''; // dXNlcjpwYXNz,密码字段
var decoded = new Buffer(encoded, 'base64').toString('utf-8').split(":");
var user = decoded[0]; // user
var pass = decoded[1]; // pass
if (!checkUser(user, pass)) {
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
res.writeHead(401);
res.end();
} else {
handle(req, res);
}
}

缺点: 对于密码字段没有加密,只能在https下才能用,但几乎所有浏览器都支持。RFC 2069对它做了改进

构建websocket服务

websocket的优势:

  • 客户端与服务器只需要一个tcp连接
  • 服务器可以推送到客户端
  • 轻量化的协议头,提高传输效率

node使用websocket的优势:

  • WebSocket客户端基于事件的编程模式和node的自定义事件类似
  • websocket需要客户端与服务器之间的长连接,node事件驱动的方式擅长与量大的客户端保持高并发连接

WebSocket握手

客户端发起升级协议请求:

1
2
3
4
5
6
7
GET / chat HTTP / 1.1
Host: server.example.com
Upgrade: websocket //升级协议为websocket
Connection: Upgrade
Sec - WebSocket - Key: dGhlIHNhbXBsZSBub25jZQ ==
Sec - WebSocket - Protocol: chat, superchat //子协议
Sec - WebSocket - Version: 13 //版本号

Sec-WebSocket-Key用于安全校验,值是随机生成的Base64编码的字符串。服务端将其与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接,然后再用sha1计算再Base64编码

1
2
3
4
5
6
7
8
9
var crypto = require('crypto');
var val = crypto.createHash('sha1').update(key).digest('base64');
//服务端响应b报文
HTTP / 1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Accept: s3pPLMBiTxaQ9kYGzzhZRbK + xOo =
Sec - WebSocket - Protocol: chat

客户端校验Sec-WebSocket-Accept,正确的话就开始数据传输

WebSocket数据传输

在握手后就开始websocket数据帧协议, 握手完成客户端onopen()被触发

1
2
3
socket.onopen = function() {
// TODO: opened()
};

服务端没有onopen()方法,想完成tcp套接字事件到websocket事件的封装,需要在收发数据时处理,Websocket的数据帧是在底层data事件上封装的

1
2
3
4
5
6
7
8
9
10
//接收
WebSocket.prototype.setSocket = function(socket) {
this.socket = socket;
this.socket.on('data', this.receiver);
};
//发送
WebSocket.prototype.send = function(data) {
this._send(data);
};

当一端调用send()发送时,另一端会触发onmessage,协议可能将数据封装为多帧发送。客户端需要对发送的数据帧做掩码处理,服务端收到无掩码帧会断开连接,而服务端发送时不需要

websocket数据帧定义
Smaller icon

  • fin 如果这数据帧是最后一帧时为1(如果数据就一帧,它也是1),其余为0
    • rsv1、rsv2、rsv3:1位长 用于标识拓展,当有拓展时为1
    • opcode: 4位(0~15) 0:附加数据帧 ,1:文本数据帧 ,2:二进制数据帧,8:发送一个连接关闭帧,9:ping数据帧 ,10:pong数据帧 ping,pong用于心跳检测,一端发ping、一端发pong
    • masked 是否进行掩码处理 客户端发送时是1 服务端是0
    • payload 标识数据长度
    • masking key 当masked为1时存在 长度32位 用于解密
    • payload data 目标数据 位数为8的倍数

网络服务和安全

  • ssl(Secure Sockets Layer,安全套接层),应用在传输层
  • TLS(Transport Layer Security,安全传输层协议),由IETF标准化

node提供crypto,tls,https。crypto用于加解密,tls与net功能类似,区别是它建立在TLS/SSL加密的tcp.https和http接口也一致,也是区别在建立于安全的连接

TLS/SSL

  • 非对称加密,公钥用于加密传输数据,私钥解密

    Smaller icon

    node的tls/ssl是用openssl实现的,公、私钥生成参照:

1
2
// 生成服务器端私 openssl genrsa -out server.key 1024 //生成客户端私 openssl genrsa -out client.key 1024
// 利用上面的1024位长的RSA私钥生成公钥 openssl rsa -in server.key -pubout -out server.pem openssl rsa -in client.key -pubout -out client.pem
数字证书
  • 由CA颁发,并提供验证
  • 防止中间人攻击

中间人攻击:在服务端和客户端交换密钥时,伪装成其中一方发送公钥,如对客户端就伪装成服务端。所以需要对公钥认证,确认来自目标服务器

服务端通过私钥生成CSR(Certificate Signing Request,证书签名请求),ca通过它颁发属于该服务器的签名证书

自签名证书流程:

1
2
3
\\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,向ca申请签名,获取证书 $ openssl req -new -key server.key -out server.csr $ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt

客户端发起安全连接会获取服务端证书,然后用ca的证书验证服务器证书,包括真伪、服务器名称、ip等。对于知名ca,它的证书一般预装在浏览器,自签的ca需要客户端安装才能验证

创建tcl服务

  • 通过node的tls创建安全的tcp服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//服务端
var tls = require('tls');
var fs = require('fs');
var options = {
key: fs.readFileSync('./keys/server.key'),
cert: fs.readFileSync('./keys/server.crt'),
requestCert: true,
ca: [fs.readFileSync('./keys/ca.crt')]
};
var server = tls.createServer(options, function(stream) {
console.log('server connected', stream.authorized ? 'authorized' : 'unauthorized');
stream.write("welcome!\n");
stream.setEncoding('utf8');
stream.pipe(stream);
});
server.listen(8000, function() {
console.log('server bound');
});
//测试: $ openssl s_client -connect 127.0.0.1:8000
//客户端
$ openssl genrsa - out client.key 1024
$ openssl req - new - key client.key - out client.csr
$ openssl x509 - req - CA ca.crt - CAkey ca.key - CAcreateserial - in client.csr - out client.crt
var fs = require('fs');
var tls = require('tls');
var options = {
key: fs.readFileSync('./keys/client.key'),
cert: fs.readFileSync('./keys/client.crt'),
ca: [fs.readFileSync('./keys/ca.crt')]
};
var stream = tls.connect(8000, options, function() {
console.log('client connected', stream.authorized ? 'authorized' : 'unauthorized');
process.stdin.pipe(stream);
});
stream.setEncoding('utf8');
stream.on('data', function(data) {
console.log(data);
});
stream.on('end', function() {
server.close();
});
//和tcp相比只是多了证书配置

https服务

  • 使用node的https,比http多了一个配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 world\n");
}).listen(8000);
//验证 $ curl https://localhost:8000/ -k, 忽略证书验证 -carcert ca证书地址
//客户端
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);//https代理另设
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);
});

TCP服务

  • 传输控制协议,网络传输层
  • OSI模型: 物理层 数据链路层 网络层 传输层 会话层 表示层 应用层

传输之前先要三次握手形成会话,一个套接字socket只用于一个服务

创建TCP服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var net = require('net');
//使用net.createServer(listener)创建,**listener是连接事件connection的侦听器**
var server = net.createServer(function(socket) {
socket.on('data', function(data) {
socket.write("hello");
});
socket.on('end', function() {
console.log('断开');
});
socket.write("hello111:\n");
});
server.listen(8124, function() {
console.log('server bound');
});
//或者
var server = net.createServer();
server.on('connection', function(socket) { //新的连接
});
server.listen(8124);
//可以使用$ telnet 127.0.0.1 8124,或者使用net模块构建客户端
var net = require('net');
var client = net.connect({ //
port: 812
}, function() { //'connect' listener
console.log('client connected');
client.write('world!\r\n');
});
client.on('data', function(data) {
console.log(data.toString());
client.end();
});
client.on('end', function() {
console.log('client disconnected');
});
tcp.listen(1337, '127.0.0.1');

也可以对Domain Socket监听 server.listen(‘/tmp/echo.sock’);
使用 $ nc -U /tmp/echo.sock 测试

服务器事件

使用net.createServer()创建的服务器是个EventEmitter实例

  1. listening 在调用server.lisren()绑定端口或者Domain Socket后触发server.listen(port,listeningListener)
  2. connection 每个客户端套接字连接到服务端时触发 net.createServer(),最后一个参数传递
  3. close 当服务器关闭时触发 在调用server.close()后,服务器会停止接受新的套接字,但当前的连接不会断,直到所有连接都断后触发
  4. error 服务器发生异常时触发,如果不帧听error事件,会抛出异常

连接事件

服务器可以同时与多个客户端保持连接,每个连接都是可读写的stream对象,用于服务器和客户端通信

  1. data 当一端调用write()发送数据时,另一端触发data事件
  2. end 任意一端发送了FIN数据时触发
  3. connect 用于客户端,当套接字与服务端连接成功时触发
  4. error 发生异常
  5. close 套接字完全关闭时触发
  6. timeout 连接在一定时间内部活跃,触发
1
2
3
4
5
6
7
8
//使用pipe管道 管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。
var net = require('net');
var server = net.createServer(function(socket) {
socket.write('Echo server\r\n');
socket.pipe(socket);//
});
server.listen(1337, '127.0.0.1');

Nagle算法:将tcp中的小数据包缓存合并到一定数量或时间后发出,避免浪费网络资源,但数据可能被延迟。tcp默认启动Nagle算法,可以调用socket.setNoDelay(true)关闭,关闭后一端调用write(),另一端可能将多个小数据包合并后触发一次data

UDP服务

  • 用户数据包协议,网络传输层
  • 一个套接字可以和多个UDP服务通信,无须连接,资源消耗低,处理快速灵活
  • 可能丢包,应用在低丢包不影响的的场景,视频、DNS

创建UDP套接字

UDP套接字可以做客户端也可做服务端

  • 作为服务端 调用dgram.bind(port,[address])方法对网卡和端口绑定
1
2
3
4
5
6
7
8
9
10
11
var dgram = require('dgram');
var server = dgram.createSocket('udp4');
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);//绑定完成后触发listening事件
  • 作为客户端:使用dgram.send(buf, offset, length, port, address, [callback])发送消息到网络;参数对应含义,buffer,buffer偏移,buffer长度,目标端口,目标地址,发送完成后的回调。它可以随意发送数据到网络,tcp需要重新通过套接字构建新连接
1
2
3
4
5
6
var dgram = require('dgram');
var message = new Buffer("Node.js");
var client = dgram.createSocket("udp4");
client.send(message, 0, message.length, 41234, "localhost", function(err, bytes) {
client.close();
});

UPD套接字事件

  • UDP是个EventEmitter实例使用更简单,TCP是个Stream实例
  1. message UDP套接字帧听网卡端口后,接受数据时触发,触发携带的数据为Buffer对象和远程地址
  2. listening UDP开始帧听时触发
  3. close 调用close()时触发,并不再触发message事件
  4. error 异常时触发,不帧听则直接抛出,进程退出

构建HTTP服务

  • 超文本传输协议 HyperText Transfer Protocol
  • 构建在TCP之上 属于应用层协议 B/S模式,目前最知名的标准RFC 2616

通常的http通信的信息分三部分,TCP的3次握手,客户端向服务器发送请求报文,服务器完成处理后向客户端发送响应内容。
浏览器其实是http的代理,将用户的行为转化为http请求发送给服务端,服务端处理请求然后发送响应报文给代理,代理解析报文再展示给用户。http服务只做处理http请求和发送http响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//创建http服务
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
//使用net模块创建
var tcp = require('net').createServer();
var http = require('http');
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('handled by child, pid is ' + process.pid + '\n');
});
tcp.on('connection', function(socket) {
server.emit('connection', socket);
});

http模块

  • 继承自TCP服务(net模块),通信方法都是使用net的,包括创建服务、关闭服务等
  • 能够与多个客户端保持连接,因为采用事件驱动,不用给每个连接创建额外的线程、进程,内存占用低,能高并发
  • tcp以connection为单位服务,http以request为单位服务

http模块将tcp连接的读操作封装为ServerRequest对象,报文头部使用http_parser解析然后放在req.headers上传递给业务逻辑。报文体部分为只读流对象,需要在数据流结束后转字符串

1
2
3
4
5
6
7
8
9
10
11
function(req, res) {
// console.log(req.headers);
var buffers = [];
req.on('data', function(trunk) {
buffers.push(trunk);
}).on('end', function() {
var buffer = Buffer.concat(buffers); // TODO
res.end('Hello world');
});
}
//express中使用bodyParse,会自动解析,但要求请求头添加Content-Type=application/json

封装对底层连接的写操作为ServerResponse对象
影响响应报文头api为res.setHeader()res.writeHead(),可以调用res.setHeader()进行多次设置,只要调用writeHead后,报头才会写入连接

1
res.writeHead(200, {'Content-Type': 'text/plain'});

设置报文体API为write(),end();end()会先调用write()发送数据,再发信号告知响应结束。

一旦开始了数据发送,writeHead()和setHeader()将不再生效
结束时要调用res.end(),否则客户端将一直处于等待状态,无论异常与否

http服务端事件

http服务器是个EventEmitter实例

  • connection 客户端与服务器建立底层的tcp连接时触发。连接开启keep-alive,可以在多次请求响应之间使用
  • request 当请求数据发送到服务端,在解析出http请求头后触发该事件
  • close 调用server.close()方法停止接受新连接,当已有的连接也都断开后触发,可以给server.close()传个回调来快速注册该事件
  • checkContinue 某些客户端在发送大数据时,会先发一个头部带Expect: 100-continue的请求,服务器接收后触发checkContinue。如果不监听,就自动回复100 Continue。如果不接收就响应400 Bad Request。注意,在客户端收到100 Continue后重新发起请求才会触发request
  • connect 客户端发起connect请求时触发,通常在http代理时才会发起connect,如果不监听,发起该请求的连接会关闭
  • upgrade 客户端要升级连接协议时会在请求头部带上upgrade,服务端接收后触发,如果不监听,发起该请求的连接会关闭
  • clientError 连接的客户端触发error事件,会传递到服务器端并触发

http客户端

  • http.request(options, connect) 构建http客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var options = {
hostname: '127.0.0.1',
port: 1334,
path: '/',
method: 'GET',
localAddress //建立连接的本地网卡
socketPath //Domain套接字路径
headers //请求头对象
auth //Basic认证 将被计算成请求头的Authorization部分
};
// 用request 包请求 不用自己来处理on end
var req = http.request(options, function(res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function(chunk) {
console.log(chunk);
});
});
req.end();

报文体的内容通过请求对象的write()写入,end()告知报文结束
客户端clientRequest(req)解析完响应头就触发response,并传递ClientResponse以供操作,之后报文体以只读流提供)

http代理
  • 为了重用tcp连接(在keeplive时,一个tcp连接可以多次用于请求)
  • http里包含一个客户端代理对象http.globalAgent,它管理每个服务端(host+port)创建的连接并创建一个连接池,默认每个服务器端5个连接

在通过ClientRequest调用http请求时会走代理。可以在options中传递agentX修改连接限制

1
2
3
4
5
6
7
8
9
10
var agent = new http.Agent({
maxSockets: 10
});
var options = {
hostname: '127.0.0.1',
port: 1334,
path: '/',
method: 'GET',
agent: agent //设置为false就不受连接池限制
};
http客户端事件
  • response 客户端得到响应后触发
  • socket 当连接池中建立的连接分配给当前的对象时触发
  • upgrade 客户端向服务端发起upgrade 服务端响应101 Switching Protocols时触发
  • continue 客户端想发大数据,头部带Expect: 100-continue,服务端同意并响应100 continue时触发

Buffer结构

  • 类似Array,为16进制的两位数,即占一个字节
  • js与c++结合的模块,内存由c++申请,js分配。因为v8垃圾回收影响性能
  • node启动时就加载,放在全局对象global
1
2
var buf = new Buffer(100);
console.log(buf.length); // => 100

如给buffer赋值数字,则范围在0-255,否则负数就加256,过大就减256

内存使用slab分配机制,动态内存管理,包含三种状态(full,partial,empty)。Node以8KB为界限区分Buffer是大对象还是小对象(Buffer.poolSize=8*1024),即8kb为slab单元大小,js以它为单元分配内存

分配小Buffer对象

  • 小于8kb

使用局部变量pool,让处于分配状态的slab单元指向它

1
2
3
4
5
6
var pool;
function allocPool() {
pool = new SlowBuffer(Buffer.poolSize);
pool.used = 0;
}

新建小buffer时,如果还没pool,就创建一个slab指向它,当前的buffer对象的parent指向slab

1
2
3
4
this.parent = pool; //当前的buffer对象的parent指向slab
this.offset = pool.used;//slab开始使用的位置
pool.used += this.length;//slab已使用量
if (pool.used & 7) pool.used = (pool.used + 8) & ~7;

此时slab状态为partial,在创建buffer时会判断次slab是否够用,如果不够,就构建新的slab,原来的slab剩余的空间浪费,如果不释放就占据8kb

分配大Buffer

直接分配一个SlowBuffer对象作为slab单元,并且独占

1
2
// Big buffer,just alloc one SlowBuffer由c++定义,勿直接操作它
this.parent=new SlowBuffer(this.length);this.offset=0;

Buffer对象是js层面的,能被v8标记回收,但其内部parent指向SlowBuffer,由c++提供的Buffer,所以内存由c++提供,js只是使用它,对于小buffer的频繁操作,使用slab机制来先申请后分配,减少内存申请的系统调用,对于大buffe就直接用C++提供内存

Buffer转换

  • 支持的字符串编码 ASCII,UTF-8,UTF-16LE/UCS-2,Base64,Binary,Hex

字符串与Buffer的转换

字符串转buffer,使用构造函数new Buffer(str,[encoding]);默认UTF-8

一个Buffer对象可以存多种编码类型的字符串转码的值

1
buf.write(string, [offset], [length], [encoding])

buffer转字符串

1
buf.toString([encoding], [start], [end]) //encoding默认UTF-8,配合startend实现局部转换

Buffer不支持的编码类型

  • 使用Buffer.isEncoding(encoding)判断

iconv 通过c++调用libiconv
iconv-lite使用纯js实现,但基于v8高性能,少了c++到js的转换,所以比C++实现好

1
2
var iconv = require("iconv-lite");var str = iconv.decode(buf,"win1251");
var buf = iconv.encode('Sample input string', 'win1251');

对无法转换的内容会降级处理,输出部分或者?

Buffer拼接

1
2
3
4
5
6
7
8
9
var fs = require('fs');
var rs = fs.createReadStream('test.md');
var data = '';
rs.on("data", function(chunk) {
data += chunk; //等价于data = data.toString() + chunk.toString();此处对宽字节的中文可能造成乱码,即字节没读全就转码
});
rs.on("end", function() {
console.log(data);
});

解决乱码问题

  • 在调用setEncoding()时,可读流对象在内部设置一个decoder对象

req.setEncoding(‘utf8’);

1
2
3
4
5
6
7
8
var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf8');
var buf1 = new Buffer([0xE5, 0xBA, 0x8A, 0xE5, 0x89, 0x8D, 0xE6, 0x98, 0x8E, 0xE6, 0x9C]);
console.log(decoder.write(buf1));
// => 床前明
var buf2 = new Buffer([0x88, 0xE5, 0x85, 0x89, 0xEF, 0xBC, 0x8C, 0xE7, 0x96, 0x91, 0xE6]);
console.log(decoder.write(buf2));
// =>月光,凝

StringDecoder在得到编码后,知道宽字节在utf-8下占3个字节,所以在处理末尾不全的字节时,会保留到第二次write().目前只能处理UTF-8、Base64和UCS-2/UTF-16LE

正确拼接Buffer

1
2
3
4
5
6
7
8
9
10
11
var chunks = [];
var size = 0;
res.on('data', function(chunk) {
chunks.push(chunk);
size += chunk.length;
});
res.on('end', function() {
var buf = Buffer.concat(chunks, size);
var str = iconv.decode(buf, 'utf8');
console.log(str);
});

用数组来储存接收的所有Buffer片段并记录总长度,然后调用Buffer.concat()–>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Buffer.concat = function(list, length) {
if (!Array.isArray(list)) {
throw new Error('Usage: Buffer.concat(list, [length])');
}
if (list.length === 0) {
return new Buffer(0);
} else if (list.length === 1) {
return list[0];
}
if (typeof length !== 'number') {
length = 0;
for (var i = 0; i < list.length; i++) {
var buf = list[i];
length += buf.length;
}
}
var buffer = new Buffer(length);
var pos = 0;
for (var i = 0; i < list.length; i++) {
var buf = list[i];
buf.copy(buffer, pos);
pos += buf.length;
}
return buffer;
};

Buffer与性能

Buffer广泛应用于文件I/O和网络I/O,尤其在网络传输,使用Buffer比直接使用字符串要性能要高很多。在web开发中对于静态内容可以预先转成buffer,在不需要改变内容时,只读取buffer,不做转换

文件读取

fs.createReadStream()先在内存中准备一段buffer,然后在fs.read()读取时逐步将磁盘中的字节复制到buffer,读完一次就用slice()从buffer取出部分作为小buffer通过data事件传给调用方。Buffer用完会重新分配

1
2
3
4
5
6
7
8
9
fs.createReadStream(path, opts)
//参数
{
flags: 'r',
encoding: null,
fd: null,
mode: 0666,
highWaterMark: 64 * 1024 // 每次读取的长度
}

重新分配

1
2
3
4
5
6
7
8
9
10
11
12
var pool;//常驻内存
function allocNewPool(poolSize) {
pool = new Buffer(poolSize);
pool.used = 0;
}
//当pool剩余数量小于128(kMinPoolSpace)字节时,会重新分配
if (!pool || pool.length - pool.used < kMinPoolSpace) {
// discard the old pool
pool = null;
allocNewPool(this._readableState.highWaterMark);
}

highWaterMark的大小对性能的影响有:buffer内存的分配和使用、系统调用次数;

  • 文件流读取基于buffer,buffer基于slowbuffer,文件小于8kb可能造成slab浪费
  • fs.createReadStream()内部使用fs.read(),会引起系统对磁盘的调用,highWaterMark的大小决定调用次数和data事件次数

Smaller icon

垃圾回收机制

  • nodejs在执行JavaScript时,内存受到v8限制,64位约为1.4g,32位0.7g
  • 所有js对象是通过堆分配,查看process.memoryUsage()
  • 限制内存原因:垃圾回收时,js线程会暂停执行(避免JS应用逻辑与垃圾回收器看到的不一样),大量的堆内存回收严重影响性能
  • v8内存整体包含新生代和老生代

    // 调整内存限制的大小
    node --max-old-space-size=1700 test.js // 单位为MB 
    node --max-new-space-size=1024 test.js // 单位为KB
    
    在V8初始化时生效,一旦生效不能动态变化  
    

新生代

  • 由两个reserved_semispacesize(32位16mb,62位32mb)构成
  • 通过Scavenge算法进行回收,具体实现采用Cheney算法

Cheney算法采用复制方式实现垃圾回收,将堆内存分成2块,一个使用(From),一个空闲(To).分配对象在From空间。开始垃圾回收时,检查From里的存活对象,并将它们复制到To,非存活对象占用的空间会被释放,然后From、to角色对换。将存活对象在两个空间之间复制

优点是时间短、缺点是只能使用一半堆内存。新生代对象生命周期短,适合此算法

当对象经过多次复制依然存活,就会晋升到老生代。对像晋升的条件,是否经历过Scavenge回收,To空间内存占用比超过限制

老生代

  • 在64未系统下为1400 MB,在32为700 MB
  • 使用Mark-Sweep和Mark-Compact进行垃圾回收

Mark-Sweep 标记清除,先遍历堆的对象,标记活的对象,之后清除没标记的对象。死对象在老生代只占少部分,所以高效

Mark-Compact 整理内存,将Mark-Sweep清理后散开的对象移动到一段。

v8主要使用Mark-Sweep,在空间不足以对新晋升对象分配时才用Mark-Compact

增量标记(incremental marking)

  • 降低老生代的全堆垃圾回收带来的时间停顿
  • 从标记阶段入手,拆分为许多小步进,与应用逻辑交替运行
  • 垃圾回收最大停顿时间降为原来的1/6

垃圾回收是影响性能的因素之一,要尽量减少垃圾回收,尤其全堆垃圾回收

查看垃圾回收日志

  • 在启动时添加–trace_gc

启动时使用–prof,可以得到v8性能分析数据,包含垃圾回收占用的时间,需要使用工具读取,在Node源码的deps/v8/tools,linux-tick-processor

高效使用内存

作用域

  • js中能形成作用域的有函数调用、with和全局作用域

例如,在函数调用时,会创建对应的作用域,在执行结束后销毁,并且在该作用域申明的局部变量也会被销毁

  1. 标识符查找(即变量名) 先查找当前作用域,再向上级作用域,一直到全局作用域
  2. 变量主动释放 全局变量要直到进程退出才释放,导致引用对象常驻老生代,可以用delete删除或者赋undefined、null(delete删除对象的属性可能干扰v8,所以赋值更好)

闭包

  • 外部作用域访问内部作用域的方法,得益于高阶函数特性
1
2
3
4
5
6
7
8
9
10
var foo = function() {
var bar = function() {
var local = "局部变量";
return function() {
return local;
};
};
var baz = bar();
console.log(baz());
};

bar()返回一个匿名函数,一旦 有变量引用它,它的作用域将不会释放,直到没有引用

把闭包赋值给一个不可控的对象时,会导致内存泄漏。使用完,将变量赋其他值或置空

查看内存使用情况

  • 查看进程内存占用 process.memoryUsage(),其中rss为进程的常驻内存(node所占的内存),heapTotal、heapUsed为堆内存使用情况
  • os.totalmem(),os.freemem() 查看系统内存

Node使用的内存不是都通过v8分配,还有堆外内存,用于处理网络流、I/O流

内存泄漏

造成的原因:缓存、队列消费不及时、作用域未释放

缓存

  • 限制内存当缓存,要限制好大小,做好释放
  • 进程之间不能共享内存,所以用内存做缓存也是

为了加速模块引入,模块会在编译后缓存,由于通过exports导出(闭包),作用域不会释放,常驻老生代。要注意内存泄漏

1
2
3
4
5
var leakArray = [];
exports.leak = function() {
leakArray.push("leak" + Math.random());
};
//局部变量leakArray不停增加内存占用,且不会释放,如果必须如此设计,要提供释放接口

推荐使用进程外缓存,RedisMemcached

队列状态

  • 在生产者和消费者中间
  • 监控队列的长度,超过长度就拒绝
  • 任意的异步调用应该包含超时机制

内存泄漏排查

node-heapdump
  1. 安装 npm install heapdump
  2. 在开头引入 var heapdump = require(‘heapdump’);
  3. 发送命令kill -USR2 ,heapdump会抓拍一份堆内存快照,文件为heapdump-..heapsnapshot格式,是json文件
node-memwatch
1
2
3
4
5
6
7
8
var memwatch = require('memwatch');
memwatch.on('leak', function(info) {
console.log('leak:');
console.log(info);
});
memwatch.on('stats', function(stats) {
console.log('stats:') console.log(stats);
});

在进程使用node-memwatch后,每次全堆垃圾回收,会触发stats事件,该事件会传递内存的统计信息

1
2
3
4
5
6
7
8
9
10
stats: {
num_full_gc: 4, // 第几次全堆垃圾回收
num_inc_gc: 23, // 第几次增量垃圾回收
heap_compactions: 4, // 第几次对老生代整理
usage_trend: 0, // 使用趋势
estimated_base: 7152944, // 预估基数
current_base: 7152944, // 当前基数
min: 6720776, // 最小
max: 7152944 //最大
}

如果经过连续的5次垃圾回收后,内存仍没有被释放,意味有内存泄漏,node-memwatch会触发leak事件

1
2
3
4
5
6
7
leak: 8 {
start: Mon Oct 07 2013 13: 46: 27 GMT + 0800(CST),
end: Mon Oct 07 2013 13: 54: 40 GMT + 0800(CST),
growth: 6222576,
reason: 'heap growth over 5 consecutive GCs (8m 13s) - 43.33 mb/hr'
}
//显示内存增长了多了

使用node-memwatch的抓取快照和比较快照,能将内存泄漏定位到v8的堆上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var memwatch = require('memwatch');
var leakArray = [];
var leak = function() {
leakArray.push("leak" + Math.random());
};
// Take first snapshot
var hd = new memwatch.HeapDiff();
for (var i = 0; i < 10000; i++) {
leak();
}
// Take the second snapshot and compute the diff
var diff = hd.end();
console.log(JSON.stringify(diff, null, 2));
{
"before": {
"nodes": 11719,
"time": "2013-10-07T06:32:07.000Z",
"size_bytes": 1493304,
"size": "1.42 mb"
},
"after": {
"nodes": 31618,
"time": "2013-10-07T06:32:07.000Z",
"size_bytes": 2684864,
"size": "2.56 mb"
},
"change": {
"size_bytes": 1191560,
"size": "1.14 mb",
"freed_nodes": 129, //释放的节点数
"allocated_nodes": 20028,//分配的节点数
"details": [{
"what": "Array",
"size_bytes": 323720,
"size": "316.13 kb",
"+": 15,
"-": 65
}, {
"-": 28
}, {
"what": "String",
"size_bytes": 879424,
"what": "Code",
"size_bytes": -10944,
"size": "-10.69 kb",
"+": 8,
"size": "858.81 kb",
"+": 20001, //大量的string未被回收
"-": 1
}]
}
}

大内存应用

  • 使用stream模块处理大文件,fs的createReadStream(),createWriteStream()
1
2
3
4
5
6
7
8
9
10
11
12
13
var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.on('data', function(chunk) {
writer.write(chunk);
});
reader.on('end', function() {
writer.end();
});
//管道方法pipe(),封装了data事件和写入
var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.pipe(writer);

在不需要进行字符串操作时,可以不借助v8,使用Buffer操作,这样不会受到v8的内存限制

Smaller icon

虚拟主机和请求分发

  • 多个域名对应同一ip时,配置多个server虚拟主机,对应多个域名,server_name对应用户请求的域名

监听端口

  • 默认: listen 80;
  • 配置块: server

可以加ip、端口、主机名

1
2
3
4
listen 127.0.0.1:8000;
listen 127.0.0.1;//不加端口号,默认监听80
listen *:8000;
listen 127.0.0.1 default_server backlog=1024;
parameters 定义
default/default_server 默认server块,不设置就默认第一个server,当请求没能匹配到主机域名时,就选择默认
backlog=num tcp中的backlog队列大小。默认-1,不设置。存放等待建立tcp三次握手连接
rcvbuf=size 监听句柄的SO_RCVBUF
sndbuf=size 监听句柄的SO_SNDBUF
accpet_filter 设置accept过滤器,只对FreeBSD
deferred 只有用户发送请求数据,内核在网卡中收到请求数据包,内核才会唤醒worker进程处理,适用于大并发情况,一般不建议使用
bind 绑定当前端口/地址对,如127.0.0.1:8000.只有同时对一个端口监听多个地址时有效
ssl 监听的端口连接必须基于ssl

主机域名

  • 语法: server——name name[…];可以跟多个主机名
  • 默认: server_name “”; 匹配没有Host的http请求
  • 配置块: server

当header的host和多个server匹配,则按匹配优先级

  1. 字符串完全匹配 www.test.com
  2. 通配符在前面的 *.test.com
  3. 通配符在后面的 www.test. *
  4. 使用正则的

都不匹配就选默认的

location

  • 语法: location [=|~|~*|^~|@]
  • 默认: server_name “”; 匹配没有Host的http请求
  • 配置块: server

负载均衡

  • 此处指的是选择一种策略,尽量把请求均匀的分布到每一个上游服务器

upstream

  • 配置块 http
  • 语法 upstream name{}
  • 定义一个上游服务器的集群,便于反向代理的proxy_pass使用
1
2
3
4
5
6
7
8
9
upstream backend {
server 127.0.0.1:5566 weight=1 max_fails=2 fail_timeout=30s;
}
server {
location / {
proxy_pass http://backend; #backend指上游服务器
}
}
server
  • 配置块 upstream
  • 语法 server names[parameters]
  • 指定一台上游服务器的名字 可以是域名,ip,UNIX句柄
parameters 定义
weight=number 上游服务器转发的权重 默认1
max_fails=number 在fail_timeout时间内,如果向上游服务器转发失败次数超过number,就认为该服务器在此时间段里不可用,默认1 设为0就不检查失败次数
fail_timeout=time 用于优化反向代理功能,与连接、读取、响应超时无关,默认10秒
down 永久下线服务器,只在使用ip_hash有效
backup 在使用ip_hash时无效 表示上游服务器只是备份服务器,只有在所有非备份上游服务器失效后,才会指向备份
ip_hash
  • 配置块 upstream
  • 语法 ip_hash;
  • 将单个用户的请求固定到某个上游服务器

先根据客户端的ip地址计算一个key,将可以按照upstream集群的上游服务器数量进行取模,再根据取模的结果把请求地址转发到相应的上游服务器,不能与weight同时使用,在标识上游服务器不可用时,要用down不能直接删除,确保转发策略一贯性

1
2
3
4
5
upstream backend {
ip_hash;
server 127.0.0.1:5566 weight=1 max_fails=2 fail_timeout=30s;
server backend down;
}

记录日志时支持的变量

变量名 定义
$upstream_addr 处理请求的上游服务器地址
$upstream_cache_status 表示是否命中缓存,取值范围:MISS、EXPIRED、UPDATING、SATLE、HIT
$upstream_status 上游服务器返回的响应中HTTP响应码
$upstream_response_time 上游服务器响应时间,精度毫秒
$upstreamhttp$HEADER http头部,如upstream_http_host
1
2
3
4
5
6
log_format timing '$remote_addr - $remote_user [$time_local] $request '
'upstream_response_time $upstream_response_time '
'msec $msec request_time $request_time';
log_format up_head '$remote_addr - $remote_user [$time_local] $request '
'upstream_http_content_type $upstream_http_content_type'

反向代理配置

proxy_pass

  • 语法:proxy_pass URL;
  • 配置块 location、if

将当前请求反向代理到URL参数指定的服务器上,URL可以是主机名或ip地址

1
2
3
4
5
6
7
8
9
10
11
12
13
proxy_pass http://localhost:3000/uri/;
可以加上负载均衡 使用upstream
upstream backend {...}
server {location / {
proxy_pass http://backend; #backend指上游服务器
}}
可以把httpz转换成https
proxy_pass https://192.168.0.1/;
若需要转发host头部
proxy_set_header Host $host;

proxy_method

  • 语法:proxy_method method;
  • 配置块 http、server、location

转发时的协议方法名
proxy_method POST; 客户端的GET请求也会被转发成POST

proxy_hide_header

  • 语法:proxy_hide_header the_header;
  • 配置块 http、server、location

Nginx会将上游服务器的响应转发给客户端,但默认不会转发以下HTTP头部字段:Date、Server、X-Pad和X-Accel-*.使用proxy_hide_header可以指定哪些不能转发

proxy_hide_header Cache-Control;

proxy_pass_header

  • 语法:proxy_pass_header the_header;
  • 配置块 http、server、location

将原来禁止转发的header设为转发

proxy_pass_request_body

  • 语法:proxy_pass_request_body on|off;
  • 默认 on;
  • 配置块 http、server、location
    是否向上游转发http body

proxy_pass_request_headers

  • 语法:proxy_pass_request_headers on|off;
  • 默认 on;
  • 配置块 http、server、location
    是否向上游转发http header

prxoy_redirect

  • 语法:prxoy_redirect [default|off|redirect rep|replacement]];
  • 默认 default;
  • 配置块 http、server、location

当上游返回响应是重定向或刷新请求(301,302),proxy_redirect 可以重设HTTP头部的location或refresh字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
proxy_redirect http://localhost:8000/two/ http://frontend/one/;
对location字段的URI是http://localhost:8000/two/some/uri.实际转发给客户端的是http://frontend/one/;
可以使用ngx-http-core-module提供的变量来设置
proxy_redirect http://localhost:8000/ http://$host:$server_port/;
可以省略replacement参数的主机名部分,这时会用虚拟主机名称填充
proxy_redirect http://localhost:8000/two/ /one/;
使用default参数时,会按照proxy_pass配置项和所属的location配置项重组
location /one/ {
proxy_pass http://upstream:port/two/;
proxy_redirect default;
}
等于
location /one/ {
proxy_pass http://upstream:port/two/;
proxy_redirect http://upstream:port/two/ /one/;
}

proxy_next_upstream

  • 语法:proxy_next_upstream [error|timeout|invalid_header|http_500|http_502|http_503|http_504|http_404|off];
  • 默认 error timeout;
  • 配置块 http、server、location

当向一台上游转发请求出错时,继续换一台处理
(invaild_header 上游响应不合法)

  • pm2 是一个带有负载均衡功能的Node应用的进程管理器
  • 内建负载均衡(使用Node cluster 集群模块)
  • 后台运行
  • 0秒停机重载,热升级
  • 具有Ubuntu和CentOS 的启动脚本
  • 停止不稳定的进程(避免无限循环)
  • 控制台检测
  • 提供 HTTP API
  • 远程控制和实时的接口API ( Nodejs 模块,允许和PM2进程管理器交互 )
  • github
command explain
$ pm2 restart .pm2.yml –only ra 根据配置文件重启一个服务
$ npm install pm2 -g # Install PM2
$ pm2 start app.js # Start, Daemonize and auto restart application
$ pm2 start app.js -i 4 # Start 4 instances of application in cluster mode it will load balance network queries to each app
$ pm2 start app.js –name=”api” # Start application and name it “api”
$ pm2 start app.js –watch # Restart application on file change
$ pm2 start script.sh # Start bash script
$ pm2 list # List all processes started with PM2
$ pm2 monit # Display memory and cpu usage of each app
$ pm2 show [app-name] # Show all informations about application
$ pm2 logs # Display logs of all apps
$ pm2 logs [app-name] # Display logs for a specific app
$ pm2 flush
$ pm2 stop all # Stop all apps
$ pm2 stop 0 # Stop process with id 0
$ pm2 restart all # Restart all apps
$ pm2 reload all # Reload all apps in cluster mode
$ pm2 gracefulReload all # Graceful reload all apps in cluster mode
$ pm2 delete all # Kill and delete all apps
$ pm2 delete 0 # Delete app with id 0
$ pm2 scale api 10 # Scale app with name api to 10 instances
$ pm2 reset [app-name] # Reset number of restart for [app-name]
$ pm2 startup # Generate a startup script to respawn PM2 on boot
$ pm2 save # Save current process list
$ pm2 resurrect # Restore previously save processes
$ pm2 update # Save processes, kill PM2 and restore processes
$ pm2 generate # Generate a sample json configuration file
$ pm2 deploy app.json prod setup # Setup “prod” remote server
$ pm2 deploy app.json prod # Update “prod” remote server
$ pm2 deploy app.json prod revert 2 # Revert “prod” remote server by 2
$ pm2 module:generate [name] # Generate sample module with name [name]
$ pm2 install pm2-logrotate # Install module (here a log rotation system)
$ pm2 uninstall pm2-logrotate # Uninstall module
$ pm2 publish # Increment version, git push and npm publish

redux的作用

Smaller icon

Redux 主要分为三个部分 Action、Reducer、及 Store

action

  • action用来传递操作 State 的信息,以 Javascript Plain Object 的形式存在,形如
1
2
3
4
{
type: 'ADD_FILM',
name: 'Mission: Impossible'
}

type 属性是必要的,用来表达处理 state 数据的方式,其他属性任意,建议简单

但为了方便组织,可以创建函数来生产 action,即Action Creator

1
2
3
function addFilm(name) {
return { type: 'ADD_FILM', name: name };
}

Reducer

  • 处理action传达需要操作的信息
  • 通过传入旧的 state 和指示操作的 action 来更新 state

Reducer 根据传入的 action.type 来匹配 case 进行不同的 state 更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function films(state = initialState, action) {
switch (action.type) {
case 'ADD_FILM':
// 更新 state 中的 films 字段
return [{
id: state.films.reduce((maxId, film) => Math.max(film.id, maxId), -1) + 1,
name: action.name
}, ...state];
case 'DELETE_FILM':
return state.films.filter(film =>
film.id !== action.id
);
case 'SHOW_ALL_FILM':
return Object.assign({}, state, {
visibilityFilter: action.filter
});
//不要修改原先state(redux会比较前后两份state的引用,相同就直接return不做更新)。 使用 Object.assign() 新建了一个副本
default:
return state;
//在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state
}

当 action.type变多时,可以按照功能拆分,在通过组合函数合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function rootReducer(state = {}, action) {
return {
films: filmReducer(state.films, action),
filter: filterReducer(state.filter, action)
};
}
// rootReducer 将不同部分的 state 传给对应的 reducer 处理,最终合并所有 reducer 的返回值,组成整个state
//使用Redux 提供的 combineReducers() 方法
import { combineReducers } from 'redux'
var rootReducer = combineReducers({
films: filmReducer,
filter: filterReducer
});
//combineReducers() 将调用一系列 reducer,并根据对应的 key 来筛选出 state 中的一部分数据给相应的 reducer,这样也意味着每一个小的 reducer 将只能处理 state 的一部分数据

Store

  • 衔接action和reducer

Store 是单一的,维护着一个全局的 State,并且根据 Action 来进行事件分发处理 State,Store 是一个把 Action 和 Reducer 结合起来的对象。

生成store

1
2
3
import {createStore } from 'redux'
var store = createStore(rootReducer);

store 对象可以简单的理解为如下形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function createStore(reducer, initialState) {
//闭包私有变量
var currentReducer = reducer;
var currentState = initialState;
var listeners = [];
function getState() {
return currentState;
}
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
var index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
function dispatch(action) {
currentState = currentReducer(currentState, action);
listeners.slice().forEach(listener => listener());
return action;
}
//返回一个包含可访问闭包变量的公有方法
return {
dispatch,
subscribe,
getState
};
}

store.getState() 用来获取 state 数据。
store.subscribe(listener) 用于注册监听函数。每当 state 数据更新时,将会触发监听函数。
store.dispatch(action) 是用于将一个 action 对象发送给 reducer 进行处理

1
2
3
4
store.dispatch({
type: 'ADD_FILM',
name: 'Mission: Impossible'
});

connect

  • Connect 组件主要为 React 组件提供 store 中的部分 state 数据 及 dispatch 方法

通过import { connect } from ‘react-redux’将state的值和action绑定到组件的props上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { connect } from 'react-redux'
import Counter from '../components/Counter' //一个组件
import * as CounterActions from '../actions/counter'
//将state.counter绑定到props的counter
// state 将由 store 提供
function mapStateToProps(state) {
return {
counter: state.counter
}
}
//将action的所有方法绑定到props上
function mapDispatchToProps(dispatch) {
return bindActionCreators(CounterActions, dispatch)
}
//通过react-redux提供的connect方法将我们需要的state中的数据和actions中的方法绑定到props上
export default connect(mapStateToProps, CounterActions)(Counter)

saga

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import {
takeEvery
} from 'redux-saga'
import {
call,
put,
fork,
take,
cancel
} from 'redux-saga/effects'
import {
GET_LOGINED_REQUEST,
LOGIN_REQUEST
} from './login.js'
import {
loginAPI,
loginedAPI
} from '../../../api'
export function* watchRequestLogined() {
yield takeEvery(GET_LOGINED_REQUEST, queryFlow)
}
export function* queryFlow(action) {
const task = yield fork(logined)
yield take(LOGIN_CANCEL)
yield cancel(task)
}
export function* logined(){
try {
const response = yield call(loginedAPI)
yield put({
type: LOGIN_SUCCESS,
response
})
} catch (error) {
yield put({
type: LOGIN_CANCEL,
error
})
}
}
export function* watchRequestLogin() {
yield takeEvery(LOGIN_REQUEST, loginFlow)
}
export function* authorize({account, password}){
try {
const response = yield call(loginAPI, {
account,
password
})
yield put({
type: LOGIN_SUCCESS,
response
})
} catch (error) {
yield put({
type: LOGIN_ERROR,
error
})
}
}
export function* loginFlow(action) {
const { account, password } = action.payload
const task = yield fork(authorize, { account, password })
yield take(LOGIN_CANCEL)
yield cancel(task)
}

简介

  • 单向的数据流动
  • Flux应用主要包括三部分:dispatcher、store和views(React components)

Resize icon

dispatcher(调度者)

  • 管理所有的数据流
  • 本质store callback 的注册表,用来向stores分发action,可以指定注册的callback的执行顺序来管理store之间的依赖

store(仓库)

  • 包含应用的状态和逻辑,管理多个对象状态
  • 在dispatcher注册,并提供相应的回调
  • 更新后向应用广播change事件,view响应并重新获取数据

action

  • dispatcher提供了一个可以允许我们向store中触发分发的方法