分针网每日分享:Node.js 之 HTTP实现详细分析(下)node
Node.js 之 HTTP实现详细分析(上)讲了node代码思路分析,还讲了一下事件。这篇继续让咱们了解一下node。
若是客户端在发送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
-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的默认行为,后一个是应用程序代码行为。
在实际开发时,用到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) => {
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,自身也作了些性能优化。
http-parser对象处理完一个请求以后不会被当即释放,而是被放入缓存池(/lib/internal/freelist),最多缓存1000个http-parser对象。
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个以内。
理论上,Node.js容许的同时链接数只与进程能够打开的文件描述符上限有关。可是随着链接数愈来愈多,占用的系统资源也愈来愈多,颇有可能连正常的服务都没法保证,甚至可能拖垮整个系统。这时,咱们能够设置http.Server的maxConnections,若是当前并发量大于服务器的处理能力,则服务器会自动关闭链接。另外,也能够设置socket的超时时间为可接受的最长响应时间。
为了简单分析下Node.js引入的开销,如今基于libuv和http_parser编写一个纯C的HTTP服务器。基本思路是,在默认事件循环队列上监听指定TCP端口;若是该端口上有请求到达,会在队列上插入一个一个的任务;当这些任务被消费时,会执行connection_cb。见核心代码片断:
int
main() {
loop
= uv_default_loop();
uv_tcp_t server
;
struct sockaddr_in addr
;
uv_ip4_addr("192.168.132.136", 3333, &addr);
uv_tcp_init(loop, &server);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
int r
= uv_listen((uv_stream_t*)&server, 256, connection_cb);
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);
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));
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虽然引入了一些开销,可是从代码实现行数上确实要简洁不少。