You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Linux下C++ HTTP服务器epoll ET模式二次读事件无数据问题排查

问题:epoll ET模式下第二次读事件触发但无法读取数据,程序意外跳出死循环

问题描述

在Linux环境下用C++编写HTTP服务器,采用epoll的ET触发模式。首次触发读事件时成功读取首页请求数据,但第二次触发读事件(对应favicon.ico请求)时,同一个socket始终无法读取到数据。此外,程序中写了死循环,但偶尔会出现“跳出循环走到close代码”的情况,无法理解原因。

服务器端代码

void Server::serverListen()
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);

    sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);
    addr.sin_family = AF_INET;

    int tmp = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &tmp, sizeof(tmp));

    int ret = bind(listenfd, (sockaddr*)&addr, sizeof(addr));
    assert(ret >= 0);

    ret = listen(listenfd, 5);
    assert(ret >= 0);

    int epollfd = epoll_create(5);
    assert(epollfd != -1);
    epoll_event events;
    events.data.fd = listenfd;
    events.events = EPOLLIN;
    int n = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &events);
    assert(n != -1);
    epoll_event eve[10000];
    while (true)
    {
        int num = epoll_wait(epollfd, eve, 10000, -1);
        for (int i = 0; i < num; ++i)
        {
            int fd = events.data.fd;
            if (fd == listenfd)
            {
                sockaddr_in caddr;
                socklen_t tmplen = sizeof(caddr);

                connfd = accept(listenfd, (sockaddr*)&caddr, &tmplen);
                assert(connfd != -1);

                int flag = fcntl(connfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(connfd, F_SETFL, flag);

                epoll_event even;
                events.data.fd = connfd;
                events.events = EPOLLIN | EPOLLET;
                int n = epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &even);
                assert(n != -1);
            }
            else
            {
                worker.work(connfd, epollfd);
            }
        }

    }
    //Why do I still run here even though I write a dead loop program?
    //When I step into debugging, the program returns to the loop after running these two statements, so I commented them out
    //When I keep them, the program returns to the loop after running them and continues to trigger the first read event, which makes me unable to read any data even for the first read event
    //close(epollfd);
    //close(listenfd);
}

读数据函数代码

void Worker::work(const int& connfd, const int& epollfd)
{
    int len = 0,totle = 0;
    char buf[40960]{ '0' };
    char temp[40960]{ '0' };

    while ((len = recv(connfd, temp, sizeof(temp), NULL)) > 0)
    {
        if (totle + len < sizeof(buf))
        {
            memcpy(buf + totle, temp, len);
        }
        totle += len;
    }
    std::cout << "len:" << len << std::endl << "buf:" << buf << std::endl;
    if (len == -1 && errno == EAGAIN)
    {
        int ll = 0;
        parser.getLine(buf, tline);
        std::cout << "tline:" << tline << std::endl;
        parser.getStatus(tline, tstatus);
        parser.getFile(tline, tfile);
    //Omit the logic processing code after reading
    }
}

请求结果

首次请求结果

len:-1
buf:GET / HTTP/1.1

Host: 192.168.10.150:10000

Connection: keep-alive

Cache-Control: max-age=0

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.69

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

Accept-Encoding: gzip, deflate

Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6


num:820

第二次请求结果

len:-1
buf:0

问题原因及解决方案

1. 第二次读事件无法读取数据的核心错误

你的代码存在两个致命的变量使用错误:

  • 遍历epoll事件时引用了错误的变量:在for循环中,你用events.data.fd获取触发事件的fd,但events是初始化listenfd时的变量,正确的应该是eve[i].data.fd(eve是epoll_wait返回的事件数组)。这会导致第一次处理连接后,events.data.fd被覆盖为connfd,后续所有事件都错误地处理这个旧的fd。
  • 处理非监听事件时传错了fd:你直接传递类成员connfd给worker.work,而不是当前触发事件的eve[i].data.fd,这会导致连接信息混乱,即使是单个连接,也会因为前面的错误导致处理无效的fd。

