abbshr.github.io
abbshr.github.io copied to clipboard
Node学习笔记
近两年用Node写过几个web应用。过程远比预期的困难,涉及到了HTTP协议、TCP/IP协议、WebSocket协议等计算机网络知识,MongoDB、noSQL、schema设计等NoSQL数据库知识,进程、线程、同步、异步、并发、阻塞、文件系统、锁等操作系统知识,这些都是超出Node范畴的东西。然而Node中却处处提到他们,而Node的核心也构建在大多数这些基础知识之上,我觉得仅仅是“使用”并不能让你真正理解精髓。所以,要想理解&精通Node,需先广泛汲取必备知识,这也是我为毛要学习底层的原因。
这篇笔记记录了我学习libuv的过程,包括对一些模糊概念的解释,Node事件机制的实现。
没错,libuv就是Node两个核心架构(libuv + V8)之一!
/* note: 操作系统默认为*nix */
先获取libuv的源码。git clone下来github上的项目或通过HTTP访问dist页面下载,当时的版本是 libuv-v0.11.17 。
编译源码。过程很简单,我们可以参照README进行构建,这里提供了两种方式:
1.通过autotools:
$ sh autogen.sh
$ ./configure
$ make
$ make check
$ make install
2.通过GYP自动构建工具(我采用了第一种方式)
make会自动在/usr/local/lib
目录下增加编译libuv应用程序使用的动态/静态链接库:libuv.so
和libuv.a
,同时在/usr/local/include
目录下添加uv.h
头文件。
源码编译通过后就可以开始学习之旅了。可以根据joyent的wiki或者源码注释详细的学习libuv的实现与使用方法。目前网上有一份官方英文版的初步介绍libuv的doc“An Introduction to libuv”。
我会按照上面提到的文档中目录的顺序写这篇笔记,并且只要大学认真的学过C语言就能读懂代码。
Chapter 1:科普libuv
libuv是Node.js底层架构的一部分,作为异步I/O库,为Node提供了事件循环与回调机制以及对POSIX标准系统API的访问能力,例如:Socket、FileSystem、Process、Thread以及进程间通信等ECMAScript标准中不具备的。
在libuv之前也有一个名为libev的库,只不过libev仅适用于*nix,对Windows并没有提供支持。而libuv封装了Windows平台和Unix平台一些底层的特性,对外提供了一套统一的API。
Chapter 2:从Node说起
Node对外宣传自身无与伦比的并发性能时常常提到“单线程异步无阻塞I/O模型”,不过大多数人刚开始接触Node时总存在这样几个疑惑:
- 既然Node是单线程的,那么怎么会存在“无阻塞”呢?
- 事件循环究竟是怎么回事?
- 如何做到异步回调的?
为了解决这些问题,我们必须从底层的libuv入手。 其实在Node启动后至少跑起了两个线程:V8引擎线程和libuv线程。v8用来解析执行JavaScript语法,libuv在最后开启事件循环和监听器,检查并捕捉异步I/O完成后传来的消息。(这里不要误解,并不是说Node一启动就开俩线程,而是在一个主线程中执行v8 C++代码和libuv C代码,只是libuv涉及到了启用多线程)
我们的问题都能在下面这个例子中得到解答。
/* step 1 */
var http = require('http');
var fs = require('fs');
/* step 2 */
console.log('first interrupt');
/* step 3 */
http.createServer(callback).listen(8080);
/* step 4 */
console.log('output early');
function callback(req, res) {
var buffer;
req.on('data', function (data) {
buffer += data;
});
res.end(data);
}
所谓“单线程”是指Node中主线程,所有的JavaScript代码都在这个线程中被解析执行。命令node app.js
之后,Node线程按照注释中step的顺序解析代码。
step 1解析后涉及到文件的同步读取操作,后台libuv文件读取函数调用完成后,进入step 2,执行同步的控制台输出语句,step 2完成,开始step 3创建http服务器的异步函数。v8解析执行完毕后,将会调用libuv的网络相关函数。
与此同时,被调用的libuv函数将生成一个对象req,包含来自V8提供的参数信息、将要调用的底层函数的指针。然后为对应的I/O Wathcer注册一个参数为对象req的回调函数,之后开启一个I/O线程用来处理I/O请求,这个I/O线程首先将对象req中的底层函数指针提取出来以执行这个函数,这个函数调用POSIX系统网络访问API,做一个系统调用,从用户态陷入内核态,交由操作系统内核进程完成主机端口的监听并通过操作系统注册一个消息监听器以便任务完成时通知I/O线程。
当I/O线程接收到来自操作系统的调用完成消息通知,把调用结果赋给对象req的result属性,随后将req加入对应的I/O Watchers队列里。
从这里开始,Node主线程的前三个任务完成,立即开始step 4的控制台输出。
而后libuv的工作就是做事件循环检测到来的事件。事件循环依次询问各种类型的Watchers是否有完成的事件?Wathcers开始检查自己的队列是否有req对象,如果有的话,通知事件循环“是”,然后事件循环将req对象依次出队做Wathcers的参数执行之前绑定的回调函数。在这个回调函数中,会检查req对象的V8传入参数,如果V8线程传入了callback,就以req的result属性为参数在主线程中调用callback。
然后事件循环会继续检测本次循环中是否还有活跃的(产生新事件)监视器,如果没有的话就退出事件循环程序结束,否则接着循环。
以上回答了第一和第三个问题,不过“事件循环”究竟是啥还没有回答清楚,接下来先简单的解释一下事件循环。
事件循环是libuv中捕获广播事件的一种机制,它类似一个循环的结构。Linux系统下该机制由epoll轮询实现:当Node启动时,代码最后开始第一次事件循环,如果此时没有事件发生,就阻塞进程,让出CPU。当新的req对象加入Watcher队列(也就是内核完成了I/O操作),激活事件循环,并再次询问Wathcer是否有事件到来,如果有就调用对应Watcher的回调函数,然后询问其他Wathcers是否还有待处理的事件,没有的话退出事件循环。下面是epoll实现的事件循环模型:
while (true) {
epollwait(core_events); // 内核没有发送I/O事件就阻塞循环
while (new events in Watchers)
while (Watcher[i].queue NOT NULL)
execute Watchers[i].callback with args req;
}
使用libuv编写Node模块扩展就会发现这个“事件循环”由uv_run
函数启动。按照libuv参考的说法,该函数封装了事件循环,通常最后调用,并传入uv_default_loop
参数来获取默认的事件循环:(Node使用默认事件循环作为主循环)
这里给一个官方的小例子,跑一个新的事件循环:
#include <uv.h>
#include <stdlib.h>
int main() {
uv_loop_t *loop = uv_loop_new();
printf("Now quitting.\n");
uv_run(loop, UV_RUN_DEFAULT);
return 0;
}
how to run it?
注意最开始提到的libuv.so
,这是编译时必须引入的,如果直接gcc -o test test.c
会编译出错。
gcc -o test test.c -luv
./test
Chapter 3:疑问
对于事件循环,epoll的实现机制是透明的。不过关于“epoll如何捕获内核I/O事件”仍是个问题,网上查阅大量资料无果。
仍需继续阅读libuv和Node源码。