nodejs深刻学习系列之libuv基础篇(一)

学习完nodejs基石之一的v8基础篇(还没看过的童鞋请跳转到这里:nodejs深刻学习系列之v8基础篇),咱们此次将要继续学习另一块基石:libuv。关于libuv的设计思想,我已经翻译成中文,还没看过的童鞋仍是请跳转到这里: [译文]libuv设计思想概述,若是还没看完这篇文章的童鞋,下面的内容也不建议细看了,由于会有”代沟“的问题~html

本文的全部示例代码均可以在这个仓库中找到: libuv-demonode

一、libuv入门介绍

libuv是一个跨平台聚焦于异步IO的库,著名的event-loop即是libuv的一大特点。咱们要学习Libuv,那么就要先掌握libuv的编译。linux

1.一、libuv的编译简单介绍

和v8同样,libuv的编译简单归纳以下:git

  1. 先下载GYP:git clone https://chromium.googlesource.com/external/gyp build/gyp
  2. 指定ninja:./gyp_uv.py -f ninja
  3. 编译:ninja -C out/Debug
  4. 跑测试: ./out/Debug/run-tests

1.二、libuv简单使用

利用编译好的libuv库文件,咱们能够开始写一个简单又经典的例子: Hello world。程序员

#include "stdio.h"
#include "uv.h"

int main() {
  uv_loop_t *loop = uv_default_loop();
  printf("hello libuv");
  uv_run(loop, UV_RUN_DEFAULT);
}
复制代码

喜欢动手的童鞋能够下载一开始提到的demo,其中的hello_libuv.c即是,利用如何正确地使用v8嵌入到咱们的C++应用中这篇文章讲到的运行方式,咱们借助CLion软件和CMakeLists.txt文件来编译全部的demo模块,这方面就再也不赘述了,记得将CMakeLists.txt文件中的include_directorieslink_directories改为你在第一小节编译出来的Libuv静态库文件的目录位置。github

好了,有了上面的基础以后,咱们开始结合demo来入门这个深藏众多秘密的代码库。接下去的文章可能会比较长,一次读不完的话建议收藏起来,多读几回~编程

二、libuv的基础概念介绍与实践

看懂libuv以前,咱们须要理解下面这些概念,并用实际用例来测试这些概念。api

2.一、event-loop线程

咱们都知道线程是操做系统最基本的调度单元,而进程是操做系统的最基本的资源分配单元,所以能够知道进程实际上是不能运行,能运行的是进程中的线程。进程仅仅是一个容器,包含了线程运行中所须要的数据结构等信息。一个进程建立时,操做系统会建立一个线程,这就是主线程,而其余的从线程,都要在主线程的代码来建立,也就是由程序员来建立。所以每个可执行的运用程序都至少有一个线程安全

因而libuv一开始便启动了event-loop线程,再在这个主线程上利用线程池去建立更多的线程。在event-loop线程中是一段while(1)的死循环代码,直到没有活跃的句柄的时候才会退出,这个时候libuv进程才被销毁掉。清楚这点对于后面的学习相当重要。bash

2.二、Handle

中文翻译为句柄,如[译文]libuv设计思想概述一文所属,整个libuv的实现都是基于Handle和Request。因此理解句柄以及libuv提供的全部句柄实例才可以真的掌握libuv。按照原文所述,句柄是:

表示可以在活动时执行某些操做的长生命周期对象。
复制代码

理解这句话的意思,首先咱们抓住两个关键词:长生命周期、对象。Libuv全部的句柄都须要初始化,而初始化都会调用相似这种函数:uv_xxx_initxxx表示句柄的类型,在该函数中,会将传入的形参handle初始化,并赋值返回具体的对象,好比初始化tcp句柄:

... // 随便截取一段初始化代码
handle->tcp.serv.accept_reqs = NULL;
handle->tcp.serv.pending_accepts = NULL;
handle->socket = INVALID_SOCKET;
handle->reqs_pending = 0;
handle->tcp.serv.func_acceptex = NULL;
handle->tcp.conn.func_connectex = NULL;
handle->tcp.serv.processed_accepts = 0;
handle->delayed_error = 0
...
复制代码

理解了句柄其实就是个对象,那么长生命周期要是怎样的?仍是以TCP句柄为例子,你在这个例子tcpserver.c中,能够看到后面tcp服务器的操做:绑定端口、监听端口都是基于tcp句柄,整个句柄存活于整个应用程序,只要tcp服务器没有挂掉就一直在,所以说是长生命周期的对象。

