unix域是一種基於單主機的進程間通信方式。實現模式類似tcp通信。今天先分析他的實現,後續會分析他的使用。在libuv中,unix域用uv_pipe_t表示。
struct uv_stream_s {
// uv_handle_s的字段
void* data;
// 所屬事件循環
uv_loop_t* loop;
// handle類型
uv_handle_type type;
// 關閉handle時的回調
uv_close_cb close_cb;
// 用於插入事件循環的handle隊列
void* handle_queue[2];
union {
int fd;
void* reserved[4];
} u;
// 用於插入事件循環的closing階段對應的隊列
uv_handle_t* next_closing;
// 各種標記
unsigned int flags;
// 流拓展的字段
// 用戶寫入流的字節大小,流緩存用戶的輸入,然後等到可寫的時候才做真正的寫
size_t write_queue_size;
// 分配內存的函數,內存由用戶定義,主要用來保存讀取的數據
uv_alloc_cb alloc_cb;
// 讀取數據的回調
uv_read_cb read_cb;
// 連接成功後,執行connect_req的回調(connect_req在uv__xxx_connect中賦值)
uv_connect_t *connect_req;
// 關閉寫端的時候,發送完緩存的數據,執行shutdown_req的回調(shutdown_req在uv_shutdown的時候賦值)
uv_shutdown_t *shutdown_req;
// 流對應的io觀察者,即文件描述符+一個文件描述符事件觸發時執行的回調
uv__io_t io_watcher;
// 流緩存下來的,待寫的數據
void* write_queue[2];
// 已經完成了數據寫入的隊列
void* write_completed_queue[2];
// 完成三次握手後,執行的回調
uv_connection_cb connection_cb;
// 操作流時出錯碼
int delayed_error;
// accept返回的通信socket對應的文件描述符
int accepted_fd;
// 同上,用於緩存更多的通信socket對應的文件描述符
void* queued_fds;
// 標記管道是否能在進程間傳遞
int ipc;
// 用於unix域通信的文件路徑
const char* pipe_fname;
}
unix域繼承域handle和stream。下面看一下他的具體實現邏輯。
int uv_pipe_init(uv_loop_t* loop, uv_pipe_t* handle, int ipc) {
uv__stream_init(loop, (uv_stream_t*)handle, UV_NAMED_PIPE);
handle->shutdown_req = NULL;
handle->connect_req = NULL;
handle->pipe_fname = NULL;
handle->ipc = ipc;
return 0;
}
uv_pipe_init邏輯很簡單,就是初始化uv_pipe_t結構體。剛纔已經見過uv_pipe_t繼承於stream,uv__stream_init就是初始化stream(父類)的字段。文章開頭說過,unix域的實現類似tcp的實現。遵循網絡socket編程那一套。服務端使用bind,listen等函數啓動服務。
// name是unix域的文件路徑
int uv_pipe_bind(uv_pipe_t* handle, const char* name) {
struct sockaddr_un saddr;
const char* pipe_fname;
int sockfd;
int err;
pipe_fname = NULL;
pipe_fname = uv__strdup(name);
name = NULL;
// unix域套接字
sockfd = uv__socket(AF_UNIX, SOCK_STREAM, 0);
memset(&saddr, 0, sizeof saddr);
strncpy(saddr.sun_path, pipe_fname, sizeof(saddr.sun_path) - 1);
saddr.sun_path[sizeof(saddr.sun_path) - 1] = '\0';
saddr.sun_family = AF_UNIX;
// 綁定到路徑,tcp是綁定到ip和端口
if (bind(sockfd, (struct sockaddr*)&saddr, sizeof saddr)) {
// ...
}
// 已經綁定
handle->flags |= UV_HANDLE_BOUND;
handle->pipe_fname = pipe_fname;
// 保存socket fd,用於後面監聽
handle->io_watcher.fd = sockfd;
return 0;
}
uv_pipe_bind函數的邏輯也比較簡單,就是類似tcp的bind行爲。
1 申請一個socket套接字。
2 綁定unix域路徑到socket中。
綁定了路徑後,就可以調用listen函數開始監聽。
int uv_pipe_listen(uv_pipe_t* handle, int backlog, uv_connection_cb cb) {
if (uv__stream_fd(handle) == -1)
return UV_EINVAL;
// uv__stream_fd(handle)得到bind函數中獲取的socket
if (listen(uv__stream_fd(handle), backlog))
return UV__ERR(errno);
// 保存回調,有進程調用connect的時候時觸發,由uv__server_io函數觸發
handle->connection_cb = cb;
// io觀察者的回調,有進程調用connect的時候時觸發(io觀察者的fd在init函數裏設置了)
handle->io_watcher.cb = uv__server_io;
// 註冊io觀察者到libuv,等待連接,即讀事件到來
uv__io_start(handle->loop, &handle->io_watcher, POLLIN);
return 0;
}
uv_pipe_listen執行listen函數使得socket成爲監聽型的套接字。然後把socket對應的文件描述符和回調封裝成io觀察者。註冊到libuv。等到有讀事件到來(有連接到來)。就會執行uv__server_io函數,摘下對應的客戶端節點。最後執行connection_cb回調。
這時候,使用unix域成功啓動了一個服務。接下來就是看客戶端的邏輯。
void uv_pipe_connect(uv_connect_t* req,
uv_pipe_t* handle,
const char* name,
uv_connect_cb cb) {
struct sockaddr_un saddr;
int new_sock;
int err;
int r;
// 判斷是否已經有socket了,沒有的話需要申請一個,見下面
new_sock = (uv__stream_fd(handle) == -1);
// 客戶端還沒有對應的socket fd
if (new_sock) {
err = uv__socket(AF_UNIX, SOCK_STREAM, 0);
if (err < 0)
goto out;
// 保存socket對應的文件描述符到io觀察者
handle->io_watcher.fd = err;
}
// 需要連接的服務器信息。主要是unix域路徑信息
memset(&saddr, 0, sizeof saddr);
strncpy(saddr.sun_path, name, sizeof(saddr.sun_path) - 1);
saddr.sun_path[sizeof(saddr.sun_path) - 1] = '\0';
saddr.sun_family = AF_UNIX;
// 連接服務器,unix域路徑是name
do {
r = connect(uv__stream_fd(handle),(struct sockaddr*)&saddr, sizeof saddr);
}
while (r == -1 && errno == EINTR);
// 忽略錯誤處理邏輯
err = 0;
// 設置socket的可讀寫屬性
if (new_sock) {
err = uv__stream_open((uv_stream_t*)handle,
uv__stream_fd(handle),
UV_HANDLE_READABLE | UV_HANDLE_WRITABLE);
}
// 把io觀察者註冊到libuv,等到連接成功或者可以發送請求
if (err == 0)
uv__io_start(handle->loop, &handle->io_watcher, POLLIN | POLLOUT);
out:
// 記錄錯誤碼,如果有的話
handle->delayed_error = err;
// 連接成功時的回調
handle->connect_req = req;
uv__req_init(handle->loop, req, UV_CONNECT);
req->handle = (uv_stream_t*)handle;
req->cb = cb;
QUEUE_INIT(&req->queue);
// 如果連接出錯,在pending節點會執行req對應的回調。錯誤碼是delayed_error
if (err)
uv__io_feed(handle->loop, &handle->io_watcher);
}
本文大致分析了unix域在libuv中是如何封裝的。大致的流程和網絡編程一樣。分爲服務端和客戶端兩面。libuv在操作系統提供的api的基礎上。和libuv的異步非阻塞結合。在libuv中爲進程間提供了一種通信方式。後續會繼續分析本文提到的內容。