之前分析了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字段。这个后续再开一篇文章分析。