68、libevent基础:事件循环、事件注册、bufferevent使用
说到网络编程,很多人第一反应就是epoll、select这些底层多路复用接口。说实话,直接用它们写一个稳定的服务端程序,坑确实不少。我记得刚入行那会儿,自己手撸了一个基于epoll的HTTP服务器,结果各种边界情况处理得焦头烂额——连接超时、内存泄漏、信号处理……后来接触了libevent,才觉得「嗯,这才是人写的东西」。
libevent说白了就是一个事件驱动的网络库。它把epoll、kqueue、select这些底层机制封装成了统一的接口。你不需要关心底层用的是啥,只需要告诉它「这个socket可读时通知我」就行了。我个人习惯在中小型项目中优先用libevent,因为它轻量、跨平台,而且性能足够好。
事件循环:libevent的心脏
事件循环是libevent的核心机制。你可以把它理解成一个永不停止的while循环,不断检查哪些事件发生了,然后调用对应的回调函数。
先看一个最基础的例子:
#include <event2/event.h>
#include <stdio.h>
int main() {
// 1. 创建事件基础结构
struct event_base *base = event_base_new();
if (!base) {
fprintf(stderr, "无法创建event_base\n");
return -1;
}
// 2. 进入事件循环
printf("开始事件循环...\n");
event_base_dispatch(base);
// 3. 清理
event_base_free(base);
return 0;
}
这段代码跑起来后,程序会一直卡在event_base_dispatch里。为什么?因为没有任何事件注册,循环就空转着。你可以用Ctrl+C终止它。
核心函数速查表:
| 函数 | 作用 |
|---|---|
| event_base_new() | 创建事件循环实例 |
| event_base_dispatch() | 启动循环,等价于带EVLOOP_NO_EXIT_ON_EMPTY的loop |
| event_base_loop() | 更灵活的循环控制,可设置标志位 |
| event_base_loopexit() | 在指定时间后退出循环 |
| event_base_loopbreak() | 立即退出循环 |
我在项目中遇到过一个问题:事件循环退出了,但程序还在跑。后来发现是忘了检查event_base_dispatch的返回值。它返回0表示正常退出,-1表示出错,1表示没有事件注册且设置了EVLOOP_NO_EXIT_ON_EMPTY。你想想看,如果不做这个检查,bug藏得可深了。
事件注册:告诉libevent你在乎什么
光有循环没用,你得往里面塞事件。事件注册就是把「文件描述符 + 事件类型 + 回调函数」打包成一个struct event,然后挂到event_base上。
来看一个定时器事件的例子:
#include <event2/event.h>
#include <stdio.h>
void timer_cb(evutil_socket_t fd, short what, void *arg) {
printf("定时器触发了!what = %d\n", what);
}
int main() {
struct event_base *base = event_base_new();
// 创建一个定时器事件
struct timeval tv = {2, 0}; // 2秒后触发
struct event *timer = event_new(base, -1, EV_TIMEOUT, timer_cb, NULL);
// 添加事件到循环中
event_add(timer, &tv);
printf("等待定时器...\n");
event_base_dispatch(base);
event_free(timer);
event_base_free(base);
return 0;
}
这里有几个关键点:
event_new的第一个参数是event_base,第二个是文件描述符(定时器传-1),第三个是事件类型,第四个是回调,第五个是传给回调的参数event_add把事件注册到循环中,第二个参数是超时时间- 回调函数的
what参数告诉你具体是什么事件触发了
小技巧:如果你想让定时器周期性触发,可以在回调里再次调用event_add。我个人习惯把这种逻辑封装成一个辅助函数,避免忘记重新添加。
事件类型可以组合使用:
| 标志 | 含义 |
|---|---|
| EV_READ | 文件描述符可读 |
| EV_WRITE | 文件描述符可写 |
| EV_TIMEOUT | 超时事件 |
| EV_PERSIST | 持久化,触发后不自动移除 |
| EV_ET | 边缘触发(类似epoll的ET模式) |
我曾经踩过一个坑:用EV_READ | EV_PERSIST监听socket可读,结果回调里没读完数据,下次循环又触发了,导致无限回调。后来我改成非持久模式,在回调里读完数据再重新添加,问题就解决了。
bufferevent:让数据读写更优雅
直接操作事件回调处理数据读写,代码很快就会变得混乱。你得自己管理缓冲区、处理部分读写、处理错误……libevent提供了bufferevent,它封装了这一切。
bufferevent的核心思想是:你只管提供读回调和写回调,底层的数据缓冲和事件管理交给libevent。说白了,它就是一个带缓冲区的socket包装器。
看一个echo服务器的核心片段:
#include <event2/bufferevent.h>
#include <event2/event.h>
#include <stdio.h>
void read_cb(struct bufferevent *bev, void *ctx) {
char buf[1024];
int n = bufferevent_read(bev, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("收到数据: %s\n", buf);
// 原样写回
bufferevent_write(bev, buf, n);
}
}
void event_cb(struct bufferevent *bev, short what, void *ctx) {
if (what & BEV_EVENT_ERROR) {
perror("连接错误");
} else if (what & BEV_EVENT_EOF) {
printf("对方关闭连接\n");
}
bufferevent_free(bev);
}
void accept_cb(evutil_socket_t listener, short event, void *arg) {
struct event_base *base = (struct event_base *)arg;
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int fd = accept(listener, (struct sockaddr *)&addr, &len);
if (fd < 0) return;
// 为每个新连接创建bufferevent
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, read_cb, NULL, event_cb, NULL);
bufferevent_enable(bev, EV_READ | EV_WRITE);
}
这段代码展示了bufferevent的典型用法:
bufferevent_socket_new:创建一个基于socket的buffereventbufferevent_setcb:设置读回调、写回调、事件回调bufferevent_enable:启用读写事件,默认是禁用的bufferevent_read和bufferevent_write:操作内部缓冲区
注意:BEV_OPT_CLOSE_ON_FREE这个选项很重要。如果不加,bufferevent释放时不会关闭底层socket,很容易造成文件描述符泄漏。我曾经在生产环境排查过一个连接数不断上涨的问题,最后发现就是忘了加这个选项。
知识体系总览
下面这张图把libevent的核心脉络梳理了一下。你看,从event_base出发,分出了事件注册和bufferevent两条主线,最终都汇聚到事件循环中。
从这张图可以看得很清楚:event_base是总调度,左边的事件注册机制让你能监听各种I/O和定时事件,右边的bufferevent则是在事件基础上提供了更高级的数据读写抽象。实际项目中,我通常会用bufferevent处理TCP连接,用普通事件处理信号和定时器。
避坑指南
最后分享几个我实战中遇到的教训:
- 线程安全:libevent默认不是线程安全的。如果你要在多线程中使用,记得用
event_base_loop配合EVLOOP_NO_EXIT_ON_EMPTY,或者使用event_base_once在指定线程中执行回调。 - 内存管理:每个
event_new创建的event必须用event_free释放,bufferevent同理。我曾经在长连接服务中漏释放了一个bufferevent,内存涨到几个G才发现。 - 回调不要阻塞:libevent是单线程的,如果你的回调函数里做了耗时操作(比如数据库查询),整个事件循环都会被卡住。解决办法是用
bufferevent配合工作线程池。
libevent其实还有很多高级特性,比如SSL支持、DNS解析、HTTP客户端等。但掌握了事件循环、事件注册和bufferevent这三板斧,你已经能写出大部分网络程序了。剩下的,遇到具体需求再查文档也不迟。