Node.js 之 HTTP实现详细分析(下)
分针网每日分享:Node.js 之 HTTP实现详细分析(下)
Node.js 之 HTTP实现详细分析(上)讲了node代码思路分析,还讲了一下事件。这篇继续让我们了解一下node。
1. Expect头
如果客户端在发送POST请求之前,由于传输的数据量比较大,期望向服务器确认请求是否能被处理;这种情况下,可以先发送一个包含头Expect:100-continue的http请求。如果服务器能处理此请求,则返回响应状态码100(Continue);否则,返回417(Expectation Failed)。默认情况下,Node.js会自动响应状态码100;同时,http.Server会触发事件checkContinue和checkExpectation来方便我们做特殊处理。具体规则是:当服务器收到头字段Expect时:如果其值为100-continue,会触发checkContinue事件,默认行为是返回100;如果值为其它,会触发checkExpectation事件,默认行为是返回417。
例如,我们通过curl发送HTTP请求:
curl -vs --header "Expect:100-continue" http://localhost:3333
交互过程如下
> GET / HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.49.1
> Accept: */*
> Expect:100-continue
>
< HTTP/1.1 100 Continue
< HTTP/1.1 200 OK
< Date: Mon, 03 Apr 2017 14:15:47 GMT
< Connection: keep-alive
< Content-Length: 11
<
我们接收到2个响应,分别是状态码100和200。前一个是Node.js的默认行为,后一个是应用程序代码行为。
2. HTTP代理
在实际开发时,用到http代理的机会还是挺多的,比如,测试说线上出bug了,触屏版页面显示有问题;我们一般第一时间会去看api返回是否正常,这个时候在手机上设置好代理就能轻松捕获HTTP请求了。老牌的代理工具有fiddler,charles。其实,nodejs下也有,例如node-http-proxy,anyproxy。基本思路是监听request事件,当客户端与代理建立HTTP连接之后,代理会向真正请求的服务器发起连接,然后把两个套接字的流绑在一起。我们可以实现一个简单的代理服务器:
var http = require('http');
var url = require('url');
http.createServer((req, res) => {
// request回调函数
console.log(`proxy request: ${req.url}`);
var urlObj = url.parse(req.url);
var options = {
hostname: urlObj.hostname,
port: urlObj.port || 80,
path: urlObj.path,
method: req.method,
headers: req.headers
};
// 向目标服务器发起请求
var proxyRequest = http.request(options, (proxyResponse) => {
// 把目标服务器的响应返回给客户端
res.writeHead(proxyResponse.statusCode, proxyResponse.headers);
proxyResponse.pipe(res);
}).on('error', () => {
res.end();
});
// 把客户端请求数据转给中间人请求
req.pipe(proxyRequest);
}).listen(8089, '0.0.0.0');
验证下是否真的起作用,curl通过代理服务器访问我们的“hello world”版Node.js服务器:
curl -x http://192.168.132.136:8089 http://localhost:3333/
优化策略
Node.js在实现HTTP服务器时,除了利用高性能的http-parser,自身也做了些性能优化。
1. http_parser对象缓存池
http-parser对象处理完一个请求之后不会被立即释放,而是被放入缓存池(/lib/internal/freelist),最多缓存1000个http-parser对象。
2. 预设HTTP头总数
HTTP协议规范并没有限定可以传输的HTTP头总数上限,http-parser为了避免动态分配内存,设定上限默认值是32。其他web服务器实现也有类似设置;例如,apache能处理的HTTP请求头默认上限(LimitRequestFields)是100。如果请求消息中头字段真超过了32个,Node.js也能处理,它会把已经解析的头字段通过事件kOnHeaders保存到JavaScript这边然后继续解析。 如果头字段不超过32个,http-parser会直接处理完并触发on_headers_complete一次性传递所有头字段;所以我们在利用Node.js作为web服务器时,应尽量把头字段控制在32个之内。
3. 过载保护
理论上,Node.js允许的同时连接数只与进程可以打开的文件描述符上限有关。但是随着连接数越来越多,占用的系统资源也越来越多,很有可能连正常的服务都无法保证,甚至可能拖垮整个系统。这时,我们可以设置http.Server的maxConnections,如果当前并发量大于服务器的处理能力,则服务器会自动关闭连接。另外,也可以设置socket的超时时间为可接受的最长响应时间。
性能实测
为了简单分析下Node.js引入的开销,现在基于libuv和http_parser编写一个纯C的HTTP服务器。基本思路是,在默认事件循环队列上监听指定TCP端口;如果该端口上有请求到达,会在队列上插入一个一个的任务;当这些任务被消费时,会执行connection_cb。见核心代码片段:
int main() {
// 初始化uv事件循环
loop = uv_default_loop();
uv_tcp_t server;
struct sockaddr_in addr;
// 指定服务器监听地址与端口
uv_ip4_addr("192.168.132.136", 3333, &addr);
// 初始化TCP服务器,并与默认事件循环绑定
uv_tcp_init(loop, &server);
// 服务器端口绑定
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
// 指定连接处理回调函数connection_cb
// 256为TCP等待队列长度
int r = uv_listen((uv_stream_t*)&server, 256, connection_cb);
// 开始处理默认时间循环上的消息
// 如果TCP报错,事件循环也会自动退出
return uv_run(loop, UV_RUN_DEFAULT);
}
connection_cb调用uv_accept会负责与发起请求的客户端实际建立套接字,并注册流操作回调函数read_cb:
void connection_cb(uv_stream_t* server, int status) {
uv_tcp_t* client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
// 与客户端建立套接字
uv_accept(server, (uv_stream_t*)client);
uv_read_start((uv_stream_t*)client, alloc_buffer, read_cb);
}
上文中read_cb用于读取客户端请求数据,并发送响应数据:
void read_cb(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) {
if (nread > 0) {
memcpy(reqBuf + bufEnd, buf->base, nread);
bufEnd += nread;
free(buf->base);
// 验证TCP请求数据是否是合法的HTTP报文
http_parser_execute(parser, &settings, reqBuf, bufEnd);
uv_write_t* req = (uv_write_t*)malloc(sizeof(uv_write_t));
uv_buf_t* response = malloc(sizeof(uv_buf_t));
// 响应HTTP报文
response->base = "HTTP/1.1 200 OK\r\nConnection:close\r\nContent-Length:11\r\n\r\nhello world\r\n\r\n";
response->len = strlen(response->base);
uv_write(req, stream, response, 1, write_cb);
} else if (nread == UV_EOF) {
uv_close((uv_handle_t*)stream, close_cb);
}
}
全部源码请参见simple HTTP server。我们使用apache benchmark来做压力测试:并发数为5000,总请求数为100000。
ab -c 5000 -n 100000 http://192.168.132.136:3333/
测试结果如下: 0.8秒(C) vs 5秒(Node.js)
我们再看看内存占用,0.6MB(C) vs 51MB(Node.js)
Node.js虽然引入了一些开销,但是从代码实现行数上确实要简洁很多。