libuv提供的全部句柄以下:

接下去咱们简单介绍如下全部的Libuv的句柄

2.2.一、uv_handle_t

首先libuv有一个基本的handle, uv_handle_t,libuv是全部其余handle的基本范式,任何handle均可以强转为该类型,而且和该Handle相关的全部API均可觉得其余handle使用。

libuv可否一直运行下去的前提是检查是否有活跃的句柄存在,而检查一个句柄是否活跃(可使用方法uv_is_active(const uv_handle_t* handle)检查),根据句柄类型不一样,其含义也不同:

  1. uv_async_t句柄老是活跃的而且不能停用,除非使用uv_close关闭掉
  2. uv_pipe_tuv_tcp_t, uv_udp_t等,这些牵扯到I/O的句柄通常也都是活跃
  3. uv_check_t, uv_idle_t, uv_timer_t等,当这些句柄开始调用uv_check_start(), uv_idle_start()的时候也是活跃的。

而检查哪些句柄活跃则可使用这个方法:uv_print_active_handles(handle->loop, stderr);

tcpserver.c为例子,咱们启动tcp服务器后,启动一个定时器去打印存在的句柄,结果以下:

[-AI] async    0x10f78e9d8
[RA-] tcp      0x10f78e660
[RA-] timer    0x7ffee049d7c0
复制代码

能够看到tcp的例子中一直存活的句柄是async、tcp、timer。它们前面中括号的标志解释以下:

R 表示该句柄被引用着
A 表示该句柄此时处于活跃状态
I 表示该句柄是内部使用的
复制代码

2.2.二、uv_timer_t

顾名思义,Libuv的计时器,用来在未来某个时候调用对应设置的回调函数。其调用时机是在整个轮询的最最开始,后面咱们会说到轮询的整个步骤。

2.2.三、uv_idle_t

Idle句柄在每次循环迭代中运行一次给定的回调,并且执行顺序是在prepare句柄以前。

prepare句柄的显著区别在于,当存在活动的空闲句柄时,循环将执行零超时轮询,而不是阻塞I/O。

uv_backend_timeout方法中咱们能够看到返回的轮询I/O超时时间是0:

if (!QUEUE_EMPTY(&loop->idle_handles))
    return 0;
复制代码

idle句柄的回调通常用来执行一些低优先级的任务。

**注意:尽管名称叫作“idle”,空闲句柄在每次循环迭代时都会调用它们的回调函数,而不是在循环其实是“空闲”的时候。**
复制代码

2.2.三、uv_prepare_t

prepare句柄将在每次循环迭代中运行一次给定的回调,并且是选择在I/O轮询以前。

问题是:libuv为何要创造这么一种句柄?其实从名称来猜想,libuv应该是想提供一种方式让你能够在轮询I/O以前作些事情,而后在轮询I/O以后使用check句柄进行一些结果的校验。

2.2.四、uv_check_t

check句柄将在每次循环迭代中运行一次给定的回调,并且是选择在I/O轮询以后。其目的在上面已经提过

2.2.五、uv_async_t

Async句柄容许用户“唤醒”事件循环,并在主线程(原文翻译为another thread,其实不对)调用一开始注册的回调。这里说的唤醒其实就是发送消息给主线程(event-loop线程),让其能够执行一开始注册的回调了。

**注意:libuv会对`uv_async_send()`作一个聚合处理。也就是说它并不会调用一次就执行一次回调。**
复制代码

咱们使用thread.c为例子,使用uv_queue_workuv_async_send来实践,获得的结果打印以下:

// 打印出主进程ID号和event-loop线程ID
I am the master process, processId => 90714
I am event loop thread => 0x7fff8c2d9380

// 这个是uv_queue_work执行的回调,从线程ID能够看到回调函数是在线程池中的某个线程中执行
I am work callback, calling in some thread in thread pool, pid=>90714
work_cb thread id 0x700001266000

// 这个是uv_queue_work执行完回调后结束的回调,从线程ID能够看到这个回调已经回到了主线程中执行
I am after work callback, calling from event loop thread, pid=>90714
after_work_cb thread id 0x7fff8c2d9380

// 这个是uv_async_init的回调,其触发是由于在work callback中执行了uv_async_send,能够从0x700001266000获得验证,该回调也是在主线程中执行
I am async callback, calling from event loop thread, pid=>90714
async_cb thread id 0x7fff8c2d9380
I am receiving msg: This msg from another thread: 0x700001266000
复制代码

