还没搞懂nodejs的http服务器?看这一篇就够了
http2/https不在本文的讨论范围,本文基于Nodejs v13.1.0
阅读本篇文章之前,请阅读前置文章:
- nodejs深入学习系列之libuv基础篇(一)
- nodejs深入学习系列之libuv基础篇(二)
- nodejs是如何和libuv以及v8一起合作的?(文末有彩蛋哦)
- Nodejs流学习系列之三: Duplex Stream & Transform Stream
阅读完本篇文章之后,希望你可以掌握以下知识点:
- 完整的HTTP服务器启动流程
- HTTP的数据流转
1、前置知识回顾
因为我们知道nodejs启动的服务器依赖于libuv,所以这里我们有必要将libuv如何启动tcp服务器的过程说一下,后面的内容才不会看得糊里糊涂。
这个步骤在nodejs深入学习系列之libuv基础篇(一)的2.2.10小节
uv_tcp_t有过简单的概括:
1、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
2、绑定地址:uv_tcp_bind
3、监听连接:uv_listen
4、每当有一个连接进来之后,调用uv_listen的回调,回调里要做如下事情:
4.1、初始化客户端的tcp句柄:uv_tcp_init()
4.2、接收该客户端的连接:uv_accept()
4.3、开始读取客户端请求的数据:uv_read_start()
4.4、读取结束之后做对应操作,如果需要响应客户端数据,调用uv_write,回写数据即可。
更多细节参考tcpserver.c
那么这么一个过程,nodejs是如何通过v8和js将整个过程实现出来的呢?这也是我们本文想要阐释的重点。
如果你这个时候问我:明明讲的是HTTP,为啥回顾TCP服务器的启动啊?那么请你先面壁思过三秒钟~
2、从🌰开始
nodejs的魅力在哪里?那就是启动服务器。简简单单几行代码,就可以启动一个HTTP服务器:
const http = require('http')
const server = http.createServer(() => {})
server.listen(3000)
但是你知道吗?外表看着简单,实质内心是很复杂的,就好比洋葱,光滑无比的外壳下谁会想到有一圈圈,一圈圈也就算了,还会让人流泪😭
这里的每一行代码背后都有着一套复杂的逻辑,我们将从源码入手,剖析隐藏在后面的原理。
3、http服务器涉及的主要文件
3.1、Js端
-
http.js:http模块的主入口文件
-
_http_common.js:公共模块,比如提供了我们待会会提及到的http解析器
-
_http_incoming.js:实现了
IncomingMessage
类,继承了Stream
类 -
_http_outgoing.js:实现了
OutgoingMessage
类,继承了Strema
类 -
_http_server.js:http服务器实现主文件
-
net.js:tcp服务器实现主文件
-
internal/net.js:包含一些tcp服务器辅助函数
-
internal/http.js:包含一些http服务器辅助函数
备注:_http_clien.js
和_http_agent.js
是作为HTTP客户端使用的。
3.2、C++端
- tcp_wrap.cc:实现
tcp_wrap
这个内建模块,提供诸如open
、bind
、listen
这类常用的tcp服务器方法 - connection_wrap.cc:实现一个模板类,主要根据连接的类型不同,传参不一样,支持TCP和PIPE,当有连接上来的时候会调用模板类的onConnection方法
- node_http_parser.cc:C++端的HTTP解析器,基于
llhttp
这个C语言包,可见服务器的HTTP报文解析交给执行效率更高的C++端了 - stream_wrap.cc:实现
stream_wrap
这个内建模块,基于libuv的stream
模块,继承自StreamBase
类 - stream_base.cc:实现
StreamBase
类,因为该类是stream_wrap
的父类,所以其所有的方法都可以通过stream_wrap
暴露出去
关于llhttp
的介绍,在nodejs是如何和libuv以及v8一起合作的?(文末有彩蛋哦)的第一小节**1、Nodejs依赖些啥?**有讲到过。
3.3、所有文件的关系图
下图是给出上述文件的一个简单的关系图,给大家一个基本印象:
4、http.createServer()
:服务器的实例化
当执行http.createServer
的时候,就是实例化Server
,得到的Server实例的原型结构如下图:
对应的实例结构可以通过Debug模式看到:
在这个阶段埋下两个重要的事件:request
和connection
。
到这里实例化完成,是不是很easy?
5、server.listen(3000)
当执行到server.listen(3000)
的时候,实际调用的是net.js
里面的listen
方法。而listen
方法最后归一化调用listenInCluster
,在这个方法里面,可以解释**为什么集群模式下,所有实例监听相同的端口而不会报端口被占用的错误?**因为,在这个方法中,会去判断当前实例是否是master,如果是的话才会去创建新的socket,否则是worker,则监听master中得到的socket。
而listenInCluster
中最后调用_listen2
,也就是setupListenHandle
:
Server.prototype._listen2 = setupListenHandle
setupListenHandle
最终调用:createServerHandle
,这个时候C++端才开始参与进来,这个过程的流程如下:
上述流程图,从listen方法开始到结束,展示了如何与V8和libuv的一个完美合作,期间涉及到了三个libuv方法,也就是完成我们在第一小节前置知识提到的前三个步骤:
1、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
2、绑定地址:uv_tcp_bind
3、监听连接:uv_listen
同时有一个非常重要的点就是,我们给TCP
类的实例方法onconnection
赋值了:
this._handle.onconnection = onconnection
对应于C++的代码是(初始化为Null):
t->InstanceTemplate()->Set(env->onconnection_string(), Null(env->isolate()));
env->onconnection_string()
的定义在env.h
:
V(onconnection_string, "onconnection")
也就是大家看到的js端的onconnection
方法。给这个方法赋值有啥用呢?
大家再仔细看上述流程图libuv
的一部分,最后一个调用的uv_listen
传了一个回调!这部分就是我们下一节要讲的内容。
listen完后的server实例有所变化,关注_handle
这个变量(也就是红框内):
可以看到_handle
此时也就是C++端定义的类TCP
,其原型对象是LibuvStreamWrap
,关于TCP
类的实现可以看tcp_wrap.cc
文件的TCPWrap::Initialize
,想要看得懂前提是你得先看过这篇文章:如何正确地使用v8嵌入到我们的C++应用中
到这里,服务器算是初始化完毕,接下去的内容更加有意思,请不要走开哦~
6、当有客户端的连接请求进来
在上一小节,我们埋了一个问题:设置了onconnection的js方法,但是没有后续了吗?
当然不是!我们在前置知识中讲到,调用了uv_listen
之后,给了一个回调函数,当有连接进来的时候,就会调用回调函数。而V8这里提供的回调就是在上面流程图右下角用红色加粗的函数OnConnection
。我们这一小节的内容就从这个函数开始讲起。
如下图展示了当有连接请求的时候,从操作系统底层经过Libuv之后,到js端的一个流程图:
这个过程契合了我们在前置知识中提到的这两个步骤:
4、每当有一个连接进来之后,调用uv_listen的回调,回调里要做如下事情:
4.1、初始化客户端的tcp句柄:uv_tcp_init()
4.2、接收该客户端的连接:uv_accept()
此时拿到了客户端的tcp句柄client_handle
通过回调之前设置的onconnection
方法,传值给js端:
wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
js端将该客户端封装到socket
实例后再给_http_server.js
,于是到这里主动权又回到了js端。
为了给该socket关联上http解析器,也为了在socket上监听请求的数据,在connectionListenerInternal
方法上,做了很多操作,主要有以下几件:
- 实例化解析器parser
- 对socket的
data
、end
、error
、close
、drain
、resume
、pause
等事件进行监听,并绑定对应回调 parser.onIncoming
绑定函数
关于parser的实现也是我们搞懂整个环节的一部分,这一部分我们在下一小节提及。
到这里,整个流程大家是不是有一个比较清晰的认识了?
但是,好像有一点还没有提及到,大家知道是什么吗?想想看,整个环节还缺少了啥?
没错!就是请求的数据是如何从底层传递到我们的应用程序的?这也是我们下一节要讲的内容。
7、当有请求数据到来的时候
咦~从上面一路下来,貌似整个流程已经结束了,至始至终都没有看到任何和请求数据相关的,除了监听data
事件,那么data
事件是怎么触发的?是否底层调用和我们之前前置知识的最后一个步骤如出一辙呢?带着这么多疑问,我们将视线转到刚才有一个一笔带过的环节:new Socket
7.1、new Socket
其实不简单
首先我们需要明确的一点是:
Socket
实例是一个继承双工流的套接字,因此关于流的一切用法,在Socket
上都可以用。
实例化Socket
涉及到的一些流程如下所示:
看过这篇文章Nodejs流学习系列之一: Readable Stream的童鞋都知道,Socket
实现了可读流的_read
方法,也就是上图中用①标注出来的方法,_read
是用于从底层读取数据缓存到可读流的缓存中。
上图中C++端的调用关系,请参考文件stream_base.cc
和stream_base.h
,有点C++基础的可以看下源码,里面可以看到那些复杂的C++概念:虚函数、虚类、重载、友类等。我们在这里只提一点:
-
为什么
ReadStartJS
调用了ReadStart
的时候,走到了LibuvStreamWrap::ReadStart
?因为
LibuvStreamWrap
是StreamBase
的派生类,而StreamBase
又是StreamResource
虚类的派生类,在stream_wrap.h
中声明了重载掉纯虚函数(int ReadStart() override
):virtual int ReadStart() = 0;
,所以你看到的调用关系才是上述流程图所示。
上述流程印证了我们在前置知识中提到的4.3步骤:
4.3、开始读取客户端请求的数据:uv_read_start()
我们给uv_read_start
设置的分配缓存回调如同上述流程所示。
注意:上述图的C++空间中有一块颜色特殊的注释,我们给客户端的handle实例添加了一个onread回调,这点待会在下一节会有用到
7.2、当请求数据到来时
终于来到了我们整个环节的最后一部分,兴不兴奋?激不激动?能看到这里的童鞋都很赞👍!
这个环节也是so easy!奉上经典流程图:
看上图知道最后数据到来的时候,最终是会调用到js的终极函数:onStreamRead
(stream_base_commons.js),该函数内部还有一些引用C++端的变量,有这么一张对应图:
js端将得到的缓存再通过②箭头所指的FastBuffer
实例化一块后才调用①箭头所指的stream.push()
方法。
调用这个方法有啥神奇的吗?线索又断了?😯,🙅♂️,看过这篇文章Nodejs流学习系列之一: Readable Stream的童鞋都知道,当往可读流中push数据的时候,在flow模式下是会自动触发data
事件的,于是....
大结局来了?
还记得我们之前在connectionListenerInternal
(_http_server.js)中监听的data
事件吗?
socketOnData
成了我们最后一个衔接其整个流程的最后一扣,该方法也是借助了我们在之前提到的parser
,进行各种操作。
7.3、大结局之HTTP解析器
上面我们一笔带过了parser
的分析,这一节终于轮到她粉墨登场了。parser
是对C++端内建模块http_parser
的实例化体现:
const parser = new HTTPParser()
实例化也就算了,js端还给其绑定了诸多的js回调:
parser.onIncoming = null;
parser[kOnHeaders] = parserOnHeaders;
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
parser[kOnMessageComplete] = parserOnMessageComplete;
从字面上来看,是让C++端每解析完HTTP一块就需要告知js端一次。
而socketOnData
调用的是parser.execute(d)
,我们来看一下完整的解析器流程:
将数据传给C++端,利用llhttp的高效解析,得到的HTTP头部的信息,再回传给js端,之后emit事件给在一开始我们就监听的request
事件的回调,从这里开始,你的应用代码才正式被执行,如此一气呵成!
这个时候,需要大家动动脑筋了:
为什么解析数据要搞得这么复杂?不能让C++端接收数据后一并解析完再返回给js端吗?非要将数据给js端、js端再给C++端、解析完又回传给js端,绕来绕去的~
欢迎大家留言讨论~
8、来个大总结吧
一图以蔽之来结束本文:
限于篇幅,无法面面俱到(诸如keepAlive、TCP分片之类的知识),如有想学习更多http服务器的内部实现,欢迎留言~
最后以这篇来结束在耗时两个月,网上最全的原创nodejs深入系列文章(长达十来万字的文章,欢迎收藏)立下的flag。希望整个系列对大家深入掌握nodejs有一定帮助!
感恩~
2019年的目标提前完成咯~
公众号关注一波~
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 还没搞懂nodejs的http服务器?看这一篇就够了 的内容有疑问,请在下面的评论系统中留言,谢谢。