这周给小组内部做了个关于node的小分享,在这里记录下来。因为很多是网上已经有的内容,这篇文章只是把ppt给搬了过来,列出重要的点,具体就不展开了。
一小段启动http服务的代码
|
|
require(‘http’)是怎么工作的?
node.js模块分类
- builtin module: Node 中以 c++ 形式提供的模块,如 tcp_wrap、contextify 等
constants module: Node 中定义常量的模块,用来导出如 signal, openssl 库、 文件访问权限等常量的定义。如文件访问权限中的 O_RDONLY,O_CREAT、 signal 中的 SIGHUP,SIGINT 等 - native module: Node 中以 JavaScript 形式提供的模块,如 http,https,fs 等
- 3rd-party module: 以上模块可以统称 Node 内建模块,除此之外为第三方模 块,典型的如 express 模块。
对于 node 自身提供的模块,其实无论是native JS模块还是builtin C++模块,最终都在编译生成可执行文件时,嵌入到了ELF格式的二进制文件node里面。但是原生js模块代码只是保存在node_natives.h中的C++变量不会被编译。程序加载native JS模块会通过node.cc::Binding("natives")
拿到模块代码,而加载C++模块则直接用get_builtin_module
方法。参考下图:
js模块加载
- 如果模块在缓存中,返回它的 exports 对象。
- 如果是原生的模块,通过调用 NativeModule.require() 返回结果(process.binding(‘natives’)获取保存在node_natives.h中的原生js模块代码,再通过NativeModule.require进行compile。)。
- 否则,调用Module创建一个新的模块,并保存到缓存中。
NativeModule.require
重点代码如下:
|
|
CommonJs几点注意项
同步加载同步执行
nodejs中执行到require模块时用fs.readFileSync同步加载文件,加载完同步执行,如果有缓存则直接返回exports。然后继续执行主程序。
模块引用
假设在模块main中require模块a,main模块拿到a模块的module.exports对象,如果是第一次加载则对象被缓存,下一次require(‘a’)就直接拿这个缓存。 所以如果引入一个模块后改变其中暴露的某个变量,那么在其他引入的地方获取到的变量值也会改变。
循环引用
缓存机制还解决了循环引用的问题。从源码可以发现模块加载的时候执行的时候会先保存在cache中,所以当a引用b,而b又引用a的情况下,在b执行的时候a已经缓存起来了,所以b只会拿到a的模块缓存(a的exports对象),所以不会出现循环问题。
http是怎么工作的?
http之事件
EventEmitter使用观察者模式构建了一个事件处理器。允许我们注册一个或多个函数作为 listeners ,在特定的事件触发时调用。如下图:
http之stream
普通的文件操作我们可以使用fs模块的read和write方法来进行:
不过还有一个更好用的方法stream:
nodejs 底层一共提供了4个流, Readable 流、Writable 流、Duplex 流和 Transform 流。
Duplex 流和 Transform 流是可读可写的全双工流。流的用法如下:
Unix/Linux 基本哲学之一就是“一切皆文件”,所以Stream在nodejs中有多种实现形式,并且是EventEmitter的实现,例如:
- fs read write streams
- zlib streams
- http response request
- tcp sockets
- child process stdout and stderr
从中可以看到http的response对象和request对象都是可以用Stream处理的,也就是说我们使用nodejs进行http通讯时,客户端和服务器之间进行的数据交互其实可以看成是读写流数据的读取与写入。
并且,继承自EventEmitter使得这些流可以监听数据的状态,当有数据准备就绪时Readable可触发回调获取数据。
另外,response对象和request对象之所以能进行流处理,是因为底层的一个全双工读写流实现——socket。
http之socket
http.createServer
是个什么东西?
|
|
从上面代码看看起来createServer只是创建了一个事件监听器,那到底是从哪里处理请求的呢?
Server对象通过listen方法新建一个socket监听端口,请求到来时通过C++层触发onconnection回调,封装这个连接对应的socket,触发Server对象connection事件,将socket作为参数,执行事件回调。
httpServer的connection回调对socket进一步封装,对外暴露request和response对象,然后httpServer可通过这两个对象进行数据交互。代码如下:
|
|
http之httpParser
http的request和response对象并不是直接将我们发送的数据丢给socket发送,要进行http协议交互,还需要将数据封装成http报文格式。所以这里要用到httpParser对数据进行处理。这里以服务的接收http请求为例:
- parserOnHeaders:不断解析推入的请求头数据。
- parserOnHeadersComplete:请求头解析完毕,构造 header 对象,为请求体 创建 http.IncomingMessage 实例。
- parserOnBody:不断解析推入的请求体数据。
- parserOnExecute:请求体解析完毕,检查解析是否报错,若报错,直接触发 clientError 事件。若请求为 CONNECT 方法,或带有 Upgrade 头,则直接触 发 connect 或 upgrade 事件。
- parserOnIncoming:处理具体解析完毕的请求。
最后,我们可以看到http模块的通讯利用到了node中的很多核心功能,httpServer首先继承了EventEmitter,可以监听请求状态事件,然后底层依赖了net模块的socket使用流读(fs模块)写进行数据发送与读取,最后用到httpParser对数据进行封装与解析(http报文格式)。
注:大部分内容参考自:《深入理解Node.js:核心思想与源码分析》