2.2.六、uv_poll_t

Poll句柄用于监视文件描述符的可读性、可写性和断开链接,相似于poll(2)的目的。

Poll句柄的目的是支持集成外部库,这些库依赖于事件循环来通知套接字状态的更改,好比c-areslibssh2。不建议将uv_poll_t用于任何其余目的;由于像uv_tcp_tuv_udp_t等提供了一个比uv_poll_t更快、更可伸缩的实现,尤为是在Windows上。

可能轮询处理偶尔会发出信号,代表文件描述符是可读或可写的,即便它不是。所以,当用户试图从fd读取或写入时,应该老是准备再次处理EAGAIN错误或相似的EAGAIN错误。

同一个套接字不能有多个活跃的Poll句柄,由于这可能会致使libuv出现busyloop或其余故障。

当活跃的Poll句柄轮询文件描述符时,用户不该关闭该文件描述符。不然可能致使句柄报告错误,但也可能开始轮询另外一个套接字。可是,能够在调用uv_poll_stop()uv_close()以后当即安全地关闭fd。

**在Windows上,只有套接字的文件描述符能够被轮询,Linux上,任何[`poll(2)`](http://linux.die.net/man/2/poll)接受的文件描述符均可以被轮询**
复制代码

下面罗列的是轮询的事件类型:

enum uv_poll_event {
    UV_READABLE = 1,
    UV_WRITABLE = 2,
    UV_DISCONNECT = 4,
    UV_PRIORITIZED = 8
};
复制代码

2.2.七、uv_signal_t

Signal句柄在每一个事件循环的基础上实现Unix风格的信号处理。在udpserver.c中展现了Signal句柄的使用方式:

uv_signal_t signal_handle;
r = uv_signal_init(loop, &signal_handle);
CHECK(r, "uv_signal_init");

r = uv_signal_start(&signal_handle, signal_cb, SIGINT);

void signal_cb(uv_signal_t *handle, int signum) {
  printf("signal_cb: recvd CTRL+C shutting down\n");
  uv_stop(uv_default_loop()); //stops the event loop
}
复制代码

关于Signal句柄有几个点要知悉:

  1. 以编程方式调用raise()abort()触发的信号不会被libuv检测到;因此这些信号不会对应的回调函数。
  2. SIGKILL和SIGSTOP是不可能被捕捉到的
  3. 经过libuv处理SIGBUS、SIGFPE、SIGILL或SIGSEGV会致使未定义的行为

2.2.八、uv_process_t

process句柄将会新建一个新的进程而且可以容许用户控制该进程并使用流去创建通讯通道。对应的demo能够查看:process.c,值得注意的是,args中提供的结构体的第一个参数path指的是可执行程序的路径,好比在demo中:

const char* exepath = exepath_for_process();
char *args[3] = { (char*) exepath, NULL, NULL };
复制代码

实例中的exepath是:FsHandle的执行路径。

另一个注意点就是父子进程的std的配置,demo中提供了一些参考,若是使用管道的话还能够参考另一个demo:pipe

2.2.九、uv_stream_t

流句柄提供了双工通讯通道的抽象。uv_stream_t是一种抽象类型,libuv以uv_tcp_tuv_pipe_tuv_tty_t的形式提供了3种流实现。这个没有具体实例。可是libuv有好几个方法的入参都是uv_stream_t,说明这些方法都是能够被tcp/pipe/tty使用,具体有:

int uv_shutdown(uv_shutdown_t* req, uv_stream_t* handle, uv_shutdown_cb cb)
int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb)
int uv_accept(uv_stream_t* server, uv_stream_t* client)
int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb)
int uv_read_stop(uv_stream_t*)
int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb)
int uv_write2(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_stream_t* send_handle, uv_write_cb cb)
复制代码

2.2.十、uv_tcp_t

tcp句柄能够用来表示TCP流和服务器。上小节说到的uv_stream_tuv_tcp_t的”父类“,这里使用结构体继承的方式实现,uv_handle_tuv_stream_tuv_tcp_t三者的结构关系以下图:

使用libuv建立tcp服务器的步骤能够概括为:

