高渐离の屋

一个不起眼的个人小站

0%

Libuv初探

前言

作为一个 Node.js 玩家,libuv的鼎鼎大名可谓是如雷贯耳。在我的印象中,libuv 就是个“封装了 ICOP/epoll 等的超级牛逼的基于事件循环的库”,换句话说,就是“我知道你很牛逼,但是我啥都不知道”。
在生活中,有很多事情不是不能做,只是需要一个契机。有了这个契机,我就能有足够的动力去完成之。而我这学期的 C++专业选修课大作业便给了我这个契机:

题目三(10 分)
在题目二的基础上,将游戏由本地单机,扩展为服务器多人游戏平台,使用客户端/服务器的方式,同一时间可以多人登录系统。将所有闯关者、出题者信息保存在服务器。
要求:

  • 必须在题目二基础上进行修改。
  • 使用 socket 进行通信。
  • 需要完成服务器端程序,以及客户端程序。客户端可以启动多个同时与服务器交互,要求服务器具有并发处理能力。

从入门到放弃

其实一开始,我曾经被 libuv 吓退过,究其原因就是那一大堆uv_开头的指针,而代码写出来大概是这个画风:
Echo-Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>

uv_loop_t *loop;

typedef struct {
uv_write_t req;
uv_buf_t buf;
} write_req_t;

void free_write_req(uv_write_t *req) {
write_req_t *wr = (write_req_t*) req;
free(wr->buf.base);
free(wr);
}

void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = malloc(suggested_size);
buf->len = suggested_size;
}

void echo_write(uv_write_t *req, int status) {
if (status < 0) {
fprintf(stderr, "Write error %s\n", uv_err_name(status));
}
free_write_req(req);
}

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);
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);
}

void on_new_connection(uv_stream_t *server, int status) {
if (status == -1) {
// error!
return;
}

uv_pipe_t *client = (uv_pipe_t*) malloc(sizeof(uv_pipe_t));
uv_pipe_init(loop, client, 0);
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);
}
}

void remove_sock(int sig) {
uv_fs_t req;
uv_fs_unlink(loop, &req, "echo.sock", NULL);
exit(0);
}

int main() {
loop = uv_default_loop();

uv_pipe_t server;
uv_pipe_init(loop, &server, 0);

signal(SIGINT, remove_sock);

int r;
if ((r = uv_pipe_bind(&server, "echo.sock"))) {
fprintf(stderr, "Bind error %s\n", uv_err_name(r));
return 1;
}
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);
}

这堆结构体是什么???光这一堆指针就足够劝退了吧!尤其是需要进行回调,还涉及到了各种函数指针,简直是恶心到不能再恶心了。不过这也没办法,谁叫人家只是个 C 语言库呢。

绝处逢生

幸而天无绝人之路,在关于libuv少的可怜的文档中,我发现了这个 Wrapper:UVW

uvw is a header-only, event based, tiny and easy to use libuv wrapper in modern C++.
The basic idea is to hide completely the C-ish interface of libuv behind a graceful C++ API. Currently, no uv_*_t data structure is actually exposed by the library.
Note that uvw stays true to the API of libuv and it doesn’t add anything to its interface. For the same reasons, users of the library must follow the same rules who are used to follow with libuv.
As an example, a handle should be initialized before any other operation and closed once it is no longer in use.

字面意思,一个header-only的库,对libuv的 C 风格 API 进行了封装,并转换成了 C++14 的语法。看起来非常的香,但是吃起来就有些咯牙了——本来关于libuv的资料就非常之少,如果再使用了这个库的话,你能够获得的资料就更少了。
别无他法,只有按照它doxygen生成的文档结合它的单元测试一点点来啃了。
接下来我们的例子都来源于Server.cpp

创建循环

在使用任何基于事件循环的功能之前,你得先有一个循环才行。得益于良好的封装,创建循环变得非常的简单,仅仅需要短短的一行:

1
2
3
auto loop = uvw::Loop::getDefault();//创建循环
listen(*loop);//绑定监听事件
loop->run();//运行循环

监听事件绑定

UVW 将libuv中几乎所有的对象(结构体)都封装到了loop对象之中,取用只需要调用loop::resource<T>()即可。因为我们主要需要绑定的是针对socket的监听,所以首先我们要创建一个TCPHandle

1
std::shared_ptr<uvw::TCPHandle> tcp = loop.resource<uvw::TCPHandle>();

由于是整个过程是异步的,我们无法在适当的时候释放这个 handle,因此必须使用智能指针 shared_ptr 进行托管。

