epoll 异步 IO

为什么要使用epoll,直接使用原生socket,用原生api不可以吗,使用epoll有什么好处?

只用原生 socket、while 循环阻塞 recvfrom,语法上完全能跑,不是不能用。但工业场景下有明显短板;epoll 不是 socket 替代品,只是【IO 事件多路复用器】,它依然调用原生 socket API,只是解决 "如何同时等待多个 socket 可读" 的问题。


先理清层级关系:

cpp 复制代码
socket() → bind() → recvfrom() 【底层原生套接字API,必用】
epoll/select/poll 【多路复用机制,可选,用来同时监听多个fd】

方案 1:最简单粗暴写法 ------ 两个独立线程,各自阻塞 recvfrom(很多新手第一想法)

cpp 复制代码
//线程1
while(1){
    recvfrom(fd1, buf, len, 0, ...); 
    //阻塞在这里,等到192.9.2.100的数据才返回
    process();
}
//线程2
while(1){
    recvfrom(fd2, buf, len, 0, ...);
    //阻塞在这里,等到192.9.10.1的数据才返回
    process();
}

✅优点:代码最简单,不需要 epoll

❌缺点(工业项目痛点):

  1. 多线程开销 :两个线程就要两套栈、内核调度开销;如果后续扩展更多 UDP 端口,线程数量持续上涨;
  2. 线程间同步麻烦 :两路数据如果需要共享变量、共享缓冲区,必须加锁,极易出现竞态、死锁;
  3. 资源管理复杂:退出时需要安全通知两个线程结束,处理线程回收;
  4. 大量空闲等待:多数时间线程休眠,资源浪费。

方案 2:单线程,轮询非阻塞 recvfrom(不使用任何多路复用)

cpp 复制代码
while(1)
{
    recvfrom(fd1, buf, len, MSG_DONTWAIT,...);
    recvfrom(fd2, buf, len, MSG_DONTWAIT,...);
    usleep(1000);
}

❌致命缺陷:轮询(忙轮询)

  • 不加 sleep:CPU 占用 100%;
  • 加 sleep:引入固定延迟,实时性变差;
  • 无法感知什么时候真正有数据,只能盲等。

方案 3:多路复用(epoll /poll/select)【推荐方案】

单线程,阻塞等待,内核通知:哪个 socket 来了数据

没有数据时,进程完全休眠,CPU 占用≈0;

有 UDP 报文到达,内核唤醒程序,执行 recvfrom 读取。


epoll、select、poll 三者简单对比(Linux)

方式 核心特点 工业场景适用性
select 老旧标准,fd 有最大上限 (1024),每次调用要重置 fd 集合,内核频繁拷贝,性能差 不推荐长期运行工控软件
poll 无 1024 上限,但依旧每次遍历全部 fd,fd 数量变多性能线性下降 少量 fd 勉强可用
epoll Linux 特有,事件驱动。内核维护就绪列表,只返回有数据的 fd。fd 数量增加性能几乎不衰减 ✅工控首选

你当前场景只有 2 个 UDP fd,哪怕 select/poll 都够用。

使用 epoll 本质是工程扩展性,现在两路,将来扩展成 4 路、8 路、16 路 UDP,代码不用大改。


运行平台:银河麒麟 ARM(Linux)

原生 C、UDP、双 socket:

  • sock1 bind:192.9.2.100:PORT
  • sock2 bind:192.9.10.1:PORT
  • 采用 epoll 异步 IO(工业首选,性能远高于 select,适合长时间稳定运行)

核心特性:

  1. 一个 epoll 实例同时监听两个 UDP fd;
  2. 任意 socket 有数据到达立即触发回调
  3. 使用recvfrom获取发送方地址,支持后续应答;
  4. 代码包含错误处理、端口复用、资源释放;
  5. 单网卡多 IP 环境直接可用,前提:网卡预先配置好两个 IP

重要原理:socket 显式绑定独立本地 IP → 内核自动根据目标 IP把数据包投递到对应 fd,两个套接字流量天然隔离。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define UDP_PORT        8080
#define EPOLL_MAX_EVENTS 8
#define BUF_LEN         1024

// 两个本地绑定IP
#define IP_BIND1    "192.9.2.100"
#define IP_BIND2    "192.9.10.1"

// 设置fd为非阻塞
static int set_nonblock(int fd)
{
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1)
        return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