一、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
二、绑定地址:uv_tcp_bind
三、监听链接:uv_listen
四、每当有一个链接进来以后,调用uv_listen的回调,回调里要作以下事情:
  4.一、初始化客户端的tcp句柄:uv_tcp_init()
  4.二、接收该客户端的链接:uv_accept()
  4.三、开始读取客户端请求的数据:uv_read_start()
  4.四、读取结束以后作对应操做,若是须要响应客户端数据,调用uv_write,回写数据便可。
复制代码

更多细节参考demo

2.2.十一、uv_pipe_t

Pipe句柄在Unix上提供了对本地域套接字的抽象,在Windows上提供了命名管道。它是uv_stream_t的“子类”。管道的用途不少,能够用来读写文件,还能够用来作线程间的通讯。咱们在实例中用来实现主线程与多个子线程的互相通讯。实现的模型是这样的:

从模型中能够看出,咱们利用管道将客户端的链接绑定到随机的一个线程上,以后的操做都是该线程和客户端的通讯。

2.2.十二、uv_tty_t

TTY句柄表示控制台的一种流,用的比较少,就很少说了~

2.2.1三、uv_udp_t

UDP句柄为客户端和服务器封装UDP通讯。使用libuv建立udp服务器的步骤能够归纳为:

一、初始化接收端的uv_udp_t: uv_udp_init(loop, &receive_socket_handle)
二、绑定地址:uv_udp_bind
三、开始接收消息:uv_udp_recv_start
四、uv_udp_recv_start里执行回调,可使用下面方法回写数据发送给客户端
  4.一、uv_udp_init初始化send_socket_handle
  4.二、uv_udp_bind绑定发送者的地址,地址能够从recv获取
  4.三、uv_udp_send发送指定消息
复制代码

若是是官方文档给出的示例的话,那么会使用uv_udp_set_broadcast设置广播的地址。具体能够参考udp

2.2.1四、uv_fs_event_t

FS事件句柄容许用户监视一个给定的路径的更新事件,例如,若是文件被重命名或其中有一个通用更改。这个句柄使用每一个平台上最佳的解决方案。

2.2.1五、uv_fs_poll_t

FS轮询句柄容许用户监视给定的更改路径。与uv_fs_event_t不一样,fs poll句柄使用stat检测文件什么时候发生了更改,这样它们就能够在不支持fs事件句柄的文件系统上工做。

2.三、Request

那么接下去就说到Request这个短生命周期的概念,中文翻译为”请求“,相似于nodejs中的req,它也是一个结构体。仍是以上述的tcp服务器为例子,有这么一段代码:

if (r < 0) {
    // 若是接受链接失败,须要清理一些东西
    uv_shutdown_t *shutdown_req = malloc(sizeof(uv_shutdown_t));

    r = uv_shutdown(shutdown_req, (uv_stream_t *)tcp_client_handle, shutdown_cb);
    CHECK(r, "uv_shutdown");
  }
复制代码

当客户端链接失败,须要关闭掉这个链接,因而咱们就会初始化一个request,而后传递给咱们须要请求的操做,这里是关闭请求shutdown

关于libuv提供的句柄和request,我这里整理一张思惟导图,能够一看:

libuv的Request操做对比于句柄,仍是比较少的。上图把每个request的使用说明都讲得一清二楚了。咱们能作的就是随时翻阅这篇文章便可。

2.3.一、uv_request_t

uv_request_t是基本的request,其余任何request都是基于该结构进行扩展,它定义的全部api其余request均可以使用。和uv_handle_t同样的功效。

2.四、libuv运行的三种模式

接着说说Libuv提供的三种运行模式:

  • UV_RUN_DEFAULT 默认轮询模式,此模式会一直运行事件循环直到没有活跃句柄、引用句柄、和请求句柄
  • UV_RUN_ONCE 一次轮询模式,此模式若是pending_queue中有回调,则会执行回调而直接跨过uv__io_poll。若是没有,则此方式只会执行一次I/O轮询(uv__io_poll)。若是在执行事后有回调压入到了pending_queue中,则uv_run会返回非0,你须要在将来的某个时间再次触发一次uv_run来清空pending_queue。
  • UV_RUN_NOWAIT 一次轮询(无视pending_queue)模式,此模式相似UV_RUN_ONCE可是不会判断pending_queue是否存在回调,直接进行一次I/O轮询。

最后

ok,限于篇幅,libuv的基础篇仍未结束,你能够点我继续阅读第二篇,也能够先本身消化消化~

相关文章
相关标签/搜索