💖 点赞 + 收藏 + 关注,Socket 工业级网络编程全套实战持续更新! ✨ 本专栏 全程手写源码、无阉割、纯工程

0. 前言

前面章节我们从零手写了 select、poll、epoll 网络服务,掌握了事件驱动底层原理。但自研网络框架存在明显短板:跨平台适配成本极高(Linux epoll、Windows IOCP、macOS kqueue),还要自行处理定时器、信号、内存管理、各种系统兼容性坑。

在商用项目开发中,一般不会从零造完整轮子,而是选用成熟开源事件库:libevent、libuv。两个库都封装了底层 IO 多路复用接口,统一对外提供一套标准 API,自动适配不同操作系统内核。

  • libevent:老牌事件库,广泛用于 memcached、Nginx 早期模块,专注事件分发;
  • libuv:Node.js 底层核心库,除网络事件外,内置线程池、信号、定时器、文件 IO,功能更加全面,嵌入式、服务端都大量使用。

本章学习重点:

  1. libevent /libuv 架构对比、选型指南
  2. libevent 基础 API 讲解 + TCP 服务端完整 Demo
  3. libuv 核心概念(loop、handle、callback)+ TCP 回声服务 Demo
  4. 基于库封装简易通用 Reactor 网络组件
  5. 跨平台移植注意事项、编译链接、静态库打包
  6. 自研框架 vs libevent vs libuv 工程选型分析

前置说明:本章示例基于 Linux 环境编译演示,文末给出 Windows 交叉编译要点。

1. libevent & libuv 整体架构与选型对比

1.1 libevent 核心组成

libevent 核心组件:event_base(事件反应堆)、event(通用事件:IO / 定时器 / 信号)、bufferevent(封装缓冲区,简化 TCP 读写,自动处理半包) 底层自动适配:epoll /kqueue/devpoll /select/poll

优点:

  1. 轻量,专注事件驱动,体积小;
  2. bufferevent 封装 TCP 读写缓冲区,粘包半包处理开箱即用; 缺点:
  3. 多线程支持偏弱,没有内置线程池;
  4. 维护节奏放缓,新特性迭代慢;

1.2 libuv 核心组成

核心:uv_loop_t(事件循环),各类 handle 句柄(uv_tcp_t、uv_timer_t、uv_signal_t、uv_work_t) 底层自动适配:epoll、IOCP、kqueue、poll 优点:

  1. 功能齐全:网络、定时器、信号、异步文件 IO、内置线程池;
  2. 跨平台一致性更好,Node.js 生态背书,社区活跃;
  3. 支持异步回调模型,适合大型异步业务; 缺点:
  4. 学习曲线略陡,回调嵌套容易产生回调地狱;
  5. 二进制体积相比 libevent 更大;

1.3 工程选型建议

  1. 小型嵌入式网关、资源受限 MCU/Linux 工控板,仅需要网络事件:优先 libevent
  2. 大型后台服务、需要异步文件读写、多任务、跨 Windows/Linux 产品:优先 libuv
  3. 学习底层原理、极简 Demo 验证:手写 epoll/poll,不要直接上第三方库

2. libevent 完整实战(TCP 回声服务端)

2.1 环境安装(Ubuntu)

sudo apt install libevent-dev

2.2 核心 API 简介

  1. event_base_new():创建事件循环反应堆 event_base
  2. event_new():创建原始事件;bufferevent_socket_new() 创建带缓冲区 TCP 事件
  3. bufferevent_setcb():设置读、写、异常回调函数
  4. event_add():将事件注册进入事件循环
  5. event_base_dispatch():启动事件循环(阻塞运行)
  6. event_base_free()、bufferevent_free():资源释放

bufferevent 内部自带收发环形缓冲区,自动处理非阻塞读写、EAGAIN,不用手动处理半包,大幅简化业务代码。

2.3 libevent TCP 回声服务完整代码 libevent_tcp.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/listener.h>

#define LISTEN_PORT 8888

// 客户端连接可读回调
void ReadCallback(struct bufferevent *bev, void *ctx)
{
    struct evbuffer *input = bufferevent_get_input(bev);
    size_t len = evbuffer_get_length(input);
    if(len <= 0)
        return;

    char buf[1024] = {0};
    evbuffer_copyout(input, buf, sizeof(buf)-1);
    printf("recv data: %s\n", buf);

    // 回声:数据原样发回客户端
    bufferevent_write(bev, buf, len);
    // 清空输入缓冲区
    evbuffer_drain(input, len);
}