创建好了 Handle,就可以开始绑定事件了!但是在开始之前,要先提一个问题:还记得在 Node.js 里面是怎么绑定事件的吗?

1
2
3
event.on("someEvent", (data) => {
//do some thing
});

上述代码中someEvent是事件的名字,而第二个参数则是事件触发时将要执行的回调函数。有了这份基础,我们来理解uvw中的事件监听就非常容易了。
在 UVW 中,事件绑定主要有两种方法:ononce。前者就是普通的监听,后者除了只能触发一次之外,和前者并没有太大区别。
on方法的原型长这样:

1
Connection<E> uvw::Emitter< T >::on (Listener<E> f)

如果看不懂上面那一堆模板的话,可以直接看这个例子:

1
2
3
tcp->on<uvw::ErrorEvent>([](const uvw::ErrorEvent & event, uvw::TCPHandle&) {
cout << "Error occurred:" << event.what() << endl;
});

在这个例子中,监听的事件是uvw::ErrorEvent,而后面那个 lambda 表达式就是所谓回调函数。这句话的功能就是在发生错误的时候打印出错误来。怎么样,和上面的 Node.js 是不是非常相似?顺带一提,这句话在 Javascript 中会这么写: JS 是万物之母!

1
tcp.on("error", console.log);

连接事件绑定

那么,现在我们已经知道了如何绑定事件,那么就疯狂来绑定吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
tcp->on<uvw::ListenEvent>([](const uvw::ListenEvent&, uvw::TCPHandle & srv) {//监听事件,当客户端连接时会触发
std::shared_ptr<uvw::TCPHandle> client = srv.loop().resource<uvw::TCPHandle>();//获取一个客户端的Handle
srv.accept(*client);
#ifdef DEBUG //调试模式下显示客户端连接信息和断开信息
uvw::Addr remote = client->peer();
std::cout << std::endl
<< remote.ip << ":" << remote.port << " Connected" << std::endl;
client->on<uvw::CloseEvent>([remote](const uvw::CloseEvent&, uvw::TCPHandle&) {//连接关闭时触发的事件
std::cout << "Connection from " << remote.ip << " closed." << std::endl
<< std::endl;
});
#endif // DEBUG
client->on<uvw::EndEvent>([](const uvw::EndEvent&, uvw::TCPHandle & client) {
client.close();//连接结束关闭连接
});
client->on<uvw::ErrorEvent>([](const uvw::ErrorEvent & event, uvw::TCPHandle & client) {
cout << "Error occurred:" << event.what() << endl;//错误监听
client.close();
});
client->on<uvw::DataEvent>([](const uvw::DataEvent & event, uvw::TCPHandle & client) {//接收到数据
if (event.length == 0)
return;
auto temp = new char[event.length + 1];
memcpy_s(temp, event.length, event.data.get(), event.length);
temp[event.length] = '\0';//向数据流尾部追加\0使之被截断为字符串
auto response = handler::mainHandler(temp, client);//移交请求给业务代码
auto toWrite = new char[response.size()];
memcpy_s(toWrite, response.size(), response.c_str(), response.size());
client.tryWrite(toWrite, (unsigned)response.size());//copy后写回Client
delete[] toWrite;
delete[] temp;
});
client->read();
});

上述的DataEvent也和 Node.js 中的

1
2
3
readerStream.on("data", (chunk) => {
data += chunk;
});

非常相似,毕竟 socket 本质上也是文件描述符。总而言之,通过 UVW 我们便可将不熟悉的 C++开发变成我们熟悉的 Node 后端开发,所谓“知己知彼百战不殆”,若不知彼将其化为知的“彼”即可。

绑定端口

当然,前面我们只是绑定好了相应的事件,还差最后一点微小的工作:

1
2
tcp->bind<uvw::IPv6>("[::]", port);//监听0.0.0.0:port和[::]:port
tcp->listen();

值得注意的是:不同于 Nginx,在 libuv 中监听 IPv6 端口即可同时完成对 IPv4 和 IPv6 的双栈监听,若要 IPv6 Only 还需要显式加入TCPHandle::Bind::IPV6ONLY的 Flag。

结语

本文所有代码可以在这里下载:https://github.com/Gaojianli/Word-Clear
事实证明,纵使是libuv这种库也并不是什么洪水猛兽。令和元年,站在巨人的肩膀上的我们面前,面前并不存在什么完全无法跨越的高峰,需要的仅仅是个契机。而所谓契机,在我看来仅仅是个打破自己惰性的借口罢了。自己主动研究是不可能研究的,只有布置了作业才能去看看这样子
今日写在这里,引以为鉴。
では、諸君は。