作者:正龙(沪江Web前端开发工程师)
本文为原创文章,转载请注明作者及出处
上文“走进Node.js启动过程”中我们算是成功入门了。既然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事件
当服务器接受了一个连接请求后,会触发connection事件。我们可以在这个结点获取到套接字文件描述符,之后就可以在这个文件描述符上做流式读或写,也就是所谓的全双工模式。上文提到net.Server的listen方法会创建TCP对象,并且提供TCP对象的onconnection事件回调方法;这里可以利用字段net.Server.maxConnections做过载保护,后面会讲到。并且会把clientHandle(本次连接的套接字文件描述符)封装成net.Socket对象,作为connection事件的参数。我们来看看调用过程:
tcp_wrap.cc
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_);
// uv_accept can fail if the new connection has already been closed, in
// which case an EAGAIN (resource temporarily unavailable) will be
// returned.
if (uv_accept(handle, client_handle))
return;
// Successful accept. Call the onconnection callback in Jav