libevent 服务端入门:`event_base`、`evconnlistener` 与 `bufferevent`
libevent 服务端入门:event_base、evconnlistener 与 bufferevent
时间:2026/05/04
关键词:libevent、Reactor、
event_base、evconnlistener、bufferevent、evbuffer、TCP 服务端
核心目标:理解 libevent 如何把socket + epoll + 非阻塞 I/O + 缓冲区封装成事件驱动服务端模型。
1. libevent 在解决什么问题
如果手写一个高并发 TCP 服务端,通常要自己处理:
- 创建、绑定、监听 socket
- 设置非阻塞
epoll注册和事件循环accept/read/write的错误码- 半包、粘包、发送缓冲积压
- 定时器和超时清理
libevent 的目标是把这些底层细节抽象成:
event_base:事件循环event:普通 fd / timer / signal 事件evconnlistener:监听 socket 与 accept 封装bufferevent:带输入/输出缓冲区的连接 I/O 对象evbuffer:高效缓冲区
可以粗略理解成:
1 | epoll/kqueue/select 等后端 |
2. 一个 TCP Reactor 在 libevent 里的映射
手写 Reactor:
1 | listen fd readable -> accept |
libevent 里大致对应:
1 | evconnlistener accept callback -> 新连接接入 |
关键变化是:
- 你不直接处理
epoll_wait - 通常也不直接处理裸
read/write - 连接上的数据先进入
evbuffer - 业务逻辑在回调里消费输入缓冲并写入输出缓冲
3. event_base:事件循环对象
最基本的生命周期:
1 |
|
含义:
event_base_new()创建事件循环event_base_dispatch()开始调度事件event_base_free()释放事件循环资源
常见退出方式:
1 | event_base_loopexit(base, NULL); // 让 loop 尽快退出 |
工程经验:
- 一个
event_base通常只在一个 I/O 线程中运行 - 多线程服务端常见做法是“一组 I/O 线程,每个线程一个 event_base”
- 跨线程投递任务时要用线程安全机制,不要随意在别的线程直接操作连接对象
4. evconnlistener:监听与接入连接
传统 TCP 服务端需要:
1 | socket -> setsockopt -> bind -> listen -> accept |
evconnlistener_new_bind() 可以把这套监听流程封装起来。
4.1 创建接口
1 |
|
如果你已经自己创建并绑定了 socket,可以用:
1 | struct evconnlistener *evconnlistener_new( |
参数含义:
| 参数 | 含义 |
|---|---|
base |
事件循环 |
cb |
新连接到来时的回调 |
ptr |
传给回调的用户参数 |
flags |
listener 选项 |
backlog |
listen() 队列长度,-1 使用默认值 |
sa / socklen |
监听地址 |
4.2 常用 flags
常用组合:
1 | LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE |
含义:
| flag | 含义 |
|---|---|
LEV_OPT_CLOSE_ON_FREE |
evconnlistener_free() 时关闭监听 fd |
LEV_OPT_REUSEABLE |
设置地址复用,方便服务重启 |
LEV_OPT_CLOSE_ON_EXEC |
子进程 exec 后关闭 fd |
LEV_OPT_DISABLED |
创建后先不启用监听 |
LEV_OPT_REUSEABLE_PORT |
支持时启用端口复用,常用于多进程/多线程监听同端口 |
注意:libevent 里历史拼写是 REUSEABLE,不是标准英文的 REUSABLE。
4.3 accept 回调
1 | typedef void (*evconnlistener_cb)( |
新连接到来时,libevent 会把已 accept 的连接 fd 传给你。
最常见的下一步是把这个 fd 包成 bufferevent。
4.4 错误回调
1 | void evconnlistener_set_error_cb( |
错误回调里通常要打印真实错误,并按需要退出 loop:
1 | static void accept_error_cb(struct evconnlistener *listener, void *ctx) { |
5. bufferevent:带缓冲的连接对象
bufferevent 可以理解成:
1 | socket fd |
创建 socket 型 bufferevent:
1 |
|
服务端 accept 后通常写:
1 | struct bufferevent *bev = bufferevent_socket_new( |
常用 options:
| option | 含义 |
|---|---|
BEV_OPT_CLOSE_ON_FREE |
bufferevent_free() 时关闭底层 fd |
BEV_OPT_DEFER_CALLBACKS |
延迟回调,减少回调重入 |
BEV_OPT_THREADSAFE |
给 bufferevent 加锁,需启用线程支持 |
设置回调:
1 | void bufferevent_setcb( |
启用读写事件:
1 | bufferevent_enable(bev, EV_READ | EV_WRITE); |
6. 三类回调分别做什么
6.1 read callback
当输入缓冲里有数据,并且满足读水位条件时触发。
1 | static void read_cb(struct bufferevent *bev, void *ctx) { |
这个例子把输入缓冲的数据直接移动到输出缓冲,实现 echo。
6.2 write callback
当输出缓冲降到写低水位时触发。
它不是“每次写完一条消息都触发”的语义,更适合做:
- 发送完成后关闭连接
- 继续发送下一批数据
- 解除上游背压
6.3 event callback
处理连接生命周期事件:
| 事件 | 含义 |
|---|---|
BEV_EVENT_CONNECTED |
主动连接成功,客户端更常用 |
BEV_EVENT_EOF |
对端关闭 |
BEV_EVENT_ERROR |
连接错误 |
BEV_EVENT_TIMEOUT |
读/写超时 |
服务端常见写法:
1 | static void event_cb(struct bufferevent *bev, short events, void *ctx) { |
7. evbuffer:输入/输出缓冲区
每个 bufferevent 都有两个 evbuffer:
1 | struct evbuffer *bufferevent_get_input(struct bufferevent *bufev); |
常用操作:
| API | 用途 |
|---|---|
evbuffer_get_length(buf) |
当前缓冲区字节数 |
evbuffer_add(buf, data, len) |
追加数据 |
evbuffer_add_buffer(dst, src) |
把 src 内容移动到 dst |
evbuffer_remove(buf, data, len) |
拷贝并移除数据 |
evbuffer_copyout(buf, data, len) |
只拷贝,不移除 |
evbuffer_drain(buf, len) |
丢弃前 len 字节 |
evbuffer_readln(buf, &n, mode) |
按行读取 |
也可以通过 bufferevent_read/write 操作:
1 | size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size); |
经验上:
- 简单收发可以用
bufferevent_read/write - 协议解析更常直接操作
evbuffer - 不要假设一次 read callback 就是一条完整消息
8. 完整示例:TCP echo server
1 |
|
编译:
1 | cc echo_server.c -o echo_server -levent |
测试:
1 | ./echo_server 9876 |
9. 处理半包:长度前缀协议示意
TCP 是字节流,read callback 里可能拿到:
- 半条消息
- 一条消息
- 多条消息粘在一起
假设协议格式是:
1 | 4 字节网络序长度 + payload |
解析思路:
1 |
|
重点不是这段代码本身,而是循环条件:
- 缓冲区不够一个完整包头:返回
- 缓冲区不够完整包体:返回
- 够一条消息就取一条,继续尝试解析下一条
10. 水位线与背压
设置水位:
1 | void bufferevent_setwatermark( |
读水位:
1 | bufferevent_setwatermark(bev, EV_READ, 0, 64 * 1024); |
含义:
- 输入缓冲超过高水位时,libevent 会暂停继续读
- 缓冲降下来后再恢复读
写水位:
- 输出缓冲降到低水位时触发 write callback
- 可以用来继续发送、关闭连接或恢复上游生产
工程上要记住:
高性能服务端不能无限制地往输出缓冲塞数据,否则慢客户端会拖垮内存。
11. 超时管理
设置读写超时:
1 | struct timeval rto = {30, 0}; |
触发后会进入 event callback:
1 | if (events & BEV_EVENT_TIMEOUT) { |
常见用法:
- 读超时:客户端长期不发数据,关闭连接
- 写超时:输出缓冲长期发不出去,说明对端太慢或网络异常
- 应用层心跳:不要只依赖 TCP keepalive
12. 多线程与 libevent
libevent 可以启用线程支持:
1 |
|
编译链接时通常需要:
1 | cc server.c -o server -levent -levent_pthreads -lpthread |
但这不意味着“多个线程随便操作同一个连接”就安全。
更常见的结构是:
1 | main thread: |
这样可以减少锁竞争,也更容易排查连接状态。
12.1 多 I/O 线程 echo server 示例
下面这个例子采用:
1 | main event_base: |
注意:同一进程内的线程共享 fd 表,所以这里只需要把 fd 数值写给 worker,不需要像多进程那样用 SCM_RIGHTS 传递文件描述符。
1 |
|
编译:
1 | cc mt_echo_server.c -o mt_echo_server -levent -levent_pthreads -lpthread |
测试:
1 | ./mt_echo_server 9876 |
这个例子的关键点:
evthread_use_pthreads()必须在创建任何event_base之前调用- main 线程只负责监听和分发,不创建连接上的
bufferevent - 每个
bufferevent只在所属 worker 线程里创建、读写和释放 - 这里不需要给
bufferevent_socket_new()额外加BEV_OPT_THREADSAFE,因为连接没有被多个线程同时操作 socketpair只是一个唤醒和投递机制,生产环境里通常还会加队列长度限制、优雅退出和负载统计
13. 常见坑
13.1 忘记 BEV_OPT_CLOSE_ON_FREE
释放 bufferevent 后 fd 没关,容易造成句柄泄漏。
13.2 在 read callback 里假设“一次回调一条消息”
TCP 是字节流,必须做协议拆包。
13.3 一直监听写事件或无限写入输出缓冲
慢客户端会造成输出缓冲堆积。
要配合写水位、队列上限和断开策略。
13.4 回调里直接做重 CPU 业务
I/O 线程被卡住后,所有连接都会受影响。
重计算应该移交给工作线程。
13.5 跨线程直接操作 bufferevent
如果没有清晰的线程模型,很容易产生竞态。
更稳妥的是把操作投递回连接所属的 I/O 线程。
14. 一页总结
libevent 服务端这篇最重要的是:
event_base是事件循环evconnlistener封装监听 socket 和 accept- 每个连接通常对应一个
bufferevent bufferevent内部有输入/输出evbuffer- read callback 负责解析输入缓冲,write callback 负责发送进度,event callback 负责生命周期
- TCP 半包/粘包仍然要靠应用层协议解析
- 水位、超时和背压是服务端稳定性关键
如果只记一句:
libevent 帮你管理事件循环和非阻塞 I/O,但协议状态、连接生命周期和背压策略仍然要你自己设计清楚。
15. 参考资料
libevent book: Ref6 bufferevent
https://libevent.org/libevent-book/Ref6_bufferevent.htmllibevent book: Ref8 listener
https://libevent.org/libevent-book/Ref8_listener.htmllibevent book: Ref7 evbuffer
https://libevent.org/libevent-book/Ref7_evbuffer.html