分针网每日分享:Node.js 之 HTTP实现详细分析前端
Node.js的强项是处理网络请求,那咱们就来分析一个HTTP请求在Node.js中是怎么被处理的,以及JavaScript在这个过程当中引入的开销到底有多大。
Node.js采用的网络请求处理模型是IO多路复用。它与传统的主从多线程并发模型是有区别的:只使用有限的线程数(1个),因此占用系统资源不多;操做系统级的异步IO支持,能够减小用户态/内核态切换,而且自己性能更高(由于直接与网卡驱动交互);JavaScript天生具备保护程序执行现场的能力(闭包),传统模型要么依赖应用程序本身保存现场,或者依赖线程切换时自动完成。固然,并不能说IO多路复用就是最好的并发模型,关键仍是看应用场景。
咱们来看“hello world”版Node.js网络服务器:
require('http').createServer((req, res) => {
res
.end('hello world');
}).listen(3333);
createServer([requestListener])
createServer建立了http.Server对象,它继承自net.Server。事实上,HTTP协议确实是基于TCP协议实现的。createServer的可选参数requestListener用于监听request事件;另外,它也监听connection事件,只不过回调函数是http.Server本身实现的。而后调用listen让http.Server对象在端口3333上监听链接请求并最终建立TCP对象,由tcp_wrap.h实现。最后会调用TCP对象的listen方法,这才真正在指定端口开始提供服务。咱们来看看涉及到的全部JavaScript对象:
涉及到的C++类大多只是对libuv作了一层包装并公布给JavaScript,因此不在这里特别列出。咱们有必要提一下http-parser,它是用来解析http请求/响应消息的,自己十分高效:没有任何系统调用,没有内存分配操做,纯C实现。
当服务器接受了一个链接请求后,会触发connection事件。咱们能够在这个结点获取到套接字文件描述符,以后就能够在这个文件描述符上作流式读或写,也就是所谓的全双工模式。上文提到net.Server的listen方法会建立TCP对象,而且提供TCP对象的onconnection事件回调方法;这里能够利用字段net.Server.maxConnections作过载保护,后面会讲到。而且会把clientHandle(本次链接的套接字文件描述符)封装成net.Socket对象,做为connection事件的参数。咱们来看看调用过程:
void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
int err
= uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
backlog
,
OnConnection
);
args
.GetReturnValue().Set(err);
}
OnConnection 在connection_wrap.cc中定义
uv_stream_t
* client_handle =
reinterpret_cast
<uv_stream_t*>(&wrap->handle_);
if (uv_accept(handle, client_handle))
return;
argv
[1] = client_obj;
wrap_data
->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
上文提到的clientHandle其实是uv_accept的第二个参数,指服务当前链接的套接字文件描述符。net.Server的字段 _handle 会在JavaScript侧存储该字段。最后咱们上一张流程图:
connection事件的回调函数connectionListener(lib/_http_server.js)中,首先获取http-parser对象,设置parser.onIncoming回调(立刻会用到)。当链接套接字有数据到达时,调用http-parser.execute方法。http-parser在解析过程当中会触发以下回调函数:
on_message_begin:在开始解析HTTP消息以前,能够设置http-parser的初始状态(注意http-parse有多是复用的而不是重每次新建立)
on_url:解析请求的url,对响应消息不起做用
on_status, 解析状态码,只对http响应消息起做用
on_headers_complete:当全部头解析完成时
on_body:解析http消息中包含的payload
on_message_complete:解析工做结束
Node.js中Parser类是对http-parser的包装,它会注册上面全部的回调函数。同时,暴露给JavaScript5个事件:
kOnHeaders,kOnHeadersComplete,kOnBody,kOnMessageComplete,kOnExecute。在lib/_http_common.js中监听了这些事件。其中,当须要强制把头字段回传到JavaScript时会触发kOnHeaders;例如,头字段个数超过32,或者解析结束时仍然有头字段没有回传给JavaScript。当调用完http_parser_execute后触发kOnExecute。kOnHeadersComplete事件触发时,会调用parser的onIncoming回调函数。仅仅HTTP头解析完成以后,就会触发request事件。执行流程以下:
说了那么多,其实仍然离不开最基础的套接字编程步骤,对于服务器端依次是:create、bind,listen、accept和close。客户端会经历create、bind、connect和close。想了解更多套接字编程的同窗能够参考《UNIX网络编程》。
上面提到的Node.js版hello world只涵盖了HTTP处理最基本的状况,可是也足以说明Node.js处理得很是简洁。如今,咱们来分析一些典型的HTTP场景。
对于前端应用,HTTP请求瞬间数量比较多,但每一个请求传输的数据通常不大;这时,用同一个TCP链接处理同一个用户发出的HTTP请求能够显著提升性能。可是keep-alive也不是万能的,若是用户每次只发起一个请求,它反而会由于延长链接的生存时间,浪费服务器资源。
针对同一个链接,Node.js会维持一个incoming队列和一个outgoing队列。应用程序经过监听request事件,能够访问ServerResponse和IncomingMessage对象,当请求处理完成以后(调用response.end()),ServerResponse会响应finish事件。若是它是本次链接上最后一个response对象,则准备关闭链接;不然,继续触发request事件。每一个链接最长超时时间默认为2分钟,能够经过http.Server.setTimeout调整。
如今把咱们的Node.js版hello world修改一下
var delay = [2000, 30, 500];
var i = 0;
require('http').createServer((req, res) => {
setTimeout(() => {
res
.end('hello world');
}, delay[i]);
i
= (i+1)%(delay.length);
}).listen(3333, () => {
console
.log('listen at 3333');
});
var http = require('http');
var agent = new http.Agent({
keepAlive
: true,
keepAliveMsecs
: 60000
});
function doReq(again, iter) {
let request = http.request({
hostname
: '192.168.1.10',
port
: 3333,
agent
:agent
}, (res) => {
console
.log(`${new Date().valueOf()} ${iter} ${again} Headers: ${JSON.stringify(res.headers)}`);
console
.log(request.socket.localPort);
res
.setEncoding('utf8');
res
.on('data', (chunk) => {
console
.log(`${new Date().valueOf()} ${iter} ${again} Body: ${chunk}`);
});
if (again) doReq(false, iter);
});
request
.end();
}
for (let i = 0; i < 3; i++) {
doReq(true, i);
}