土法炼钢兴趣小组的算法知识备份

UDP 通信编程

目录

在网络编程中,TCP 占据了主导地位,但 UDP 在实时音视频、DNS 解析、QUIC 等场景中依然不可或缺。

很多初学者习惯了 Libevent 的 bufferevent 抽象,但在处理 UDP 时会发现无从下手。本文将讲解如何在 Libevent 中正确地处理 UDP。

1. 为什么 Bufferevent 不适合 UDP?

Libevent 的 bufferevent 是为 流式 (Stream) 协议设计的: * 它假设数据是连续的字节流。 * 它自动处理缓冲区的拼接和移动。 * 它处理“连接”状态(Connected, Disconnected)。

而 UDP 是 数据报 (Datagram) 协议: * 数据是独立的包,有明确的边界。 * 没有“连接”的概念(除非使用 connect() 绑定目标,但这只是内核状态)。 * 一个 UDP Socket 通常用于与多个客户端通信(多对一)。

虽然 Libevent 历史上曾尝试过 UDP bufferevent 的支持,但在实际开发中,直接使用原始的 struct event 是处理 UDP 的标准做法

2. 基于原始 Event 的 UDP 服务端

实现一个 UDP Server 的核心步骤非常简单: 1. 创建 UDP Socket。 2. 绑定端口 (bind)。 3. 创建一个 EV_READ | EV_PERSIST 的事件监听该 Socket。 4. 在回调函数中使用 recvfrom 读取数据,用 sendto 回复数据。

2.1 完整示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <event2/event.h>

#define PORT 9999
#define BUF_SIZE 1024

void on_read(evutil_socket_t fd, short events, void *arg) {
    char buf[BUF_SIZE];
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    ssize_t len;

    // 1. 读取数据包
    // 注意:UDP 是保留消息边界的,一次 recvfrom 对应一个包
    len = recvfrom(fd, buf, BUF_SIZE - 1, 0, 
                   (struct sockaddr *)&client_addr, &client_len);

    if (len < 0) {
        perror("recvfrom");
        return;
    }

    buf[len] = '\0';
    printf("Received %zd bytes from %s:%d: %s\n", 
           len, 
           inet_ntoa(client_addr.sin_addr), 
           ntohs(client_addr.sin_port), 
           buf);

    // 2. 回复数据 (Echo)
    sendto(fd, buf, len, 0, (struct sockaddr *)&client_addr, client_len);
}

int main() {
    struct event_base *base;
    struct event *udp_event;
    evutil_socket_t fd;
    struct sockaddr_in sin;

    // 1. 初始化 Libevent
    base = event_base_new();
    if (!base) return 1;

    // 2. 创建 UDP Socket
    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        perror("socket");
        return 1;
    }

    // 设置为非阻塞 (这是 Libevent 的要求)
    evutil_make_socket_nonblocking(fd);

    // 3. 绑定端口
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(PORT);

    if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
        perror("bind");
        return 1;
    }

    // 4. 创建并添加事件
    // 监听 EV_READ,并设置 EV_PERSIST 保持持续监听
    udp_event = event_new(base, fd, EV_READ | EV_PERSIST, on_read, NULL);
    event_add(udp_event, NULL);

    printf("UDP Server listening on port %d\n", PORT);

    // 5. 进入事件循环
    event_base_dispatch(base);

    // 清理
    event_free(udp_event);
    evutil_closesocket(fd);
    event_base_free(base);
    return 0;
}

3. 关键细节与陷阱

3.1 缓冲区大小

UDP 接收缓冲区大小有限。如果你的处理速度跟不上发包速度,内核会丢弃数据包。 * 可以使用 setsockopt(fd, SOL_SOCKET, SO_RCVBUF, ...) 增大内核缓冲区。 * 在回调中,尽量快速处理或将数据拷贝到队列中异步处理,避免阻塞 Event Loop。

3.2 消息截断

recvfrom 提供的 buffer 必须足够大以容纳最大的预期数据包(通常是 MTU 限制,互联网上建议控制在 512 或 1400 字节以内)。如果包超过 buffer 大小,多余部分会被丢弃(且设置 MSG_TRUNC 标志)。

3.3 并发模型 (SO_REUSEPORT)

在高并发场景下,单个线程处理 UDP 可能会成为瓶颈。 Linux 3.9+ 支持 SO_REUSEPORT。你可以启动多个进程/线程,每个都创建自己的 event_base,绑定到同一个端口。内核会自动进行负载均衡,将数据包分发给不同的线程。

int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
// 多个线程都执行这段 bind 代码
bind(fd, ...);

4. 总结

完整代码: 04-udp-echo.c

返回 Libevent 专题索引


By .