#Utilities
本章介紹的工具和技術對于常見的任務非常的實用。libuv吸收了[libev用戶手冊頁](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#COMMON_OR_USEFUL_IDIOMS_OR_BOTH)中所涵蓋的一些模式,并在此基礎上對API做了少許的改動。本章還包含了一些無需用完整的一章來介紹的libuv API。
##Timers
在定時器啟動后的特定時間后,定時器會調用回調函數。libuv的定時器還可以設定為,按時間間隔定時啟動,而不是只啟動一次。
可以簡單地使用超時時間`timeout`作為參數初始化一個定時器,還有一個可選參數`repeat`。定時器能在任何時間被終止。
```c
uv_timer_t timer_req;
uv_timer_init(loop, &timer_req);
uv_timer_start(&timer_req, callback, 5000, 2000);
```
上述操作會啟動一個循環定時器(repeating timer),它會在調用`uv_timer_start`后,5秒(timeout)啟動回調函數,然后每隔2秒(repeat)循環啟動回調函數。你可以使用:
```c
uv_timer_stop(&timer_req);
```
來停止定時器。這個函數也可以在回調函數中安全地使用。
循環的間隔也可以隨時定義,使用:
```c
uv_timer_set_repeat(uv_timer_t *timer, int64_t repeat);
```
它會在**可能的時候**發揮作用。如果上述函數是在定時器回調函數中調用的,這意味著:
>* 如果定時器未設置為循環,這意味著定時器已經停止。需要先用`uv_timer_start`重新啟動。
* 如果定時器被設置為循環,那么下一次超時的時間已經被規劃好了,所以在切換到新的間隔之前,舊的間隔還會發揮一次作用。
函數:
```c
int uv_timer_again(uv_timer_t *)
```
**只適用于循環定時器**,相當于停止定時器,然后把原先的`timeout`和`repeat`值都設置為之前的`repeat`值,啟動定時器。如果當該函數調用時,定時器未啟動,則調用失敗(錯誤碼為`UV_EINVAL`)并且返回-1。
下面的一節會出現使用定時器的例子。
##Event loop reference count
event-loop在沒有了活躍的handle之后,便會終止。整套系統的工作方式是:在handle增加時,event-loop的引用計數加1,在handle停止時,引用計數減少1。當然,libuv也允許手動地更改引用計數,通過使用:
```c
void uv_ref(uv_handle_t*);
void uv_unref(uv_handle_t*);
```
這樣,就可以達到允許loop即使在有正在活動的定時器時,仍然能夠推出。或者是使用自定義的uv_handle_t對象來使得loop保持工作。
第二個函數可以和間隔循環定時器結合使用。你會有一個每隔x秒執行一次的垃圾回收器,或者是你的網絡服務器會每隔一段時間向其他人發送一次心跳信號,但是你不想只有在所有垃圾回收完或者出現錯誤時才能停止他們。如果你想要在你其他的監視器都退出后,終止程序。這時你就可以立即unref定時器,即便定時器這時是loop上唯一還在運行的監視器,你依舊可以停止`uv_run()`。
它們同樣會出現在node.js中,如js的API中封裝的libuv方法。每一個js的對象產生一個`uv_handle_t`(所有監視器的超類),同樣可以被uv_ref和uv_unref。
####ref-timer/main.c
```c
uv_loop_t *loop;
uv_timer_t gc_req;
uv_timer_t fake_job_req;
int main() {
loop = uv_default_loop();
uv_timer_init(loop, &gc_req);
uv_unref((uv_handle_t*) &gc_req);
uv_timer_start(&gc_req, gc, 0, 2000);
// could actually be a TCP download or something
uv_timer_init(loop, &fake_job_req);
uv_timer_start(&fake_job_req, fake_job, 9000, 0);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
首先初始化垃圾回收器的定時器,然后在立刻`unref`它。注意觀察9秒之后,此時fake_job完成,程序會自動退出,即使垃圾回收器還在運行。
##Idler pattern
空轉的回調函數會在每一次的event-loop循環激發一次。空轉的回調函數可以用來執行一些優先級較低的活動。比如,你可以向開發者發送應用程序的每日性能表現情況,以便于分析,或者是使用用戶應用cpu時間來做[SETI](http://www.seti.org)運算:)。空轉程序還可以用于GUI應用。比如你在使用event-loop來下載文件,如果tcp連接未中斷而且當前并沒有其他的事件,則你的event-loop會阻塞,這也就意味著你的下載進度條會停滯,用戶會面對一個無響應的程序。面對這種情況,空轉監視器可以保持UI可操作。
####idle-compute/main.c
```c
uv_loop_t *loop;
uv_fs_t stdin_watcher;
uv_idle_t idler;
char buffer[1024];
int main() {
loop = uv_default_loop();
uv_idle_init(loop, &idler);
uv_buf_t buf = uv_buf_init(buffer, 1024);
uv_fs_read(loop, &stdin_watcher, 0, &buf, 1, -1, on_type);
uv_idle_start(&idler, crunch_away);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
上述程序中,我們將空轉監視器和我們真正關心的事件排在一起。`crunch_away`會被循環地調用,直到輸入字符并回車。然后程序會被中斷很短的時間,用來處理數據讀取,然后在接著調用空轉的回調函數。
####idle-compute/main.c
```c
void crunch_away(uv_idle_t* handle) {
// Compute extra-terrestrial life
// fold proteins
// computer another digit of PI
// or similar
fprintf(stderr, "Computing PI...\n");
// just to avoid overwhelming your terminal emulator
uv_idle_stop(handle);
}
```
##Passing data to worker thread
在使用`uv_queue_work`的時候,你通常需要給工作線程傳遞復雜的數據。解決方案是自定義struct,然后使用`uv_work_t.data`指向它。一個稍微的不同是必須讓`uv_work_t`作為這個自定義struct的成員之一(把這叫做接力棒)。這么做就可以使得,同時回收數據和`uv_wortk_t`。
```c
struct ftp_baton {
uv_work_t req;
char *host;
int port;
char *username;
char *password;
}
```
```c
ftp_baton *baton = (ftp_baton*) malloc(sizeof(ftp_baton));
baton->req.data = (void*) baton;
baton->host = strdup("my.webhost.com");
baton->port = 21;
// ...
uv_queue_work(loop, &baton->req, ftp_session, ftp_cleanup);
```
現在我們創建完了接力棒,并把它排入了隊列中。
現在就可以隨性所欲地獲取自己想要的數據啦。
```c
void ftp_session(uv_work_t *req) {
ftp_baton *baton = (ftp_baton*) req->data;
fprintf(stderr, "Connecting to %s\n", baton->host);
}
void ftp_cleanup(uv_work_t *req) {
ftp_baton *baton = (ftp_baton*) req->data;
free(baton->host);
// ...
free(baton);
}
```
我們既回收了接力棒,同時也回收了監視器。
##External I/O with polling
通常在使用第三方庫的時候,需要應對他們自己的IO,還有保持監視他們的socket和內部文件。在此情形下,不可能使用標準的IO流操作,但第三方庫仍然能整合進event-loop中。所有這些需要的就是,第三方庫就必須允許你訪問它的底層文件描述符,并且提供可以處理有用戶定義的細微任務的函數。但是一些第三庫并不允許你這么做,他們只提供了一個標準的阻塞IO函數,此函數會完成所有的工作并返回。在event-loop的線程直接使用它們是不明智的,而是應該使用libuv的工作線程。當然,這也意味著失去了對第三方庫的顆粒化控制。
libuv的`uv_poll`簡單地監視了使用了操作系統的監控機制的文件描述符。從某方面說,libuv實現的所有的IO操作,的背后均有`uv_poll`的支持。無論操作系統何時監視到文件描述符的改變,libuv都會調用響應的回調函數。
現在我們簡單地實現一個下載管理程序,它會通過[libcurl](http://curl.haxx.se/libcurl/)來下載文件。我們不會直接控制libcurl,而是使用libuv的event-loop,通過非阻塞的異步的[多重接口](http://curl.haxx.se/libcurl/c/libcurl-multi.html)來處理下載,與此同時,libuv會監控IO的就緒狀態。
####uvwget/main.c - The setup
```c
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
#include <curl/curl.h>
uv_loop_t *loop;
CURLM *curl_handle;
uv_timer_t timeout;
}
int main(int argc, char **argv) {
loop = uv_default_loop();
if (argc <= 1)
return 0;
if (curl_global_init(CURL_GLOBAL_ALL)) {
fprintf(stderr, "Could not init cURL\n");
return 1;
}
uv_timer_init(loop, &timeout);
curl_handle = curl_multi_init();
curl_multi_setopt(curl_handle, CURLMOPT_SOCKETFUNCTION, handle_socket);
curl_multi_setopt(curl_handle, CURLMOPT_TIMERFUNCTION, start_timeout);
while (argc-- > 1) {
add_download(argv[argc], argc);
}
uv_run(loop, UV_RUN_DEFAULT);
curl_multi_cleanup(curl_handle);
return 0;
}
```
每種庫整合進libuv的方式都是不同的。以libcurl的例子來說,我們注冊了兩個回調函數。socket回調函數`handle_socket`會在socket狀態改變的時候被觸發,因此我們不得不開始輪詢它。`start_timeout`是libcurl用來告知我們下一次的超時間隔的,之后我們就應該不管當前IO狀態,驅動libcurl向前。這些也就是libcurl能處理錯誤或驅動下載進度向前的原因。
可以這么調用下載器:
```
$ ./uvwget [url1] [url2] ...
```
我們可以把url當成參數傳入程序。
####uvwget/main.c - Adding urls
```c
void add_download(const char *url, int num) {
char filename[50];
sprintf(filename, "%d.download", num);
FILE *file;
file = fopen(filename, "w");
if (file == NULL) {
fprintf(stderr, "Error opening %s\n", filename);
return;
}
CURL *handle = curl_easy_init();
curl_easy_setopt(handle, CURLOPT_WRITEDATA, file);
curl_easy_setopt(handle, CURLOPT_URL, url);
curl_multi_add_handle(curl_handle, handle);
fprintf(stderr, "Added download %s -> %s\n", url, filename);
}
```
我們允許libcurl直接向文件寫入數據。
`start_timeout`會被libcurl立即調用。它會啟動一個libuv的定時器,使用`CURL_SOCKET_TIMEOUT`驅動`curl_multi_socket_action`,當其超時時,調用它。`curl_multi_socket_action`會驅動libcurl,也會在socket狀態改變的時候被調用。但在我們深入講解它之前,我們需要輪詢監聽socket,等待`handle_socket`被調用。
####uvwget/main.c - Setting up polling
```c
void start_timeout(CURLM *multi, long timeout_ms, void *userp) {
if (timeout_ms <= 0)
timeout_ms = 1; /* 0 means directly call socket_action, but we'll do it in a bit */
uv_timer_start(&timeout, on_timeout, timeout_ms, 0);
}
int handle_socket(CURL *easy, curl_socket_t s, int action, void *userp, void *socketp) {
curl_context_t *curl_context;
if (action == CURL_POLL_IN || action == CURL_POLL_OUT) {
if (socketp) {
curl_context = (curl_context_t*) socketp;
}
else {
curl_context = create_curl_context(s);
curl_multi_assign(curl_handle, s, (void *) curl_context);
}
}
switch (action) {
case CURL_POLL_IN:
uv_poll_start(&curl_context->poll_handle, UV_READABLE, curl_perform);
break;
case CURL_POLL_OUT:
uv_poll_start(&curl_context->poll_handle, UV_WRITABLE, curl_perform);
break;
case CURL_POLL_REMOVE:
if (socketp) {
uv_poll_stop(&((curl_context_t*)socketp)->poll_handle);
destroy_curl_context((curl_context_t*) socketp);
curl_multi_assign(curl_handle, s, NULL);
}
break;
default:
abort();
}
return 0;
}
```
我們關心的是socket的文件描述符s,還有action。對應每一個socket,我們都創造了`uv_poll_t`,并用`curl_multi_assign`把它們關聯起來。每當回調函數被調用時,`socketp`都會指向它。
在下載完成或失敗后,libcurl需要移除poll。所以我們停止并回收了poll的handle。
我們使用`UV_READABLE`或`UV_WRITABLE`開始輪詢,基于libcurl想要監視的事件。當socket已經準備好讀或寫后,libuv會調用輪詢的回調函數。在相同的handle上調用多次`uv_poll_start`是被允許的,這么做可以更新事件的參數。`curl_perform`是整個程序的關鍵。
####uvwget/main.c - Driving libcurl.
```c
void curl_perform(uv_poll_t *req, int status, int events) {
uv_timer_stop(&timeout);
int running_handles;
int flags = 0;
if (status < 0) flags = CURL_CSELECT_ERR;
if (!status && events & UV_READABLE) flags |= CURL_CSELECT_IN;
if (!status && events & UV_WRITABLE) flags |= CURL_CSELECT_OUT;
curl_context_t *context;
context = (curl_context_t*)req;
curl_multi_socket_action(curl_handle, context->sockfd, flags, &running_handles);
check_multi_info();
}
```
首先我們要做的是停止定時器,因為內部還有其他要做的事。接下來我們我們依據觸發回調函數的事件,來設置flag。然后,我們使用上述socket和flag作為參數,來調用`curl_multi_socket_action`。在此刻libcurl會在內部完成所有的工作,然后盡快地返回事件驅動程序在主線程中急需的數據。libcurl會在自己的隊列中將傳輸進度的消息排隊。對于我們來說,我們只關心是否傳輸完成,這類消息。所以我們將這類消息提取出來,并將傳輸完成的handle回收。
####uvwget/main.c - Reading transfer status.
```c
void check_multi_info(void) {
char *done_url;
CURLMsg *message;
int pending;
while ((message = curl_multi_info_read(curl_handle, &pending))) {
switch (message->msg) {
case CURLMSG_DONE:
curl_easy_getinfo(message->easy_handle, CURLINFO_EFFECTIVE_URL,
&done_url);
printf("%s DONE\n", done_url);
curl_multi_remove_handle(curl_handle, message->easy_handle);
curl_easy_cleanup(message->easy_handle);
break;
default:
fprintf(stderr, "CURLMSG default\n");
abort();
}
}
}
```
##Loading libraries
libuv提供了一個跨平臺的API來加載[共享庫shared libraries](http://liaoph.com/linux-shared-libary/)。這就可以用來實現你自己的插件/擴展/模塊系統,它們可以被nodejs通過`require()`調用。只要你的庫輸出的是正確的符號,用起來還是很簡單的。在載入第三方庫的時候,要注意錯誤和安全檢查,否則你的程序就會表現出不可預測的行為。下面這個例子實現了一個簡單的插件,它只是打印出了自己的名字。
首先看下提供給插件作者的接口。
####plugin/plugin.h
```c
#ifndef UVBOOK_PLUGIN_SYSTEM
#define UVBOOK_PLUGIN_SYSTEM
// Plugin authors should use this to register their plugins with mfp.
void mfp_register(const char *name);
#endif
```
你可以在你的程序中給插件添加更多有用的功能(mfp is My Fancy Plugin)。使用了這個api的插件的例子:
####plugin/hello.c
```c
#include "plugin.h"
void initialize() {
mfp_register("Hello World!");
}
```
我們的接口定義了,所有的插件都應該有一個能被程序調用的`initialize`函數。這個插件被編譯成了共享庫,因此可以被我們的程序在運行的時候載入。
```
$ ./plugin libhello.dylib
Loading libhello.dylib
Registered plugin "Hello World!"
```
#####Note
>共享庫的后綴名在不同平臺上是不一樣的。在Linux上是libhello.so。
使用`uv_dlopen`首先載入了共享庫`libhello.dylib`。再使用`uv_dlsym`獲取了該插件的`initialize`函數,最后在調用它。
####plugin/main.c
```c
#include "plugin.h"
typedef void (*init_plugin_function)();
void mfp_register(const char *name) {
fprintf(stderr, "Registered plugin \"%s\"\n", name);
}
int main(int argc, char **argv) {
if (argc == 1) {
fprintf(stderr, "Usage: %s [plugin1] [plugin2] ...\n", argv[0]);
return 0;
}
uv_lib_t *lib = (uv_lib_t*) malloc(sizeof(uv_lib_t));
while (--argc) {
fprintf(stderr, "Loading %s\n", argv[argc]);
if (uv_dlopen(argv[argc], lib)) {
fprintf(stderr, "Error: %s\n", uv_dlerror(lib));
continue;
}
init_plugin_function init_plugin;
if (uv_dlsym(lib, "initialize", (void **) &init_plugin)) {
fprintf(stderr, "dlsym error: %s\n", uv_dlerror(lib));
continue;
}
init_plugin();
}
return 0;
}
```
函數`uv_dlopen`需要傳入一個共享庫的路徑作為參數。當它成功時返回0,出錯時返回-1。使用`uv_dlerror`可以獲取出錯的消息。
`uv_dlsym`的第三個參數保存了一個指向第二個參數所保存的函數的指針。`init_plugin_function`是一個函數的指針,它指向了我們所需要的程序插件的函數。
##TTY
文字終端長期支持非常標準化的[控制序列](https://en.wikipedia.org/wiki/ANSI_escape_code)。它經常被用來增強終端輸出的可讀性。例如`grep --colour`。libuv提供了跨平臺的,`uv_tty_t`抽象(stream)和相關的處理ANSI escape codes 的函數。這也就是說,libuv同樣在Windows上實現了對等的ANSI codes,并且提供了獲取終端信息的函數。
首先要做的是,使用讀/寫文件描述符來初始化`uv_tty_t`。如下:
```c
int uv_tty_init(uv_loop_t*, uv_tty_t*, uv_file fd, int readable)
```
設置`readable`為true,意味著你打算使用`uv_read_start`從stream從中讀取數據。
最好還要使用`uv_tty_set_mode`來設置其為正常模式。也就是運行大多數的TTY格式,流控制和其他的設置。其他的模式還有[這些](http://docs.libuv.org/en/v1.x/tty.html#c.uv_tty_mode_t)。
記得當你的程序退出后,要使用`uv_tty_reset_mode`恢復終端的狀態。這才是禮貌的做法。另外要注意禮貌的地方是關心重定向。如果使用者將你的命令的輸出重定向到文件,控制序列不應該被重寫,因為這會阻礙可讀性和grep。為了保證文件描述符確實是TTY,可以使用`uv_guess_handle`函數,比較返回值是否為`UV_TTY`。
下面是一個把白字打印到紅色背景上的例子。
####tty/main.c
```c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <uv.h>
uv_loop_t *loop;
uv_tty_t tty;
int main() {
loop = uv_default_loop();
uv_tty_init(loop, &tty, 1, 0);
uv_tty_set_mode(&tty, UV_TTY_MODE_NORMAL);
if (uv_guess_handle(1) == UV_TTY) {
uv_write_t req;
uv_buf_t buf;
buf.base = "\033[41;37m";
buf.len = strlen(buf.base);
uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
}
uv_write_t req;
uv_buf_t buf;
buf.base = "Hello TTY\n";
buf.len = strlen(buf.base);
uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
uv_tty_reset_mode();
return uv_run(loop, UV_RUN_DEFAULT);
}
```
最后要說的是`uv_tty_get_winsize()`,它能獲取到終端的寬和長,當成功獲取后返回0。下面這個小程序實現了一個動畫的效果。
####tty-gravity/main.c
```c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <uv.h>
uv_loop_t *loop;
uv_tty_t tty;
uv_timer_t tick;
uv_write_t write_req;
int width, height;
int pos = 0;
char *message = " Hello TTY ";
void update(uv_timer_t *req) {
char data[500];
uv_buf_t buf;
buf.base = data;
buf.len = sprintf(data, "\033[2J\033[H\033[%dB\033[%luC\033[42;37m%s",
pos,
(unsigned long) (width-strlen(message))/2,
message);
uv_write(&write_req, (uv_stream_t*) &tty, &buf, 1, NULL);
pos++;
if (pos > height) {
uv_tty_reset_mode();
uv_timer_stop(&tick);
}
}
int main() {
loop = uv_default_loop();
uv_tty_init(loop, &tty, 1, 0);
uv_tty_set_mode(&tty, 0);
if (uv_tty_get_winsize(&tty, &width, &height)) {
fprintf(stderr, "Could not get TTY information\n");
uv_tty_reset_mode();
return 1;
}
fprintf(stderr, "Width %d, height %d\n", width, height);
uv_timer_init(loop, &tick);
uv_timer_start(&tick, update, 200, 200);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
escape codes的對應表如下:
代碼 | 意義
------------ | -------------
2 J | Clear part of the screen, 2 is entire screen
H | Moves cursor to certain position, default top-left
n B | Moves cursor down by n lines
n C | Moves cursor right by n columns
m | Obeys string of display settings, in this case green background (40+2), white text (30+7)
正如你所見,它能輸出酷炫的效果,你甚至可以發揮想象,用它來制作電子游戲。更有趣的輸出,可以使用`http://www.gnu.org/software/ncurses/ncurses.html`。