如果喜欢我的文章,欢迎关注我的个人公众号:周先生自留地。简书不定时更新
看过我之前的写的文章的朋友们应该会知道,使用NodeJS创建一个HTTP服务器是非常简单的。我们写的一个个API中使用req来接收请求,使用res来响应请求。那么req/res参数从何而来?http的头部信息是如何进行创建呢?接下来我们来从源码角度分析:NodeJS究竟是如何处理一个HTTP请求。
首先使用express generator快速搭建一个express项目,命令:
express analysis_http
按照提示进入项目安装依赖,然后使用npm start可以启动express项目。那究竟我们项目是如何创建http服务器并且进行启动的呢?express创建成功会在bin文件夹下生成www文件,里面有必须的启动配置。我们可以看看www文件:
我们初步可以看到,主要调用了http.createServer() 和 server.listen()两个方法。我们现在可能会有一系列疑问:
接口使用的req和res参数从何而来?
createServer()如何创建服务器?
listen()具体是进行了什么样的操作?
接下来,我们通过源码来具体分析这些问题。首先,从gitHub拉取一份NodeJS源码,地址:
https://github.com/nodejs/node.git
我们先来查看lib/http.js文件关键代码:
我们可以看到createServer()方法返回的是Server的一个实例。而参数requestListener我们我们接口中的传入的回调函数:
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构造函数中设置了request和connection事件的回调函数:
request使用了createServer中设置的回调方法requestListener。
connection则使用了回调方法:connectionListener。
那我们什么时候会触发connection事件呢?我们看下connectionListener关键源码:
这里比较需要注意的有parser对象以及parseOnIncoming()。我们先来看看parser对象,parser来自parsers.alloc():
constparser = parsers.alloc();
从文件顶部可以看出parsers来自_http_common.js文件。我们可以看看源码:
我们可以看到,为了尽可能增加对parser进行重用,减少不断调用构造函数的消耗,parser采用了FreeList的数据结构,FreeList池中设有上限1000,parser是基于事件,使用了http-parser库。然后可以看到两个比较重要的方法:parseOnHeadersComplete和parserOnMessageComplete。
parseOnHeadersComplete:请求头解析完成则触发本方法。
parserOnMessageComplete:接收body完成后触发本方法,数据接收完成会触发end事件。
我们再来看看FreeList的源码:
http默认创建了1000个http_parser实例,每次有http请求时,都会从数组中去除一个http_parser分配给当前的socket。如果1000个http_parser全部分配完毕,则会分配新的http_parser。我们解析完请求头会触发parseOnHeadersComplete方法,如果不是udp类型请求,就会触发request事件。
讲完了parser对象,我们接着回到刚才说的parseOnInComing()方法。parseOnInComing()方法使用bind,并传入参数parser,socket,state。
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。而Stream是NodeJS另一个尤其重要的知识点,不过本篇文章不进行深入讲解。
Object.setPrototypeOf(IncomingMessage.prototype,Stream.Readable.prototype);
Object.setPrototypeOf(IncomingMessage,Stream.Readable);
所以整体的逻辑应该为:
1.解析请求头,就会触发request事件。
2.请求头解析完毕执行parserOnHeadersComplete()方法。
3.在parserOnHeadersComplete()方法中执行了parseOnIncoming()方法。
4.最后server.emit('request',req,res)。
在触发request事件的时候,传入req, res参数。因为一开始我们说过了request绑定了回调方法:
function(req, res, next){res.end('respond with a resource');}
所以触发request的时候回调方法被执行。但是body数据不会被解析,而body数据会一直存放在stream中,直到用户触发data事件来接收body中的数据。回调方法中会触发res.end()事件。那究竟listen()是做了什么操作呢?
因为只有connection事件被触发,才会触发listen()事件。所以先看下onconnection()源码:
我们接着查看调用onconnection()方法的源码:
setupListenHandle(address, port, addressType, backlog, fd)
可以看到底部使用了_listen2。我们继续查看调用_listen2源码:
可以看到内部调用了server._listen2。我们再次查看调用listenInCluster的源码:
我们是使用递推,由下往上推出调用的方法,所以整体的流程应该是:
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请求的时候发生了什么。
本篇文章到这里结束了,如果喜欢我的文章,欢迎关注我的个人公众号:周先生自留地。
网友评论