8.实用工具
本章对常用任务中有用的工具和技术进行了分类。libev手册页已经介绍了一些可以通过简单的API更改被libuv采用的模式。它还涵盖了libuv API的一些部分,这些部分不需要专门用一整章来描述。
定时器
计时器在计时器启动一段时间后调用回调。还可以将Libuv计时器设置为定期调用,而不是只调用一次。
简单的用法是初始化一个监视程序,并以一个超时和可选的repeat启动它。计时器可以随时停止。
uv_timer_t timer_req; uv_timer_init(loop, &timer_req); uv_timer_start(&timer_req, callback, 5000, 2000);
将启动一个重复计时器,它首先在uv_timer_start执行后5秒(超时)开始,然后每2秒重复一次(重复)。使用:
uv_timer_stop(&timer_req);
停止计时器。这也可以在回调中安全地使用。
uv_timer_set_repeat(uv_timer_t *timer, int64_t repeat);
这将在可能的时候生效。如果这个函数从计时器回调调用,它意味着:
- 如果计时器是非重复的,则计时器已经停止。再次使用uv_timer_start。
- 如果计时器在重复,则下一个超时已经被安排,因此在计时器切换到新的间隔之前将再次使用旧的重复间隔。
实用的方法
int uv_timer_again(uv_timer_t *)
仅适用于重复计时器,相当于停止计时器,然后启动计时器,并将初始超时和repeat设置为旧的repeat值。如果计时器还没有启动,它会失败(错误码UV_EINVAL)并返回-1。
一个实际的计时器示例在引用计数部分中。
事件循环引用计数
事件循环只在有活动句柄时才运行。这个系统的工作原理是,在事件循环启动时,每个句柄增加事件循环的引用计数,在事件循环停止时减少引用计数。也可以手动更改句柄的引用计数:
void uv_ref(uv_handle_t*);
void uv_unref(uv_handle_t*);
这些函数可用于允许循环退出,即使监视程序处于活动状态,或者使用自定义对象保持循环处于活动状态。
后者可以与间隔计时器一起使用。您可能有一个每X秒运行一次的垃圾收集器,或者您的网络服务可能会定期向其他服务器发送一个心跳,但是您不希望在所有干净的退出路径或错误场景中不得不停止它们。或者你想在其他观察者结束后退出程序。在这种情况下,只需在创建后立即unref计时器,以便如果它是唯一运行的观察者,那么uv_run仍然会退出。
这也在node.js中使用,其中一些libuv方法被冒泡到JS API中。uv_handle_t(所有观察者的超类)是为每个JS对象创建的,可以是ref/unrefed。
ref-timer/main.c
1 uv_loop_t *loop; 2 uv_timer_t gc_req; 3 uv_timer_t fake_job_req; 4 5 int main() { 6 loop = uv_default_loop(); 7 8 uv_timer_init(loop, &gc_req); 9 uv_unref((uv_handle_t*) &gc_req); 10 11 uv_timer_start(&gc_req, gc, 0, 2000); 12 13 // could actually be a TCP download or something 14 uv_timer_init(loop, &fake_job_req); 15 uv_timer_start(&fake_job_req, fake_job, 9000, 0); 16 return uv_run(loop, UV_RUN_DEFAULT); 17 }
我们初始化垃圾收集器计时器,然后立即取消它的引用。观察9秒之后,当假作业完成时,程序如何自动退出,即使垃圾收集器仍在运行。
空转模式
空闲句柄的回调在每个事件循环中被调用一次。空闲回调可用于执行一些非常低优先级的活动。例如,您可以将每日应用程序性能的摘要发送给开发人员,以便在空闲期间进行分析,或者使用应用程序的CPU时间来执行SETI计算:)空闲的监视程序在GUI应用程序中也很有用。假设您正在对文件下载使用事件循环。如果TCP套接字仍在建立中,并且没有其他事件出现,您的事件循环将暂停(阻塞),这意味着您的进度条将冻结。
idle-compute/main.c
1 uv_loop_t *loop; 2 uv_fs_t stdin_watcher; 3 uv_idle_t idler; 4 char buffer[1024]; 5 6 int main() { 7 loop = uv_default_loop(); 8 9 uv_idle_init(loop, &idler); 10 11 uv_buf_t buf = uv_buf_init(buffer, 1024); 12 uv_fs_read(loop, &stdin_watcher, 0, &buf, 1, -1, on_type); 13 uv_idle_start(&idler, crunch_away); 14 return uv_run(loop, UV_RUN_DEFAULT); 15 }
这里我们初始化空闲监视器,并将其与我们感兴趣的实际事件一起排队。crunch_away现在将被反复调用,直到用户输入一些内容并按下Return。然后,当循环处理输入数据时,它将被短暂中断,之后它将继续再次调用空闲回调。
idle-compute/main.c
1 void crunch_away(uv_idle_t* handle) { 2 // Compute extra-terrestrial life 3 // fold proteins 4 // computer another digit of PI 5 // or similar 6 fprintf(stderr, "Computing PI...\n"); 7 // just to avoid overwhelming your terminal emulator 8 uv_idle_stop(handle); 9 }
向工作线程传递数据
当使用uv_queue_work时,你通常需要将复杂的数据传递给工作线程。解决方案是使用一个结构体并设置uv_work_t。数据可以证明这一点。一个细微的变化是将uv_work_t本身作为这个结构的第一个成员(称为接棒1)。这允许在一个自由调用中清理工作请求和所有数据。
struct ftp_baton { uv_work_t req; char *host; int port; char *username; char *password; }
1 ftp_baton *baton = (ftp_baton*) malloc(sizeof(ftp_baton)); 2 baton->req.data = (void*) baton; 3 baton->host = strdup("my.webhost.com"); 4 baton->port = 21; 5 // ... 6 7 uv_queue_work(loop, &baton->req, ftp_session, ftp_cleanup);
在这里,我们创建了接力棒并将任务排队。
1 void ftp_session(uv_work_t *req) { 2 ftp_baton *baton = (ftp_baton*) req->data; 3 4 fprintf(stderr, "Connecting to %s\n", baton->host); 5 } 6 7 void ftp_cleanup(uv_work_t *req) { 8 ftp_baton *baton = (ftp_baton*) req->data; 9 10 free(baton->host); 11 // ... 12 free(baton); 13 }
然后我们解放了指挥棒,这也解放了观察者。
轮训外部的输入输出
通常第三方库将处理它们自己的I/O,并在内部跟踪它们的套接字和其他文件。在这种情况下,不可能使用标准流I/O操作,但是库仍然可以集成到libuv事件循环中。所需要的就是这个库允许您访问底层的文件描述符,并提供处理由应用程序决定的小增量任务的函数。但有些库不允许这样的访问,只提供一个标准的阻塞函数,该函数将执行整个I/O事务,然后才返回。在事件循环线程中使用这些是不明智的,使用线程池工作调度代替。当然,这也意味着将失去对库的粒度控制。
libuv的uv_poll部分只是使用操作系统通知机制来监视文件描述符。在某种意义上,libuv自己实现的所有I/O操作都是由uv_poll支持的,就像代码一样。当操作系统注意到正在轮询的文件描述符中的状态变化时,libuv将调用相关的回调。
这里我们将介绍一个简单的下载管理器,它将使用libcurl下载文件。我们不会将所有的控制权交给libcurl,而是使用libuv事件循环,并在libuv通知I/O就绪时使用非阻塞、异步的多接口来进行下载。
uvwget/main.c - The setup
1 #include <assert.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <uv.h> 5 #include <curl/curl.h> 6 7 uv_loop_t *loop; 8 CURLM *curl_handle; 9 uv_timer_t timeout; 10 } 11 12 int main(int argc, char **argv) { 13 loop = uv_default_loop(); 14 15 if (argc <= 1) 16 return 0; 17 18 if (curl_global_init(CURL_GLOBAL_ALL)) { 19 fprintf(stderr, "Could not init cURL\n"); 20 return 1; 21 } 22 23 uv_timer_init(loop, &timeout); 24 25 curl_handle = curl_multi_init(); 26 curl_multi_setopt(curl_handle, CURLMOPT_SOCKETFUNCTION, handle_socket); 27 curl_multi_setopt(curl_handle, CURLMOPT_TIMERFUNCTION, start_timeout); 28 29 while (argc-- > 1) { 30 add_download(argv[argc], argc); 31 } 32 33 uv_run(loop, UV_RUN_DEFAULT); 34 curl_multi_cleanup(curl_handle); 35 return 0; 36 }
每个库与libuv集成的方式各不相同。在libcurl的情况下,我们可以注册两个回调。套接字回调函数handle_socket在套接字状态改变时被调用,我们必须开始轮询它。libcurl调用start_timeout来通知我们下一个超时时间间隔,在这个时间间隔之后,不管I/O状态如何,我们都应该向前驱动libcurl。这样libcurl就可以处理错误或做其他任何让下载移动所需的事情。
我们的下载器将被调用为:
$ ./uvwget [url1] [url2] ...
因此,我们将每个参数添加为URL
uvwget/main.c - Adding urls
1 void add_download(const char *url, int num) { 2 char filename[50]; 3 sprintf(filename, "%d.download", num); 4 FILE *file; 5 6 file = fopen(filename, "w"); 7 if (file == NULL) { 8 fprintf(stderr, "Error opening %s\n", filename); 9 return; 10 } 11 12 CURL *handle = curl_easy_init(); 13 curl_easy_setopt(handle, CURLOPT_WRITEDATA, file); 14 curl_easy_setopt(handle, CURLOPT_URL, url); 15 curl_multi_add_handle(curl_handle, handle); 16 fprintf(stderr, "Added download %s -> %s\n", url, filename); 17 }
我们让libcurl直接将数据写入文件,但是如果您愿意,还可以做更多。
Start_timeout将在libcurl第一次调用时立即被调用,所以事情是动态设置的。这只是启动一个libuv计时器,每当它超时时,它就用CURL_SOCKET_TIMEOUT驱动curl_multi_socket_action。Curl_multi_socket_action驱动libcurl,也就是每当套接字改变状态时所调用的。但在此之前,我们需要在调用handle_socket时轮询socket。
uvwget/main.c - Setting up polling
1 void start_timeout(CURLM *multi, long timeout_ms, void *userp) { 2 if (timeout_ms <= 0) 3 timeout_ms = 1; /* 0 means directly call socket_action, but we'll do it in a bit */ 4 uv_timer_start(&timeout, on_timeout, timeout_ms, 0); 5 } 6 7 int handle_socket(CURL *easy, curl_socket_t s, int action, void *userp, void *socketp) { 8 curl_context_t *curl_context; 9 if (action == CURL_POLL_IN || action == CURL_POLL_OUT) { 10 if (socketp) { 11 curl_context = (curl_context_t*) socketp; 12 } 13 else { 14 curl_context = create_curl_context(s); 15 curl_multi_assign(curl_handle, s, (void *) curl_context); 16 } 17 } 18 19 switch (action) { 20 case CURL_POLL_IN: 21 uv_poll_start(&curl_context->poll_handle, UV_READABLE, curl_perform); 22 break; 23 case CURL_POLL_OUT: 24 uv_poll_start(&curl_context->poll_handle, UV_WRITABLE, curl_perform); 25 break; 26 case CURL_POLL_REMOVE: 27 if (socketp) { 28 uv_poll_stop(&((curl_context_t*)socketp)->poll_handle); 29 destroy_curl_context((curl_context_t*) socketp); 30 curl_multi_assign(curl_handle, s, NULL); 31 } 32 break; 33 default: 34 abort(); 35 } 36 37 return 0; 38 }
我们感兴趣的是套接字fd和动作。对于每个套接字,如果它不存在,我们就创建一个uv_poll_t句柄,并使用curl_multi_assign将它与套接字关联。这样,每当调用回调时,socketp就指向它。
如果下载完成或失败,libcurl请求删除轮询。因此,我们停止并释放poll句柄。
根据libcurl希望观察的事件,我们开始使用UV_READABLE或UV_WRITABLE轮询。现在,只要套接字准备好读写,libuv就会调用轮询回调。在同一个句柄上多次调用uv_poll_start是可以接受的,它只会用新值更新事件掩码。Curl_perform是这个程序的关键。
uvwget/main.c - Driving libcurl.
1 void curl_perform(uv_poll_t *req, int status, int events) { 2 uv_timer_stop(&timeout); 3 int running_handles; 4 int flags = 0; 5 if (status < 0) flags = CURL_CSELECT_ERR; 6 if (!status && events & UV_READABLE) flags |= CURL_CSELECT_IN; 7 if (!status && events & UV_WRITABLE) flags |= CURL_CSELECT_OUT; 8 9 curl_context_t *context; 10 11 context = (curl_context_t*)req; 12 13 curl_multi_socket_action(curl_handle, context->sockfd, flags, &running_handles); 14 check_multi_info(); 15 }
我们要做的第一件事是停止计时器,因为在间隔中有一些进展。然后根据触发回调的事件设置正确的标志。然后调用curl_multi_socket_action来处理正在进行的套接字和通知发生了什么事件的标志。此时,libcurl会以小的增量执行所有内部任务,并试图尽可能快地返回,这正是一个事件程序在其主线程中所希望的。Libcurl继续将消息排队到它自己的传输进度队列中。在我们的例子中,我们只对完成的转账感兴趣。因此,我们提取这些消息,并清除传输完成的句柄。
uvwget/main.c - Reading transfer status.
1 void check_multi_info(void) { 2 char *done_url; 3 CURLMsg *message; 4 int pending; 5 6 while ((message = curl_multi_info_read(curl_handle, &pending))) { 7 switch (message->msg) { 8 case CURLMSG_DONE: 9 curl_easy_getinfo(message->easy_handle, CURLINFO_EFFECTIVE_URL, 10 &done_url); 11 printf("%s DONE\n", done_url); 12 13 curl_multi_remove_handle(curl_handle, message->easy_handle); 14 curl_easy_cleanup(message->easy_handle); 15 break; 16 17 default: 18 fprintf(stderr, "CURLMSG default\n"); 19 abort(); 20 } 21 } 22 }
检查&准备监视器
待续
加载库
libuv提供了一个跨平台API来动态加载共享库。这可以用来实现你自己的插件/扩展/模块系统,node.js用它来实现对绑定的require()支持。
只要您的库导出了正确的符号,用法就非常简单。在加载第三方代码时,要小心完整性和安全性检查,否则您的程序将表现得不可预测。这个例子实现了一个非常简单的插件系统,它除了打印插件的名称外什么也不做。
让我们先看看提供给插件作者的接口。
plugin/plugin.h
1 #ifndef UVBOOK_PLUGIN_SYSTEM 2 #define UVBOOK_PLUGIN_SYSTEM 3 4 // Plugin authors should use this to register their plugins with mfp. 5 void mfp_register(const char *name); 6 7 #endif
同样,你也可以添加更多的函数,插件作者可以使用这些函数在你的应用2中做一些有用的事情。一个使用此API的示例插件是:
plugin/hello.c
1 #include "plugin.h" 2 3 void initialize() { 4 mfp_register("Hello World!"); 5 }
我们的接口定义了所有插件都应该有一个初始化函数,该函数将被应用程序调用。这个插件被编译为一个共享库,可以通过运行我们的应用程序来加载:
$ ./plugin libhello.dylib Loading libhello.dylib Registered plugin "Hello World!"
- 注意:共享库文件名因平台而异。在Linux上是libhello.so。
这是通过使用uv_dlopen首先加载共享库libhello.dylib来完成的。然后使用uv_dlsym访问初始化函数并调用它。
plugin/main.c
1 #include "plugin.h" 2 3 typedef void (*init_plugin_function)(); 4 5 void mfp_register(const char *name) { 6 fprintf(stderr, "Registered plugin \"%s\"\n", name); 7 } 8 9 int main(int argc, char **argv) { 10 if (argc == 1) { 11 fprintf(stderr, "Usage: %s [plugin1] [plugin2] ...\n", argv[0]); 12 return 0; 13 } 14 15 uv_lib_t *lib = (uv_lib_t*) malloc(sizeof(uv_lib_t)); 16 while (--argc) { 17 fprintf(stderr, "Loading %s\n", argv[argc]); 18 if (uv_dlopen(argv[argc], lib)) { 19 fprintf(stderr, "Error: %s\n", uv_dlerror(lib)); 20 continue; 21 } 22 23 init_plugin_function init_plugin; 24 if (uv_dlsym(lib, "initialize", (void **) &init_plugin)) { 25 fprintf(stderr, "dlsym error: %s\n", uv_dlerror(lib)); 26 continue; 27 } 28 29 init_plugin(); 30 } 31 32 return 0; 33 }
Uv_dlopen期望一个到共享库的路径,并设置不透明的uv_lib_t指针。成功时返回0,错误时返回-1。使用uv_dlerror获取错误消息。
Uv_dlsym在第三个参数中存储一个指向第二个参数中的符号的指针。Init_plugin_function是一个函数指针,指向我们在应用程序插件中寻找的那种函数。
TTY
文本终端支持基本格式已经有很长一段时间了,使用的是相当标准化的命令集。程序经常使用这种格式来提高终端输出的可读性。例如grep——colour。libuv提供了uv_tty_t抽象(一个流)和相关函数来实现跨所有平台的ANSI转义代码。我的意思是,libuv将ANSI代码转换为Windows等效代码,并提供获取终端信息的函数。
要做的第一件事是用uv_tty_t读写的文件描述符初始化uv_tty_t。这是通过以下方法实现的:
int uv_tty_init(uv_loop_t*, uv_tty_t*, uv_file fd, int unused)
现在自动检测并忽略未使用的参数。以前需要将它设置为在流上使用uv_read_start()。
因此,最好使用uv_tty_set_mode将模式设置为普通模式,以支持大多数TTY格式、流控制和其他设置。其他模式也可以使用。
记得在程序退出时调用uv_tty_reset_mode来恢复终端状态。只是很有礼貌。另一种礼貌是意识到“重定向”。如果用户将命令的输出重定向到一个文件,则不应该写入控制序列,因为它们会影响可读性和grep。要检查文件描述符是否确实是TTY,请使用文件描述符调用uv_guess_handle,并将返回值与UV_TTY进行比较。
下面是一个在红色背景上打印白色文本的简单例子:
tty/main.c
1 #include <stdio.h> 2 #include <string.h> 3 #include <unistd.h> 4 #include <uv.h> 5 6 uv_loop_t *loop; 7 uv_tty_t tty; 8 int main() { 9 loop = uv_default_loop(); 10 11 uv_tty_init(loop, &tty, STDOUT_FILENO, 0); 12 uv_tty_set_mode(&tty, UV_TTY_MODE_NORMAL); 13 14 if (uv_guess_handle(1) == UV_TTY) { 15 uv_write_t req; 16 uv_buf_t buf; 17 buf.base = "\033[41;37m"; 18 buf.len = strlen(buf.base); 19 uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL); 20 } 21 22 uv_write_t req; 23 uv_buf_t buf; 24 buf.base = "Hello TTY\n"; 25 buf.len = strlen(buf.base); 26 uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL); 27 uv_tty_reset_mode(); 28 return uv_run(loop, UV_RUN_DEFAULT); 29 }
最后一个TTY助手是uv_tty_get_winsize(),它用于获取终端的宽度和高度,成功时返回0。下面是一个小程序,它使用函数和字符位置转义代码做一些动画。
tty-gravity/main.c
1 #include <stdio.h> 2 #include <string.h> 3 #include <unistd.h> 4 #include <uv.h> 5 6 uv_loop_t *loop; 7 uv_tty_t tty; 8 uv_timer_t tick; 9 uv_write_t write_req; 10 int width, height; 11 int pos = 0; 12 char *message = " Hello TTY "; 13 14 void update(uv_timer_t *req) { 15 char data[500]; 16 17 uv_buf_t buf; 18 buf.base = data; 19 buf.len = sprintf(data, "\033[2J\033[H\033[%dB\033[%luC\033[42;37m%s", 20 pos, 21 (unsigned long) (width-strlen(message))/2, 22 message); 23 uv_write(&write_req, (uv_stream_t*) &tty, &buf, 1, NULL); 24 25 pos++; 26 if (pos > height) { 27 uv_tty_reset_mode(); 28 uv_timer_stop(&tick); 29 } 30 } 31 32 int main() { 33 loop = uv_default_loop(); 34 35 uv_tty_init(loop, &tty, STDOUT_FILENO, 0); 36 uv_tty_set_mode(&tty, 0); 37 38 if (uv_tty_get_winsize(&tty, &width, &height)) { 39 fprintf(stderr, "Could not get TTY information\n"); 40 uv_tty_reset_mode(); 41 return 1; 42 } 43 44 fprintf(stderr, "Width %d, height %d\n", width, height); 45 uv_timer_init(loop, &tick); 46 uv_timer_start(&tick, update, 200, 200); 47 return uv_run(loop, UV_RUN_DEFAULT); 48 }
转义码为:
代码 含义
2 J 清除屏幕的一部分,2是整个屏幕
H 移动光标到特定位置,默认为左上角
n B 向下移动光标n行
n C 向右移动光标n列
m 服从显示设置字符串,在这种情况下绿色背景(40+2),白色文本(30+7)
正如你所看到的,这对于生成格式化良好的输出是非常有用的,甚至是基于主机的街机游戏,如果你喜欢的话。为了更好的控制,您可以尝试ncurses。
在1.23.1版更改::readable形参现在未使用并被忽略。现在将从内核自动检测到适当的值。

浙公网安备 33010602011771号