10、Reactor模式与Libevent:Reactor设计模式、事件循环、Libevent库入门、基于Libevent的HTTP服务器

说到网络编程,绕不开一个经典模式——Reactor。我刚开始做网络服务时,用的是最原始的多线程,每个连接一个线程。结果呢?连接数一上去,系统直接崩了。后来我才明白,Reactor模式才是解决高并发IO的钥匙。

说白了,Reactor模式就是一个事件分发器。它不主动干活,而是等着事件来了再处理。你想想看,这比轮询高效多了吧?

Reactor设计模式的核心思想

Reactor模式包含几个关键角色:

  • 事件源:就是那些文件描述符,比如socket
  • Reactor:事件循环的核心,负责监听和分发
  • 事件处理器:处理具体事件的回调函数

我习惯把Reactor比作一个前台接待员。客户来了(事件发生),接待员喊一声(分发),对应的业务员来处理(回调)。这样接待员一个人就能处理很多客户,不用每个客户配一个业务员。

核心要点:Reactor模式将IO等待和业务处理分离,一个线程就能管理成千上万个连接。

Reactor模式架构图 Reactor 事件循环 + 分发 Socket 1 (读事件) Socket 2 (写事件) Socket N (异常事件) 读事件处理器 写事件处理器 异常事件处理器 事件源 → Reactor(事件通知)→ 事件处理器(业务逻辑)

事件循环:Reactor的心脏

事件循环就是那个一直在运行的while循环。它调用select/poll/epoll等系统调用,等待事件发生。一旦有事件,就调用对应的回调函数。

伪代码大概是这样的:

while (1) {
    // 等待事件,超时时间设为NULL表示一直等
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    
    for (int i = 0; i < n; i++) {
        // 获取事件类型和对应的回调
        struct epoll_event *ev = &events[i];
        event_handler_t handler = ev->data.ptr;
        
        // 调用回调函数处理事件
        handler(ev->events);
    }
}

嗯,这里要注意:事件循环不能阻塞太久。如果某个回调函数执行了耗时操作,整个循环就卡住了。我在项目中就吃过这个亏,一个数据库查询把事件循环堵了3秒,所有连接都超时了。

避坑指南:事件循环中的回调函数必须是非阻塞的。如果需要执行耗时操作,要么异步处理,要么扔到线程池里。

Libevent库入门

自己手写Reactor太累了,还要处理跨平台问题。Libevent就是帮你干这活的。它封装了epoll、kqueue、select等底层机制,提供统一的API。

Libevent的核心数据结构:

结构体 作用
struct event_base 事件循环的上下文,相当于Reactor实例
struct event 代表一个事件,包含fd、事件类型、回调函数
struct bufferevent 带缓冲的事件,自动处理读写缓冲区
struct evhttp HTTP服务器封装,基于bufferevent

我刚开始用Libevent时,觉得API有点多。但用顺手了就会发现,它把网络编程的脏活累活都干了。你只需要注册回调,剩下的Libevent帮你搞定。

基于Libevent的HTTP服务器

下面是一个完整的HTTP服务器示例。它监听8080端口,收到请求后返回"Hello, World!"。

#include <event2/event.h>
#include <event2/http.h>
#include <event2/buffer.h>
#include <event2/keyvalq_struct.h>
#include <stdio.h>
#include <string.h>

// 请求处理回调
void request_handler(struct evhttp_request *req, void *arg) {
    struct evbuffer *buf = evbuffer_new();
    if (!buf) {
        evhttp_send_error(req, HTTP_INTERNAL, "Memory error");
        return;
    }
    
    // 构造响应内容
    const char *response = "<html><body><h1>Hello, World!</h1>"
                           "<p>This is a Libevent HTTP server.</p>"
                           "</body></html>";
    
    evbuffer_add_printf(buf, "%s", response);
    
    // 设置响应头
    evhttp_add_header(evhttp_request_get_output_headers(req),
                      "Content-Type", "text/html; charset=utf-8");
    
    // 发送响应
    evhttp_send_reply(req, HTTP_OK, "OK", buf);
    
    evbuffer_free(buf);
}

int main() {
    // 初始化事件循环
    struct event_base *base = event_base_new();
    if (!base) {
        fprintf(stderr, "Could not create event base\n");
        return 1;
    }
    
    // 创建HTTP服务器
    struct evhttp *http = evhttp_new(base);
    if (!http) {
        fprintf(stderr, "Could not create HTTP server\n");
        return 1;
    }
    
    // 设置默认请求处理函数
    evhttp_set_gencb(http, request_handler, NULL);
    
    // 绑定端口
    if (evhttp_bind_socket(http, "0.0.0.0", 8080) != 0) {
        fprintf(stderr, "Could not bind to port 8080\n");
        return 1;
    }
    
    printf("HTTP server started on port 8080...\n");
    
    // 进入事件循环
    event_base_dispatch(base);
    
    // 清理
    evhttp_free(http);
    event_base_free(base);
    
    return 0;
}

编译命令:

gcc -o http_server http_server.c -levent -levent_core

小技巧:Libevent的HTTP服务器支持路由吗?原生不支持,但你可以通过解析请求URI自己实现。我一般用evhttp_request_get_uri()获取路径,然后做字符串匹配。

Libevent的高级特性

除了基本的HTTP服务器,Libevent还提供了很多实用功能:

  • 定时器:用event_new()加EV_TIMEOUT标志,实现定时任务
  • 信号处理:用evsignal_new()捕获系统信号
  • DNS解析:用evdns_getaddrinfo()异步解析域名
  • SSL支持:用bufferevent_openssl_new()加密通信

我记得有一次做游戏服务器,需要定时发送心跳包。用Libevent的定时器,几行代码就搞定了。要是自己写定时器,还得维护一个时间堆,麻烦得很。

性能调优建议

用Libevent做服务器,有几个地方要注意:

  1. 事件循环数量:一般一个进程一个event_base就够了。如果CPU核数多,可以开多个,但要注意负载均衡
  2. 缓冲区大小:bufferevent默认缓冲区是16KB,可以根据业务调整
  3. 连接超时:用bufferevent_set_timeouts()设置读写超时,防止连接泄漏
  4. 内存管理:Libevent内部有内存池,但回调函数里分配的内存要自己释放

避坑指南:我曾经在生产环境遇到过一个问题——Libevent的event_base_dispatch()在信号中断后会返回。如果你在循环里调用它,记得检查返回值,必要时重新进入事件循环。

好了,Reactor模式和Libevent就讲到这里。说白了,Reactor就是事件驱动,Libevent就是帮你实现事件驱动的工具。掌握了这些,写高性能网络服务就不再是难事了。


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