之前分析了unix域在libuv的基本原理。今天以一個簡單的例子看一下如何使用它。本文涉及到一些網絡編程的知識,不過文章不打算講解這些,如果不瞭解可以先了解一下,或者留言。
void remove_sock(int sig) {
uv_fs_t req;
// 刪除unix域對應的路徑
uv_fs_unlink(loop, &req, PIPENAME, NULL);
// 退出進程
exit(0);
}
int main() {
loop = uv_default_loop();
uv_pipe_t server;
uv_pipe_init(loop, &server, 0);
// 註冊SIGINT信號的信號處理函數是remove_sock
signal(SIGINT, remove_sock);
int r;
// 綁定unix路徑到socket
if ((r = uv_pipe_bind(&server, PIPENAME))) {
fprintf(stderr, "Bind error %s\n", uv_err_name(r));
return 1;
}
/*
把unix域對應的文件文件描述符設置爲listen狀態。
開啓監聽請求的到來,連接的最大個數是128。有連接時的回調是on_new_connection
*/
if ((r = uv_listen((uv_stream_t*) &server, 128, on_new_connection))) {
fprintf(stderr, "Listen error %s\n", uv_err_name(r));
return 2;
}
// 啓動事件循環
return uv_run(loop, UV_RUN_DEFAULT);
}
對於看過之前的文章或者瞭解網絡編程的同學來說。上面的代碼看起來會比較簡單。所以就不具體分析。他執行完後就是啓動了一個服務。同主機的進程可以訪問(連接)他。之前說過unix域的實現和tcp的實現類型。都是基於連接的模式。服務器啓動等待連接,客戶端去連接。然後服務器逐個摘下連接的節點進行處理。我們從處理連接的函數on_new_connection開始分析整個流程。
// 有連接到來時的回調
void on_new_connection(uv_stream_t *server, int status) {
// 有連接到來,申請一個結構體表示他
uv_pipe_t *client = (uv_pipe_t*) malloc(sizeof(uv_pipe_t));
uv_pipe_init(loop, client, 0);
// 把accept返回的fd記錄到client,client是用於和客戶端通信的結構體
if (uv_accept(server, (uv_stream_t*) client) == 0) {
/*
註冊讀事件,等待客戶端發送信息過來,
alloc_buffer分配內存保存客戶端的發送過來的信息,
echo_read是回調
*/
uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);
}
else {
uv_close((uv_handle_t*) client, NULL);
}
}
分析on_new_connection之前,我們先看一下該函數的執行時機。該函數是在uv__server_io函數中被執行,而uv__server_io是在監聽的socket(即listen的那個)有可讀事件時觸發的回調。我們看看uv__server_io的部分邏輯。
// 有連接到來,進行accept
err = uv__accept(uv__stream_fd(stream));
// 保存通信socket對應的文件描述符
stream->accepted_fd = err;
/*
有連接,執行上層回調,connection_cb一般會調用uv_accept消費accepted_fd。
然後重新註冊等待可讀事件
*/
stream->connection_cb(stream, 0);
當有連接到來時,服務器調用uv__accept摘取一個連接節點(實現上,操作系統會返回一個文件描述符,作用類似一個id)。然後把文件描述符保存到accepted_fd字段,接着執行connection_cb回調。就是我們設置的on_new_connection。
uv__stream_fd(stream)是我們啓動的服務器對應的文件描述符。stream就是表示服務器的結構體。在unix域裏,他實際上是一個uv_pipe_s結構體。uv_stream_s是uv_pipe_s的父類。類似c++的繼承。
我們回頭看一下on_new_connection的代碼。主要邏輯如下。
1 申請一個uv_pipe_t結構體用於保存和客戶端通信的信息。
2 執行uv_accept
3 執行uv_read_start開始等待數據的到來,然後讀取數據。
我們分析一下2和3。我們看一下uv_accept的主要邏輯。
switch (client->type) {
case UV_NAMED_PIPE:
// 設置流的標記,保存文件描述符到流上
uv__stream_open(
client,server->accepted_fd,
UV_HANDLE_READABLE | UV_HANDLE_WRITABLE
);
}
uv_accept中把剛纔accept到的文件描述符保存到client中。這樣我們後續就可以通過client和客戶端通信。至於uv_read_start,之前在stream的文章中已經分析過。就不再深入分析。我們主要分析echo_read。echo_read在客戶端給服務器發送信息時被觸發。
void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
// 有數據,則回寫
if (nread > 0) {
write_req_t *req = (write_req_t*) malloc(sizeof(write_req_t));
// 指向客戶端發送過來的數據
req->buf = uv_buf_init(buf->base, nread);
// 回寫給客戶端,echo_write是寫成功後的回調
uv_write((uv_write_t*) req, client, &req->buf, 1, echo_write);
return;
}
// 沒有數據了,關閉
if (nread < 0) {
if (nread != UV_EOF)
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
// 銷燬和客戶端通信的結構體,即關閉通信
uv_close((uv_handle_t*) client, NULL);
}
free(buf->base);
}
沒有數據的時候,直接銷燬和客戶端通信的結構體和撤銷結構體對應的讀寫事件。我們主要分析有數據時的處理邏輯。當有數據到來時,服務器調用uv_write對數據進行回寫。我們看到uv_write的第二個參數是client。即往client對應的文件描述符中寫數據。也就是往客戶端寫。uv_write的邏輯在stream中已經分析過,所以也不打算深入分析。主要邏輯就是在client對應的stream上寫入數據,緩存起來,然後等待可寫時,再寫到對端。寫完成後執行echo_write釋放數據佔據的內存。這就是使用unix域通信的整個過程。unix域還有一個複雜的應用是涉及到傳遞文件描述符。即uv_pipe_s的ipc字段。這個後續再開一篇文章分析。