echoserver的几种实现-2(libuv)


echoserver的几种实现-1中,通过socket、select、epoll几种方式实现了echoserver,了解了select和epoll模型的优劣。接下来,我将通过libuv实现另一个echoserver,学习libuv的使用。libuv的源码在这里,一个比较好的教程在这里
libuv是node.js实现时所用到的事件库,可以通过注册事件回调的方式实现异步处理系统中的IO事件。


使用libuv,一般都会用到一个uv_loop_t的数据结构,它表示了程序中的主循环,保存了需要处理的事件、内部使用的文件描述符、当前的状态等信息。一般的,直接使用libuv的默认uv_loop_t结构:

1
2
uv_loop_t *loop;
loop = uv_default_loop();

uv_default_loop使用了一个简单的单例实现。如果必要的话,初始化一个全局的uv_loop_t结构体;将此结构体的指针返回。


之后,我们可以向loop中添加待处理的数据结构了。

1
2
uv_tcp_t server;
uv_tcp_init(loop, &server);

uv_tcp_init做了这两件事:

a.初始化uv_tcp_t结构,并关联loop;
b.将uv_tcp_t添加到loop的处理列表中。

之后,我们可以像之前一样,为tcp绑定一个监听的地址/端口。

1
2
3
struct sockaddr_in addr;
uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);

接下来,要注册第一个需要处理的事件了。
1
uv_listen((uv_stream_t*)&server, DEFAULT_BACKLOG, on_new_connection);

每当server上有新连接到来时,就调用on_new_connection函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void on_new_connection(uv_stream_t *server, int status)
{

if(status < 0)
{
fprintf(stderr, "On new connection error %s.\n", uv_strerror(status));
return;
}
printf("new connection is coming...\n");
uv_tcp_t *client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
if(uv_accept(server, (uv_stream_t*)client) == 0)
{
uv_read_start((uv_stream_t*)client, alloc_buffer, echo_read);
} else
{
uv_close((uv_handle_t*)client, NULL);
}
}

这里的uv_stream_t和之前uv_listen的传入的第一个参数是一样的。在这个函数中,可以直接调用uv_accept函数获得和对应客户端绑定的uv_tcp_t对象,这个对象可以当作是对之前的echoserver例程中client socket的封装。
得到了client对象,就可以进行接下来的事件处理注册。
1
uv_read_start((uv_stream_t*)client, alloc_buffer, echo_read);

这次一口气传入了两个事件回调函数。
alloc_buffer负责为读事件分配存储空间;echo_read负责具体读事件处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf)
{

buf->base = (char*) malloc(suggested_size);
buf->len = suggested_size;
}
void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf)
{

if (nread < 0)
{
if (nread != UV_EOF)
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t*) client, NULL);
} else if (nread > 0)
{
uv_write_t *req = (uv_write_t*) malloc(sizeof(uv_write_t));
uv_buf_t wrbuf = uv_buf_init(buf->base, nread);
uv_write(req, client, &wrbuf, 1, echo_write);
}

if (buf->base)
free(buf->base);
}

这里libuv甚至隐藏了具体的读取过程,直接将读取的结果通过一个uv_buf_t数据结构传入函数中。因此这里的操作仅仅是复制一个uv_buf_t数据,并将其通过uv_write写回。
uv_write中也提供了一个echo_write回调函数。
1
2
3
4
5
6
7
8
void echo_write(uv_write_t *req, int status)
{

if (status)
{
fprintf(stderr, "Write error %s.\n", uv_strerror(status));
}
free(req);
}

在echo_read和echo_write的最后,都需要将之前申请的内存释放掉。


设置完这一切事件的处理规则之后,只需要调用uv_run让主循环跑起来即可。

1
uv_run(loop, UV_RUN_DEFAULT);


总结

在使用libuv开发echoserver的过程中,libuv封装了大多数的socket操作,也不用关心异步的具体实现方式,通过提炼出事件+回调的编程模型,极大的简化了异步网络编程的步骤。libuv也开放了源代码,源码阅读可以从uv_loop_t(uv_loop_s)数据结构入手,理解uv_run函数如何实现这种事件+回调的异步模型。
本文的完整代码放在我的GuitHub。另外,一个使用libuv实现的客户端程序也放在这里

转载请注明出处: http://blog.guoyb.com/2016/05/29/echo-server-2/

欢迎关注我的微信公众号TechTalking,技术·生活·思考:
后端技术小黑屋

Comments