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形参现在未使用并被忽略。现在将从内核自动检测到适当的值。

posted @ 2021-05-11 20:35  风吹大风车  阅读(471)  评论(0)    收藏  举报