引言
在前面的文章中,我们分别学习了select、poll和epoll三种 I/O 多路复用机制。虽然 epoll 性能卓越,但直接使用这些系统调用编写服务器存在以下痛点:
代码冗长:每次都要手动管理描述符集合、事件注册、循环检测
跨平台困难:Linux 用 epoll,macOS 用 kqueue,Windows 用 IOCP
缺乏扩展:定时器、信号处理等功能需要自己实现
Libevent正是为了解决这些问题而生的。它是一个轻量级、高性能的 C 语言网络库,对底层 I/O 复用函数进行了统一封装,让开发者只需关注业务逻辑即可。
第一部分:Libevent 核心概念
一、三大核心组件
二、事件类型
| 事件类型 | 含义 | 使用场景 |
|---|---|---|
EV_READ | 读就绪 | 接收客户端数据、accept 新连接 |
EV_WRITE | 写就绪 | 发送数据给客户端 |
EV_TIMEOUT | 超时事件 | 定时任务、心跳检测 |
EV_SIGNAL | 信号事件 | 捕获 SIGINT、SIGTERM 等 |
EV_PERSIST | 永久事件 | 触发后保持注册状态,不用重新添加 |
EV_ET | 边缘触发 | 配合 epoll ET 模式(需系统支持) |
三、Reactor 模式
Libevent 采用经典的Reactor(反应器)模式:
第二部分:Libevent 安装
# Ubuntu/Debian 安装开发库
sudo apt install libevent-dev# CentOS/RHEL 安装
sudo yum install libevent-devel# 编译时链接
gcc program.c -o program -levent# 查看安装的头文件位置
ls /usr/include/event2/
第三部分:基本使用流程
一、最小化示例
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <event2/event.h> /* 信号回调函数 */ void signal_cb(evutil_socket_t fd, short events, void *arg) { printf("收到信号: %d\n", fd); } /* 定时回调函数 */ void timeout_cb(evutil_socket_t fd, short events, void *arg) { printf("定时器触发!\n"); } int main() { // ===== 步骤1:创建事件循环引擎 ===== struct event_base *base = event_base_new(); if (!base) { fprintf(stderr, "创建 event_base 失败\n"); return -1; } // ===== 步骤2:注册信号事件 ===== struct event *sig_ev = evsignal_new(base, SIGINT, signal_cb, NULL); if (!sig_ev) { fprintf(stderr, "创建信号事件失败\n"); return -1; } event_add(sig_ev, NULL); // 注册到事件循环 // ===== 步骤3:注册定时事件 ===== struct timeval tv = {5, 0}; // 5 秒后触发 struct event *time_ev = evtimer_new(base, timeout_cb, NULL); if (!time_ev) { fprintf(stderr, "创建定时事件失败\n"); return -1; } event_add(time_ev, &tv); // 注册并设置超时时间 // ===== 步骤4:启动事件循环(阻塞) ===== printf("事件循环启动...\n"); event_base_dispatch(base); // ===== 步骤5:清理资源 ===== event_free(sig_ev); event_free(time_ev); event_base_free(base); return 0; }编译运行:
gcc demo.c -o demo -levent
./demo
# 输出:事件循环启动...
# 5秒后:定时器触发!
# Ctrl+C:收到信号: 2
二、使用流程总结
第四部分:事件类型详解
一、信号事件
void signal_cb(evutil_socket_t fd, short events, void *arg) { printf("捕获信号 %d\n", fd); } // 注册 SIGINT 信号 struct event *sig_ev = evsignal_new(base, SIGINT, signal_cb, NULL); event_add(sig_ev, NULL);要点:
信号事件使用
evsignal_new创建fd参数实际上是信号编号信号没有描述符,直接传
base和信号值
二、定时事件
void timeout_cb(evutil_socket_t fd, short events, void *arg) { printf("定时器触发!\n"); } // 5 秒后触发一次 struct timeval tv = {5, 0}; // 秒, 微秒 struct event *time_ev = evtimer_new(base, timeout_cb, NULL); event_add(time_ev, &tv);持久定时器:加上EV_PERSIST标志
// 每 2 秒触发一次 struct event *persist_ev = event_new(base, -1, EV_TIMEOUT | EV_PERSIST, timeout_cb, NULL); struct timeval tv = {2, 0}; event_add(persist_ev, &tv);三、IO 事件
void read_cb(evutil_socket_t fd, short events, void *arg) { char buf[128]; int n = recv(fd, buf, sizeof(buf) - 1, 0); if (n > 0) { buf[n] = '\0'; printf("收到: %s\n", buf); send(fd, "OK", 2, 0); } else { // 客户端关闭 close(fd); } } // 注册读事件(持久模式) struct event *io_ev = event_new(base, client_fd, EV_READ | EV_PERSIST, read_cb, NULL); event_add(io_ev, NULL);四、持久事件 EV_PERSIST
| 场景 | 使用标志 | 说明 |
|---|---|---|
| 一次性定时器 | EV_TIMEOUT | 触发后自动删除 |
| 周期性定时器 | EV_TIMEOUT | EV_PERSIST | 每次触发后保持 |
| 信号处理 | EV_SIGNAL | EV_PERSIST | 多次 Ctrl+C 都响应 |
| IO 监听 | EV_READ | EV_PERSIST | 持续监听读事件 |
第五部分:回显服务器完整实现
一、数据结构设计
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <event2/event.h> #define PORT 6000 #define BUFFER_SIZE 128 /* 客户端消息结构体 */ typedef struct { int fd; // 文件描述符 struct event *ev; // 关联的事件指针 char buffer[BUFFER_SIZE]; // 数据缓冲区 } ClientData;二、回调函数
/* 接收客户端数据 */ void recv_cb(evutil_socket_t fd, short events, void *arg) { ClientData *data = (ClientData *)arg; char buf[BUFFER_SIZE]; int n = recv(fd, buf, sizeof(buf) - 1, 0); if (n <= 0) { // 客户端断开 printf("客户端断开: fd=%d\n", fd); event_del(data->ev); // 从事件循环中移除 event_free(data->ev); // 释放事件 close(fd); // 关闭描述符 free(data); // 释放自定义结构 } else { buf[n] = '\0'; printf("收到 fd=%d: %s\n", fd, buf); send(fd, "OK\n", 3, 0); } } /* 接受客户端连接 */ void accept_cb(evutil_socket_t listen_fd, short events, void *arg) { struct event_base *base = (struct event_base *)arg; struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len); if (client_fd == -1) { perror("accept"); return; } printf("新连接: fd=%d, IP=%s\n", client_fd, inet_ntoa(client_addr.sin_addr)); // 为客户端分配数据结构 ClientData *data = (ClientData *)malloc(sizeof(ClientData)); if (!data) { close(client_fd); return; } memset(data, 0, sizeof(ClientData)); >int main() { // 1. 创建监听套接字 int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd == -1) { perror("socket"); return -1; } int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("bind"); close(listen_fd); return -1; } if (listen(listen_fd, 5) == -1) { perror("listen"); close(listen_fd); return -1; } // 2. 创建 Libevent 实例 struct event_base *base = event_base_new(); if (!base) { fprintf(stderr, "event_base_new 失败\n"); close(listen_fd); return -1; } // 3. 注册监听套接字事件 struct event *listen_ev = event_new(base, listen_fd, EV_READ | EV_PERSIST, accept_cb, base); if (!listen_ev) { fprintf(stderr, "event_new 失败\n"); close(listen_fd); event_base_free(base); return -1; } event_add(listen_ev, NULL); printf("Libevent 回显服务器启动,端口: %d\n", PORT); // 4. 启动事件循环 event_base_dispatch(base); // 5. 清理 event_free(listen_ev); event_base_free(base); close(listen_fd); return 0; }四、编译测试
gcc echo_server.c -o echo_server -levent
./echo_server# 另开终端测试
nc 127.0.0.1 6000
# 输入任意内容,应收到 "OK" 回复
第六部分:核心 API 速查表
一、event_base 相关
| API | 作用 |
|---|---|
event_base_new() | 创建事件循环引擎 |
event_base_dispatch(base) | 启动事件循环(阻塞) |
event_base_loopbreak(base) | 退出事件循环 |
event_base_free(base) | 释放引擎资源 |
二、event 相关
| API | 作用 |
|---|---|
event_new(base, fd, events, cb, arg) | 创建普通事件 |
evsignal_new(base, sig, cb, arg) | 创建信号事件 |
evtimer_new(base, cb, arg) | 创建定时事件 |
event_add(ev, timeout) | 注册事件到循环 |
event_del(ev) | 从循环中移除事件 |
event_free(ev) | 释放事件资源 |
三、回调函数格式
void callback(evutil_socket_t fd, short events, void *arg); // fd: 触发事件的文件描述符(信号事件为信号编号) // events:触发的事件类型(EV_READ、EV_TIMEOUT 等) // arg: 用户自定义参数第七部分:与原生 epoll 的对比
| 对比项 | 原生 epoll | Libevent |
|---|---|---|
| 代码量 | 需要手动管理一切 | 只需回调函数 |
| 跨平台 | 仅 Linux | Linux/macOS/Windows |
| 信号处理 | 需结合 signal() | 内置支持 |
| 定时器 | 需自己实现 | 内置支持 |
| 持久事件 | 需手动重新注册 | EV_PERSIST一行搞定 |
| 学习成本 | 高 | 低 |
| 性能 | 最优 | 接近原生(封装开销极小) |
总结
一、Libevent 使用流程
① event_base_new() → 创建引擎
② event_new() → 创建事件(指定 fd、类型、回调)
③ event_add() → 注册事件
④ event_base_dispatch() → 启动循环
⑤ event_free() + event_base_free() → 清理
二、事件类型速记
| 宏 | 用途 |
|---|---|
EV_READ | 可读 |
EV_WRITE | 可写 |
EV_TIMEOUT | 超时 |
EV_SIGNAL | 信号 |
EV_PERSIST | 持久(触发后保持) |
EV_ET | 边缘触发 |
三、回调函数签名
void callback(evutil_socket_t fd, short events, void *arg);四、一句话记忆
Libevent 封装了 epoll/select/poll,让你只需要写回调函数、注册事件、启动循环三步就能构建高性能网络服务器,同时内置了定时器和信号处理。