NodeJS源码解析--Node如何处理HTTP请求

2019-07-10 10:50:11 浏览数 (1)

看过我之前的写的文章的朋友们应该会知道,使用NodeJS创建一个HTTP服务器是非常简单的。我们写的一个个API中使用req来接收请求,使用res来响应请求。那么req/res参数从何而来?http的头部信息是如何进行创建呢?接下来我们来从源码角度分析:NodeJS究竟是如何处理一个HTTP请求。

首先使用express generator快速搭建一个express项目,命令:

代码语言:javascript复制
express analysis_http

按照提示进入项目安装依赖,然后使用npm start可以启动express项目。那究竟我们项目是如何创建http服务器并且进行启动的呢?express创建成功会在bin文件夹下生成www文件,里面有必须的启动配置。我们可以看看www文件:

我们初步可以看到,主要调用了http.createServer() server.listen()两个方法。我们现在可能会有一系列疑问:

代码语言:javascript复制
接口使用的req和res参数从何而来?
createServer()如何创建服务器?
listen()具体是进行了什么样的操作?

接下来,我们通过源码来具体分析这些问题。首先,从gitHub拉取一份NodeJS源码,地址:

代码语言:javascript复制
https://github.com/nodejs/node.git

我们先来查看lib/http.js文件关键代码:

我们可以看到createServer()方法返回的是Server的一个实例。而参数requestListener我们我们接口中的传入的回调函数:

代码语言:javascript复制
function(req, res, next) {
    res.send('respond with a resource');
}

在文件顶部可以看到Server引用的_http_server.js。所以我们去_http_server.js中看看Server这个构造函数:

由于Server继承net.Server,net.Server继承自events.EventEmitter所以可以使用on等方法。我们可以看到在Server构造函数中设置了requestconnection事件的回调函数:

代码语言:javascript复制
request使用了createServer中设置的回调方法requestListener。
connection则使用了回调方法:connectionListener。

那我们什么时候会触发connection事件呢?我们看下connectionListener关键源码:

这里比较需要注意的有parser对象以及parseOnIncoming()。我们先来看看parser对象,parser来自parsers.alloc():

代码语言:javascript复制
const parser = parsers.alloc();

从文件顶部可以看出parsers来自_http_common.js文件。我们可以看看源码:

我们可以看到,为了尽可能增加对parser进行重用,减少不断调用构造函数的消耗,parser采用了FreeList的数据结构,FreeList池中设有上限1000parser是基于事件,使用了http-parser库。然后可以看到两个比较重要的方法:parseOnHeadersCompleteparserOnMessageComplete

代码语言:javascript复制
parseOnHeadersComplete:请求头解析完成则触发本方法。
parserOnMessageComplete:接收body完成后触发本方法,数据接收完成会触发end事件。

我们再来看看FreeList的源码:

http默认创建了1000http_parser实例,每次有http请求时,都会从数组中去除一个http_parser分配给当前的socket。如果1000http_parser全部分配完毕,则会分配新的http_parser。我们解析完请求头会触发parseOnHeadersComplete方法,如果不是udp类型请求,就会触发request事件。

讲完了parser对象,我们接着回到刚才说的parseOnInComing()方法。parseOnInComing()方法使用bind,并传入参数parser,socket,state。

代码语言:javascript复制
parser.onIncoming = parserOnIncoming.bind(undefined, server, socket, state);

我们先看看parseOnInComing()的源码:

里面有个重要的判断为sockket._httpMessage。如果结果为true,说明有其他请求在占用socket。而parserOnInComing()方法用来处理解析完毕的请求,所以到这里代表解析请求头和请求体已经完成了。而刚才已经讲过:请求头解析完毕会执行parserOnHeadersComplete()方法,我们看看parserOnHeadersComplete()方法的源码:

我们可以看到里面调用了parser.incoming,parser.incoming则是ParserInComingMessage(socket)的一个实例。ParserInComingMessage继承自Stream.Readable。StreamNodeJS另一个尤其重要的知识点,不过本篇文章不进行深入讲解。

代码语言:javascript复制
Object.setPrototypeOf(IncomingMessage.prototype, Stream.Readable.prototype);
Object.setPrototypeOf(IncomingMessage, Stream.Readable);

所以整体的逻辑应该为:

代码语言:javascript复制
1.解析请求头,就会触发request事件。
2.请求头解析完毕执行parserOnHeadersComplete()方法。
3.在parserOnHeadersComplete()方法中执行了parseOnIncoming()方法。
4.最后server.emit('request', req, res)。

在触发request事件的时候,传入req, res参数。因为一开始我们说过了request绑定了回调方法:

代码语言:javascript复制
function(req, res, next) {
    res.end('respond with a resource');
}

所以触发request的时候回调方法被执行。但是body数据不会被解析,而body数据会一直存放在stream中,直到用户触发data事件来接收body中的数据。回调方法中会触发res.end()事件。那究竟listen()是做了什么操作呢?

因为只有connection事件被触发,才会触发listen()事件。所以先看下onconnection()源码:

我们接着查看调用onconnection()方法的源码:

代码语言:javascript复制
setupListenHandle(address, port, addressType, backlog, fd)

可以看到底部使用了_listen2。我们继续查看调用_listen2源码:

可以看到内部调用了server._listen2。我们再次查看调用listenInCluster的源码:

我们是使用递推,由下往上推出调用的方法,所以整体的流程应该是:

代码语言:javascript复制
1.listen()调用listenInCluster(this, pipeName, -1, -1, backlog, undefined, options.exclusive);
2.在listenInCluste()中调用server._listen2(address, port, addressType, backlog, fd, flags);
3.接着调用了setupListenHandle(address, port, addressType, backlog, fd, flags);
4.在setupListenHandle()中调用了onconnection()。
5.最终回到listen()方法并且self.emit('connection', socket);

这样在对listen事件的调用中实现对端口的监听。到这里一个http请求就解析完成了。我们可以看到我们几句代码创建一个http服务器,但是实际上NodeJS内部帮助我们封装了很多细节,而我们来了解具体的细节才更能帮助我们理解具体http请求的时候发生了什么。

0 人点赞