70、libuv基础:libuv事件循环、TCP handle、UV库使用
说实话,很多C语言开发者一听到“事件循环”就觉得头大。我当年刚接触libuv时也有这种感觉——这东西到底在转什么?
libuv是Node.js的底层I/O引擎,后来被独立出来。它解决了跨平台异步I/O的问题。你想想看,Windows有IOCP,Linux有epoll,macOS有kqueue,每个平台的机制都不一样。libuv帮你把这些差异全抹平了。
我个人习惯把libuv理解成一个“事件调度中心”。你注册事件,它帮你盯着,事件来了就回调你。就这么简单。
70.1 事件循环的运行机制
libuv的事件循环用uv_run()启动。它内部维护了多个阶段(phase),每个阶段处理不同类型的事件。
我记得第一次看libuv源码时,发现它把事件循环分成了这几个阶段:
- timers阶段:处理
uv_timer_t定时器回调 - I/O回调阶段:处理已完成的I/O操作回调
- idle/prepare阶段:每次循环都会执行,用于准备工作
- poll阶段:真正阻塞等待I/O事件的地方
- check阶段:poll之后执行,常用于
uv_write_t的写完成回调 - close回调阶段:处理
uv_close()的关闭回调
为什么会这样设计?说白了,就是为了保证回调的执行顺序可控。你想想看,如果定时器回调和I/O回调混在一起,程序逻辑很容易乱套。
70.2 TCP handle 的使用
libuv的TCP handle用uv_tcp_t表示。它封装了socket的创建、绑定、监听、连接、读写等操作。
我在项目中遇到过一个问题:用libuv写一个高并发TCP服务器,结果发现连接数一上去就丢包。后来排查发现,是uv_read_start的回调里处理太慢,导致内核缓冲区满了。嗯,这里要注意——回调里千万别做耗时操作。
70.2.1 创建TCP服务器
#include <uv.h>
#include <stdio.h>
uv_loop_t *loop;
uv_tcp_t server;
void on_connection(uv_stream_t *server, int status) {
if (status < 0) {
fprintf(stderr, "连接错误: %s\n", uv_strerror(status));
return;
}
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, on_read);
} else {
uv_close((uv_handle_t*)client, NULL);
}
}
int main() {
loop = uv_default_loop();
uv_tcp_init(loop, &server);
struct sockaddr_in addr;
uv_ip4_addr("0.0.0.0", 8888, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
int r = uv_listen((uv_stream_t*)&server, 128, on_connection);
if (r) {
fprintf(stderr, "监听失败: %s\n", uv_strerror(r));
return 1;
}
printf("服务器启动,监听端口 8888\n");
uv_run(loop, UV_RUN_DEFAULT);
return 0;
}
uv_listen的第二个参数是backlog,我一般设128或256。设太大反而会降低性能,因为内核要维护更大的半连接队列。
70.2.2 创建TCP客户端
void on_connect(uv_connect_t *req, int status) {
if (status < 0) {
fprintf(stderr, "连接失败: %s\n", uv_strerror(status));
return;
}
// 连接成功,开始读写
uv_read_start((uv_stream_t*)req->handle, alloc_buffer, on_read);
// 发送数据
char *msg = "Hello from libuv!";
uv_buf_t buf = uv_buf_init(msg, strlen(msg));
uv_write_t *write_req = (uv_write_t*)malloc(sizeof(uv_write_t));
uv_write(write_req, (uv_stream_t*)req->handle, &buf, 1, on_write);
}
int main() {
loop = uv_default_loop();
uv_tcp_t *socket = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, socket);
struct sockaddr_in addr;
uv_ip4_addr("127.0.0.1", 8888, &addr);
uv_connect_t *connect_req = (uv_connect_t*)malloc(sizeof(uv_connect_t));
uv_tcp_connect(connect_req, socket, (const struct sockaddr*)&addr, on_connect);
uv_run(loop, UV_RUN_DEFAULT);
return 0;
}
70.3 内存管理与handle生命周期
libuv的handle有明确的生命周期。我见过不少新手犯的错误——handle用完了不关闭,或者关闭了还在用。
| 操作 | 函数 | 说明 |
|---|---|---|
| 初始化 | uv_tcp_init() |
分配并初始化handle |
| 绑定 | uv_tcp_bind() |
绑定地址和端口 |
| 监听 | uv_listen() |
开始监听连接 |
| 接受连接 | uv_accept() |
接受客户端连接 |
| 读写 | uv_read_start() / uv_write() |
异步读写数据 |
| 关闭 | uv_close() |
关闭handle,释放资源 |
uv_close(),结果程序退出时事件循环一直不结束。因为libuv会检查所有handle是否都已关闭,没关闭的会阻止uv_run()返回。所以,用完的handle一定要关。
70.4 缓冲区管理
libuv的读写操作需要你提供缓冲区。它不会帮你管理内存,你得自己搞定。
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 on_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
if (nread > 0) {
// 处理收到的数据
printf("收到 %zd 字节: %.*s\n", nread, (int)nread, buf->base);
} else if (nread < 0) {
if (nread != UV_EOF) {
fprintf(stderr, "读取错误: %s\n", uv_strerror(nread));
}
uv_close((uv_handle_t*)client, NULL);
}
// 记得释放缓冲区
free(buf->base);
}
我个人习惯在alloc_buffer里用malloc,在on_read里用free。这样配对清晰,不容易内存泄漏。
70.5 错误处理
libuv的函数返回值或回调的status参数,都携带错误信息。用uv_strerror()可以转成可读的字符串。
我记得有一次线上排查问题,发现客户端连不上服务器。用uv_strerror一看,返回的是“Connection refused”。原来是服务器没启动。嗯,这种低级错误,有个好用的错误处理机制能省不少时间。
if (status < 0),不要用 if (status == -1),因为不同错误码代表不同错误。
70.6 实战建议
- 单线程模型:libuv的事件循环是单线程的,不要在回调里做阻塞操作。如果必须做,用
uv_queue_work()丢到线程池。 - handle复用:关闭后的handle可以用
uv_tcp_init()重新初始化,但我不建议这么做。重新malloc一个更安全。 - 调试技巧:用
UV_DEBUG环境变量可以开启libuv的调试日志。我排查问题时经常用这个。 - 性能调优:如果服务器需要处理大量短连接,考虑用
uv_tcp_nodelay()禁用Nagle算法,减少延迟。
libuv其实不复杂。你把它当成一个事件驱动的框架,注册好回调,然后让事件循环跑起来就行。剩下的,libuv会帮你搞定。
公众号:蓝海资料掘金营,微信deep3321