Node.js 之 HTTP实现详细分析(下)

时间:2022-03-20 15:56:05

分针网每日分享: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)   Node.js 之 HTTP实现详细分析(下)    我们再看看内存占用,0.6MB(C) vs  51MB(Node.js)   Node.js 之 HTTP实现详细分析(下)    Node.js虽然引入了一些开销,但是从代码实现行数上确实要简洁很多。    本文转自:http://www.f-z.cn/id/288