// 事件异常回调:断开、读写错误
void EventCallback(struct bufferevent *bev, short events, void *ctx)
{
    if(events & BEV_EVENT_EOF)
    {
        printf("client close connection\n");
    }
    else if(events & BEV_EVENT_ERROR)
    {
        perror("bufferevent error");
    }
    // 释放bufferevent资源
    bufferevent_free(bev);
}

// 监听器回调:收到新客户端连接
void AcceptCallback(struct evconnlistener *listener, evutil_socket_t fd,
                    struct sockaddr *addr, int socklen, void *ctx)
{
    struct event_base *base = ctx;
    // 创建带缓冲区的bev,绑定fd
    struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
    if(!bev)
    {
        evutil_closesocket(fd);
        return;
    }
    // 设置读写、异常回调
    bufferevent_setcb(bev, ReadCallback, NULL, EventCallback, NULL);
    // 开启缓冲区自动水位
    bufferevent_enable(bev, EV_READ | EV_WRITE);
    printf("new client connect\n");
}

int main(void)
{
    struct event_base *base = event_base_new();
    if(!base)
    {
        printf("event_base create fail\n");
        return -1;
    }

    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(LISTEN_PORT);
    sin.sin_addr.s_addr = htonl(INADDR_ANY);

    // 创建监听器
    struct evconnlistener *listener = evconnlistener_new_bind(base, AcceptCallback, base,
        LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 5,
        (struct sockaddr*)&sin, sizeof(sin));

    if(!listener)
    {
        perror("evconnlistener create fail");
        event_base_free(base);
        return -1;
    }

    printf("libevent tcp server running on port %d\n", LISTEN_PORT);
    // 启动事件循环
    event_base_dispatch(base);

    evconnlistener_free(listener);
    event_base_free(base);
    return 0;
}

编译 & 运行命令:

gcc libevent_tcp.c -o libevent_tcp -levent
./libevent_tcp

3. libuv 完整实战(TCP 回声服务端)

3.1 环境安装(Ubuntu)

sudo apt install libuv1-dev

3.2 libuv 核心概念

  1. uv_loop_t:事件循环主体,每个程序至少一个 loop,uv_default_loop () 获取默认主循环
  2. handle:各类句柄(uv_tcp_t TCP 句柄、uv_timer_t 定时器句柄)
  3. callback:所有事件都是异步回调驱动
  4. uv_run ():启动事件循环

libuv 所有 IO 默认非阻塞,开发者需要自行管理收发缓冲区(没有内置 bufferevent)

3.3 libuv TCP 回声服务完整代码 libuv_tcp.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>

#define LISTEN_PORT 8888
#define BUF_SIZE 1024

// 客户端连接上下文
typedef struct
{
    uv_tcp_t client;
    char buffer[BUF_SIZE];
} ClientCtx;

// 释放客户端资源
void FreeClient(ClientCtx *ctx)
{
    if(ctx)
    {
        uv_close((uv_handle_t*)&ctx->client, NULL);
        free(ctx);
    }
}

// 客户端数据读取回调
void OnRead(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf)
{
    ClientCtx *ctx = (ClientCtx*)client->data;
    if(nread > 0)
    {
        printf("recv data: %.*s\n", (int)nread, buf->base);
        // 回声发送
        uv_buf_t send_buf = uv_buf_init(buf->base, nread);
        uv_write_t *write_req = malloc(sizeof(uv_write_t));
        uv_write(write_req, client, &send_buf, 1, NULL);
        free(write_req);
    }
    else if(nread <= 0)
    {
        if(nread < 0)
        {
            printf("client disconnect or read error\n");
        }
        FreeClient(ctx);
    }
}

// 新连接回调
void OnAccept(uv_stream_t *server, int status)
{
    if(status < 0)
    {
        printf("accept error\n");
        return;
    }
    ClientCtx *ctx = malloc(sizeof(ClientCtx));
    uv_tcp_init(uv_default_loop(), &ctx->client);
    ctx->client.data = ctx;

    if(uv_accept(server, (uv_stream_t*)&ctx->client) == 0)
    {
        uv_buf_t buf = uv_buf_init(ctx->buffer, BUF_SIZE);
        uv_read_start((uv_stream_t*)&ctx->client, uv_alloc_buf, OnRead);
        printf("new client connected\n");
    }
    else
    {
        FreeClient(ctx);
    }
}

int main(void)
{
    uv_tcp_t server;
    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", LISTEN_PORT, &addr);

    uv_tcp_init(uv_default_loop(), &server);
    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);

    int r = uv_listen((uv_stream_t*)&server, 5, OnAccept);
    if(r)
    {
        printf("listen fail: %s\n", uv_strerror(r));
        return -1;
    }
    printf("libuv tcp server running on port %d\n", LISTEN_PORT);
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
    uv_loop_close(uv_default_loop());
    return 0;
}

