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的bufferevent
  • bufferevent_setcb:设置读回调、写回调、事件回调
  • bufferevent_enable:启用读写事件,默认是禁用的
  • bufferevent_readbufferevent_write:操作内部缓冲区

注意:BEV_OPT_CLOSE_ON_FREE这个选项很重要。如果不加,bufferevent释放时不会关闭底层socket,很容易造成文件描述符泄漏。我曾经在生产环境排查过一个连接数不断上涨的问题,最后发现就是忘了加这个选项。

知识体系总览

下面这张图把libevent的核心脉络梳理了一下。你看,从event_base出发,分出了事件注册和bufferevent两条主线,最终都汇聚到事件循环中。

libevent 核心知识体系 event_base 事件循环实例 事件注册 event_new / event_add bufferevent 带缓冲的socket包装 EV_READ EV_WRITE EV_TIMEOUT 读回调 写回调 事件回调 三者关系: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这三板斧,你已经能写出大部分网络程序了。剩下的,遇到具体需求再查文档也不迟。

公众号:蓝海资料掘金营,微信deep3321