69、libevent实战:使用libevent实现高性能TCP服务器
说实话,很多刚接触网络编程的朋友,一上来就撸原生epoll,结果被事件循环、边缘触发、内存管理搞得焦头烂额。我个人习惯是,先拿libevent这种封装好的库把业务跑通,再回头研究底层细节。今天我们就用libevent实现一个高性能TCP服务器,看看它到底能省多少事。
为什么选libevent?
你想想看,一个高性能服务器要处理什么?无非就是:新连接进来、数据可读、数据可写、连接断开。原生epoll你得自己管理这些事件,还得处理惊群、锁竞争、内存池。libevent把这些都封装好了,你只需要注册回调函数就行。
我在项目中遇到过几次,团队里有人自己手写epoll,结果线上出了几次内存泄漏。后来换成libevent,稳定性明显提升。说白了,除非你要做极致性能优化,否则libevent完全够用。
核心架构
libevent的核心就三个东西:event_base(事件循环)、event(事件对象)、bufferevent(带缓冲的事件)。bufferevent尤其好用,它自动帮你处理了读写缓冲区的管理。
关键点:libevent是跨平台的。你在Linux上用epoll,在macOS上用kqueue,代码完全不用改。这一点在项目迁移时特别香。
代码实现
下面是一个完整的echo服务器实现。客户端发什么,服务器就原样返回什么。麻雀虽小五脏俱全。
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 读取回调:客户端发来数据时触发
void read_cb(struct bufferevent *bev, void *ctx) {
struct evbuffer *input = bufferevent_get_input(bev);
struct evbuffer *output = bufferevent_get_output(bev);
// 直接把输入数据拷贝到输出
evbuffer_add_buffer(output, input);
}
// 事件回调:处理连接错误或关闭
void event_cb(struct bufferevent *bev, short events, void *ctx) {
if (events & BEV_EVENT_ERROR) {
perror("连接错误");
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
bufferevent_free(bev);
}
}
// 新连接回调
void accept_cb(struct evconnlistener *listener,
evutil_socket_t fd,
struct sockaddr *addr,
int socklen,
void *ctx) {
struct event_base *base = evconnlistener_get_base(listener);
// 为每个新连接创建bufferevent
struct bufferevent *bev = bufferevent_socket_new(
base, fd, BEV_OPT_CLOSE_ON_FREE);
if (!bev) {
fprintf(stderr, "创建bufferevent失败\n");
close(fd);
return;
}
// 设置回调
bufferevent_setcb(bev, read_cb, NULL, event_cb, NULL);
bufferevent_enable(bev, EV_READ | EV_WRITE);
printf("新连接接入\n");
}
int main() {
struct event_base *base = event_base_new();
if (!base) {
fprintf(stderr, "初始化event_base失败\n");
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 创建连接监听器
struct evconnlistener *listener = evconnlistener_new_bind(
base,
accept_cb,
NULL,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
-1,
(struct sockaddr*)&addr,
sizeof(addr));
if (!listener) {
fprintf(stderr, "创建监听器失败\n");
event_base_free(base);
return -1;
}
printf("服务器启动,监听端口 8888\n");
// 进入事件循环
event_base_dispatch(base);
// 清理
evconnlistener_free(listener);
event_base_free(base);
return 0;
}
编译与运行
编译时需要链接libevent库。我建议用pkg-config自动获取编译参数,省得手动找路径。
gcc -o echo_server echo_server.c $(pkg-config --cflags --libs libevent)
运行后,用telnet或nc测试一下:
# 终端1:启动服务器
./echo_server
# 终端2:连接测试
echo "hello libevent" | nc localhost 8888
关键点解析
| 组件 | 作用 | 注意事项 |
|---|---|---|
| event_base | 事件循环的核心,管理所有事件 | 一个线程一个base,不要共享 |
| evconnlistener | 监听端口,接受新连接 | LEV_OPT_REUSEABLE一定要加,否则端口释放慢 |
| bufferevent | 每个连接一个,自动管理缓冲区 | BEV_OPT_CLOSE_ON_FREE自动关闭socket |
| 回调函数 | 处理读写和事件 | 不要在回调里做耗时操作,否则阻塞事件循环 |
小技巧:如果回调里需要做耗时操作(比如数据库查询),建议用libevent的bufferevent_rate_limit做限速,或者把任务丢到线程池里。我一般用event_base_defer_callback把任务延迟执行。
避坑指南
我曾经在一个项目里犯过这样的错误:在回调函数里直接调用了sleep()。结果整个服务器卡死了,因为libevent是单线程事件循环,一个回调阻塞,所有连接都受影响。
还有一次,我忘了设置BEV_OPT_CLOSE_ON_FREE,结果连接断开后socket没关闭,导致文件描述符泄漏。排查了半天才发现。
警告:千万不要在回调函数里做阻塞操作!包括但不限于:sleep、阻塞I/O、加锁等待。如果必须做,请使用libevent的线程池或异步任务机制。
性能调优建议
如果你要处理高并发,有几个地方可以优化:
- 调整缓冲区大小:bufferevent默认缓冲区是4096字节,可以根据业务调整。我一般设成16KB或64KB。
- 使用多线程:libevent支持多线程,每个线程一个event_base。注意连接要均匀分配到各个线程。
- 开启TCP_NODELAY:减少小包延迟,对交互式应用特别有效。
- 调整backlog:evconnlistener_new_bind的backlog参数,默认-1表示用系统默认值。高并发场景建议设成1024或更大。
嗯,到这里一个完整的libevent TCP服务器就实现了。代码量不大,但该有的都有了。你想想看,如果用原生epoll,光处理边缘触发和缓冲区就得写多少代码?libevent把这些脏活累活都干了,我们专心写业务逻辑就好。
我个人觉得,libevent最大的价值不是性能(虽然它性能确实不错),而是让你从底层I/O细节中解放出来。毕竟,大部分业务场景下,开发效率和代码可维护性比那点性能差异重要得多。
公众号:蓝海资料掘金营,微信deep3321