修复方法:
修改服务器循环中的代码,正确引用事件数组中的fd,并传递正确的fd给work函数:

while (true)
{
    int num = epoll_wait(epollfd, eve, 10000, -1);
    if (num == -1) {
        perror("epoll_wait failed");
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i < num; ++i)
    {
        int fd = eve[i].data.fd; // 改为引用当前事件的fd
        if (fd == listenfd)
        {
            sockaddr_in caddr;
            socklen_t tmplen = sizeof(caddr);
            int new_connfd = accept(listenfd, (sockaddr*)&caddr, &tmplen);
            if (new_connfd == -1) {
                perror("accept failed");
                continue;
            }

            int flag = fcntl(new_connfd, F_GETFL);
            if (flag == -1) {
                perror("fcntl F_GETFL failed");
                close(new_connfd);
                continue;
            }
            flag |= O_NONBLOCK;
            if (fcntl(new_connfd, F_SETFL, flag) == -1) {
                perror("fcntl F_SETFL failed");
                close(new_connfd);
                continue;
            }

            epoll_event even;
            even.data.fd = new_connfd; // 用新的even变量,不要修改原events变量
            even.events = EPOLLIN | EPOLLET;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, new_connfd, &even) == -1) {
                perror("epoll_ctl add connfd failed");
                close(new_connfd);
                continue;
            }
        }
        else
        {
            worker.work(fd, epollfd); // 传递当前事件的fd
        }
    }
}

2. ET模式下读数据的逻辑隐患

  • 缓冲区溢出未处理:当totle + len >= sizeof(buf)时,你没有处理超出的数据,会导致数据丢失。需要增加溢出判断,直接关闭连接或扩容缓冲区。
  • 缓冲区初始化错误:用{'0'}初始化缓冲区会把每个字节设为ASCII码'0',而不是空字符,应该用{0}初始化,避免干扰后续的数据处理。
  • 未处理客户端关闭连接的情况:当recv返回0时,表示客户端已经关闭连接,需要主动关闭fd并从epoll中移除该事件。

修复后的work函数:

void Worker::work(const int& connfd, const int& epollfd)
{
    int len = 0,totle = 0;
    char buf[40960]{0}; // 用{0}初始化空字符
    char temp[40960]{0};

    while ((len = recv(connfd, temp, sizeof(temp), 0)) > 0)
    {
        if (totle + len <= sizeof(buf))
        {
            memcpy(buf + totle, temp, len);
        }
        else
        {
            fprintf(stderr, "buffer overflow, closing connection\n");
            close(connfd);
            epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, nullptr);
            return;
        }
        totle += len;
    }

    std::cout << "len:" << len << std::endl << "buf:" << buf << std::endl;

    if (len == 0)
    {
        std::cout << "client closed connection" << std::endl;
        close(connfd);
        epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, nullptr);
        return;
    }
    else if (len == -1)
    {
        if (errno == EAGAIN || errno == EWOULDBLOCK)
        {
            int ll = 0;
            parser.getLine(buf, tline);
            std::cout << "tline:" << tline << std::endl;
            parser.getStatus(tline, tstatus);
            parser.getFile(tline, tfile);
            // 处理完请求后,根据Connection头决定是否保持长连接
        }
        else
        {
            perror("recv failed");
            close(connfd);
            epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, nullptr);
        }
    }
}

3. 程序为何会“跳出死循环”?

你看到的“跳出死循环”并非真的退出了循环,而是断言失败导致程序崩溃。代码中大量使用assert,当accept、epoll_ctl等系统调用失败时,assert会触发,终止程序,此时的栈回溯可能让你误以为走到了close代码。

解决方法:

  • 替换assert为实际的错误处理逻辑,比如用perror打印错误信息,然后根据情况继续或退出。
  • assert只应该用于调试阶段的逻辑断言,生产环境中会被编译优化掉,不能依赖它处理系统调用错误。

内容的提问来源于stack exchange,提问作者wblxwz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 19:10:19