编译 & 运行命令:

gcc libuv_tcp.c -o libuv_tcp -luv
./libuv_tcp

4. 简易封装:基于 libevent 实现通用 Reactor 组件

实际项目不会零散调用原生 API,需要二次封装:连接管理、心跳定时器、统一日志、断线重连。 封装分层思路:

  1. 底层:libevent event_base、bufferevent 事件管理
  2. 中层:ConnContext 连接上下文封装(fd、心跳计时、收发缓存、连接状态)
  3. 上层:业务回调注册(收到数据回调、连接断开回调)
  4. 工具层:统一定时器封装、内存池分配(上一章实现的内存池)

核心封装接口示例:

// 业务回调函数类型定义
typedef void (*RecvDataCb)(void *ctx, uint8_t *data, uint16_t len);
typedef void (*ConnCloseCb)(void *ctx);

// 对外统一接口
int ReactorInit(uint16_t port, RecvDataCb recv_cb, ConnCloseCb close_cb);
void ReactorLoopRun(void);
int ReactorSendData(void *conn_handle, uint8_t *data, uint16_t len);
void ReactorDeinit(void);

封装好处:底层 libevent 可以无缝替换为 libuv,上层业务代码无需改动,实现底层框架可插拔。

5. 跨平台移植要点、静态库编译

5.1 Windows 移植注意

  1. Windows 没有 epoll,libevent/libuv 底层自动切换为 IOCP;
  2. socket 句柄类型从 int 变为 SOCKET,关闭使用 closesocket,不能直接 close;
  3. 编译工具:MinGW 或者 MSVC,推荐静态编译,避免依赖系统 dll;
  4. Windows 下默认 fd 数量限制、权限防火墙需要额外处理。

5.2 静态库编译(嵌入式常用)

  1. 下载 libevent /libuv 源码包
  2. ./configure --prefix=output --disable-shared (只编译静态库)
  3. make && make install
  4. 工程链接 output/lib 下的.a 静态库,头文件引用 output/include 优点:固件发布不需要部署动态 so 库,避免嵌入式环境库缺失问题。

6. 自研网络框架 vs libevent vs libuv 优缺点汇总

方案优点缺点适用场景
手写 epoll/poll 自研完全可控,无第三方依赖,体积最小跨平台工作量巨大,各类兼容性 bug 需要自行修复专用嵌入式设备,资源极度受限,开发人力充足
libevent轻量,bufferevent 简化 TCP,成熟稳定功能偏少,多线程支持弱工控网关、小型 TCP 透传服务
libuv全功能,内置线程池 / 定时器,跨平台优秀回调嵌套,体积更大后台服务、Node.js 插件、Windows/Linux 跨平台产品

7. 工程高频踩坑汇总

  1. libevent bufferevent_free 必须配合 BEV_OPT_CLOSE_ON_FREE,否则 fd 泄漏;
  2. libuv 所有 handle 必须调用 uv_close 释放,否则内存句柄泄漏;
  3. 回调内部禁止执行长时间阻塞操作(文件读写、数据库查询),会阻塞整个事件循环;阻塞任务投递内置线程池;
  4. 多线程操作同一个 event_base /uv_loop,必须加锁保护,事件库主线循环一般不推荐多线程并发操作;
  5. 静态编译时注意依赖库顺序,gcc 链接顺序从右往左,依赖库放在目标文件后方;
  6. Windows 静态编译经常缺少 ws2_32 等系统库链接,编译报错需要补充系统依赖库。

8. 本章小结

本章对比了两大主流跨平台事件驱动库 libevent 和 libuv,提供可直接编译运行的 TCP 服务 Demo,讲解二次封装 Reactor 组件思路、静态编译、跨平台移植规范。 核心要点:

  1. libevent 主打轻量事件分发,bufferevent 简化 TCP 半包处理;libuv 功能完整,内置线程池,跨平台能力更强;
  2. 第三方库本质是封装底层 IO 多路复用,底层原理依然是前面学的 select/poll/epoll,底层基础不能跳过;
  3. 项目推荐分层封装,业务层和底层网络解耦,方便后续切换网络组件;
  4. 量产嵌入式项目优先静态编译库,减少动态库依赖带来的部署问题。

遗留思考: 整套 Socket 网络体系学习完成后,工业通信项目还有大量配套知识:协议编解码、日志框架、配置解析、OTA 升级、性能压测、网络抓包调试。下一章:网络压测、Wireshark 抓包实战、性能瓶颈定位与调优,完成整套网络开发闭环。

下一章:网络压测、Wireshark 抓包实战、性能瓶颈定位与调优,网络开发全流程闭环

 

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