// 创建UDP socket并绑定指定IP
static int create_udp_socket(const char *local_ip, uint16_t port)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0)
    {
        perror("socket create failed");
        return -1;
    }

    // 地址复用,同端口不同IP可以bind
    int opt = 1;
    setsockopt(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);
    if (inet_pton(AF_INET, local_ip, &addr.sin_addr) <= 0)
    {
        perror("inet_pton invalid ip");
        close(fd);
        return -1;
    }

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

    // epoll配合必须非阻塞
    if (set_nonblock(fd) < 0)
    {
        perror("set nonblock failed");
        close(fd);
        return -1;
    }

    printf("UDP socket bind success: %s:%d\n", local_ip, port);
    return fd;
}

// 数据接收处理回调
static void handle_udp_data(int fd, const char *bind_ip)
{
    char buf[BUF_LEN];
    struct sockaddr_in client_addr;
    socklen_t addr_len = sizeof(client_addr);

    ssize_t n = recvfrom(fd, buf, BUF_LEN - 1, 0,
                         (struct sockaddr *)&client_addr, &addr_len);
    if (n <= 0)
    {
        if (errno != EAGAIN && errno != EWOULDBLOCK)
            perror("recvfrom error");
        return;
    }

    buf[n] = '\0';
    char client_ip[32];
    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));

    printf("==============================\n");
    printf("Local Bind IP: %s\n", bind_ip);
    printf("Remote: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
    printf("Recv Data len:%zd, content: %.*s\n", n, (int)n, buf);

    /*
    // 如需回包示例:sendto(fd, reply_buf, len, 0, (struct sockaddr*)&client_addr, addr_len);
    */
}

int main(void)
{
    int epoll_fd;
    int sock_fd1, sock_fd2;

    // 创建epoll实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd < 0)
    {
        perror("epoll_create1");
        return -1;
    }

    // 创建两个UDP套接字,分别绑定两个IP
    sock_fd1 = create_udp_socket(IP_BIND1, UDP_PORT);
    sock_fd2 = create_udp_socket(IP_BIND2, UDP_PORT);
    if (sock_fd1 < 0 || sock_fd2 < 0)
    {
        close(sock_fd1);
        close(sock_fd2);
        close(epoll_fd);
        return -1;
    }

    // epoll事件结构体
    struct epoll_event ev;
    memset(&ev, 0, sizeof(ev));

    // 添加sock1到epoll,data.ptr携带绑定IP标识(区分来源)
    ev.data.ptr = IP_BIND1;
    ev.events = EPOLLIN; // 可读事件
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd1, &ev);

    // 添加sock2到epoll
    ev.data.ptr = IP_BIND2;
    ev.events = EPOLLIN;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd2, &ev);

    struct epoll_event events[EPOLL_MAX_EVENTS];
    printf("epoll waiting udp data...\n");

    while (1)
    {
        // -1 阻塞等待事件
        int nready = epoll_wait(epoll_fd, events, EPOLL_MAX_EVENTS, -1);
        if (nready < 0)
        {
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < nready; i++)
        {
            // 获取当前触发事件对应的绑定IP字符串
            const char *local_bind_ip = (const char *)events[i].data.ptr;
            handle_udp_data(events[i].data.fd, local_bind_ip);
        }
    }

    // 正常退出清理(工业程序一般常驻进程,信号捕获后执行)
    close(sock_fd1);
    close(sock_fd2);
    close(epoll_fd);
    return 0;
}
相关推荐
chen<>11 天前
网络 IO 多路复用万字深度详解:select/poll/epoll/io_uring
网络·select·epoll·io_uring
Dlrb12111 个月前
信息查询Web服务器-->电子商城信息查询项目
linux·服务器·数据库·html·嵌入式·epoll·数据查询
阿pin1 个月前
Android随笔-epoll机制
android·epoll
minji...2 个月前
Linux 高级IO(七)多进程、多线程的Reactor反应堆模式扩展、OTOL
linux·运维·c++·多路转接·epoll·reactor反应堆模型
minji...2 个月前
Linux高级IO(六)基于ET模式、单reactor反应堆的epoll版本的TCP计算服务器
linux·服务器·网络·c++·epoll·socket套接字·reactor反应堆模式
minji...2 个月前
Linux 高级IO(四)多路转接之epoll,epoll 模型及原理
linux·运维·服务器·多路转接·epoll·epoll模型·红黑树/就绪队列/回调
minji...2 个月前
Linux高级IO(五)epoll 的两种工作模式(LT/ET),多路转接之epoll版本的TCP服务器,对比 select/poll/epoll
linux·运维·服务器·epoll·epoll的工作模式·selectpollepoll·水平触发边缘触发
磊 子2 个月前
详细讲解一下epoll
linux·io·epoll·io多路复用
minji...2 个月前
Linux 高级IO(三)多路转接之poll,poll的原理,poll版本的TCP服务器的实现
linux·服务器·网络·select·多路转接·epoll·poll