Project Demo

← 返回项目列表

基于 Reactor 模型的高并发 C++ 网络服务器

基于 Linux + epoll + 线程池实现轻量级 Reactor 模型 HTTP 服务器,支持并发连接接入、HTTP GET 请求解析、 静态资源返回、长连接管理与超时回收。

Reactor epoll Socket HTTP C++ Linux

项目核心架构:基于 Reactor 的事件循环

事件循环
C++
while (true) {
    int nReady = epoll_wait(...);
    // 将内核态就绪队列中就绪的 FD 和相关状态
    // 拷贝到用户态的 events[MAX_EVENTS] 数组
    for (int i = 0; i < nReady; i++) {
        if (是新连接请求)   → handleAccept();  // 接受新连接
        if (是可读事件) → handleRead();    // 读取并解析 HTTP 请求
        if (是可写事件) → handleWrite();   // 拼装并返回 HTTP 响应
    }
}
对于请求连接事件,使用 LT 水平触发
对于可读可写事件,使用 ET 边缘触发
可读和可写事件使用 EPOLLIN(可读)/ EPOLLOUT(可写)标记状态,并将可读|可写任务加入线程池;
使用 EPOLLET 标记状态和设置 非阻塞,实现 ET 边缘触发。

epoll 实现 I/O 多路复用

由 Linux 内核提供的 I/O 多路复用实现,在内核中创建并维护一棵 红黑树 和一个 就绪队列(双向链表);
红黑树的增删改查时间复杂度为 O(logN),优于 select 的位图和 poll 的数组。

epoll_create —— 在内核中创建并维护红黑树和就绪队列:
epoll_create
C++
int epollFd = epoll_create(1);
epoll_ctl —— 将新事件的 fd 加入红黑树或从红黑树中移除:
epoll_ctl
C++
struct epoll_event ev;
ev.events  = EPOLLIN | EPOLLET | EPOLLONESHOT;
ev.data.fd = clientFd;
epoll_ctl(epollFd, op, clientFd, &ev);
// op: EPOLL_CTL_ADD 添加 | EPOLL_CTL_MOD 修改 | EPOLL_CTL_DEL 删除
epoll_wait —— 将就绪队列中 fd 对应的事件及相关信息从内核态写到用户态的 events 数组:
epoll_wait
C++
struct epoll_event events[MAX_EVENTS];
int nReady = epoll_wait(epollFd, events, MAX_EVENTS, waitTime);
// nReady  : 就绪事件数量
// waitTime: 超时时间(ms),-1 为永久阻塞,0 为立即返回
epoll_event 结构体中 events 字段的标志位:
epoll_event 结构体
C++
struct epoll_event {
    uint32_t     events;  // 事件标志位EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_data_t data;    // 在 data.fd 中存入事件的 fd
};
常用标志组合
C++
EPOLLIN | EPOLLET | EPOLLONESHOT;
  • EPOLLIN:标志可读事件,用于触发 HTTP 请求读取与解析。
  • EPOLLOUT:标志可写事件,用于触发 HTTP 响应的发送。
  • EPOLLET:标志边缘触发,clientFd 事件使用边缘触发,每次 HTTP 请求必须一次读完、一次写完。
  • EPOLLONESHOT:将同一个 fd 的任务交给线程后,标记该 fd 暂时失效,后续该 fd 的其他任务不返回就绪队列,避免资源竞争造成死锁
    例如:客户端发送数据,该 clientFd = n 可读,主线程交给线程 A 处理;如果 n 又发送一个请求,没有 EPOLLONESHOT 将会把 n 交给线程 B 处理,此时线程 A 和 B 处理同一个 n,数据产生错乱。

⑤ 关键错误码与非阻塞设置:
错误码
C++
// EAGAIN == EWOULDBLOCK == 11
// 表示内核读缓冲区为空 或 内核写缓冲区已满
if (errno == EAGAIN || errno == EWOULDBLOCK) {
    break;  // 数据已全部读完 / 写缓冲区满,退出循环
}
O_NONBLOCK:配合 ET 边缘触发模式,设置非阻塞标志位,当读缓冲区为空时退出循环不会阻塞线程:
设置非阻塞
C++
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
// 设置非阻塞后,read() 在无数据时立即返回 -1 并设 errno = EAGAIN

Socket 实现TCP网络通信

Socket 是操作系统提供的网络通信抽象接口,它封装了底层 TCP/IP 协议栈的细节,让应用层可以像读写文件一样进行网络数据传输。(在Linux中everything is a file.)

客户端:【创建套接字socket()】→【三次握手请求连接connect()】→【收发数据revc/send()】→【四次挥手关闭套接字close()】


服务端:【创建套接字socket()】→【绑定端口bind()】→【监听连接listen()】→【接受连接accept()】→【收发数据revc/send()】→【四次挥手关闭套接字close()】


服务端 Socket 通信的核心流程:创建 → 绑定 → 监听 → 接受连接

wrk测试并发性能

function:handleRead()
C++
wrk -t4 -c100 -d10s http://127.0.0.1:8080/   //100并发用户不间断访问10s
wrk -t4 -c500 -d10s http://127.0.0.1:8080/   //500并发用户不间断访问10s
测试结果:100并发 服务端运行截图

测试结果:500并发 服务端运行截图
测试结果对比
指标 100 并发 500 并发
QPS58575965
平均延迟25.37ms49.22ms
最大延迟111.91ms1.74s
总请求数5867159802
超时046

核心代码:addFd(),将初始的listenfd和监听到的clientfd加入epoll红黑树

function:addFd()
C++
void WebServer::eventLoop() {
    void addFd(int epollFd, int fd, bool isListen = false) {
    epoll_event ev;
    ev.data.fd = fd;
    if (isListen) {
        ev.events = EPOLLIN;
    } else {
        ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    }
    epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &ev);
    setNonBlocking(fd);
}        
功能:将检测到的fd加入内核红黑树,如果是服务器socket的监听fd,标志为可读;如果是客户端请求fd(请求连接||http请求),标志为可读|边沿触发|一次触发。将所有检测的fd都设置为非阻塞,来配合客户端请求fd的边缘触发模式。

核心代码:writeAll(),保证一次写完http回复

function:writeAll()
C++
class ThreadPool {
ssize_t writeAll(int fd, const char* data, size_t len) {
    size_t totalWritten = 0;
    while (totalWritten < len) {
        ssize_t n = write(fd, data + totalWritten, len - totalWritten);
        if (n > 0) {
            totalWritten += n;
        } else if (n < 0) {
            if (errno == EINTR) {
                // 被信号中断,继续写
                continue;
            } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 非阻塞模式下缓冲区满了,短暂等待后重试
                usleep(100);
                continue;
            } else {
                // 真正的错误
                LOG_ERROR("write error fd=%d errno=%d", fd, errno);
                return -1;
            }
        }
    }
    return totalWritten;
}
功能:配合边缘触发模式,保证不出现写缓冲区满而写失败,必须一次写完http回复

核心代码:handleAccept(),处理客户端http连接请求

function:handleAccept()
C++
void handleAccept(int listenFd, int epollFd) {
    struct sockaddr_in clientAddr;
    socklen_t clientLen = sizeof(clientAddr);
    int clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, &clientLen);
    if (clientFd < 0) {
        LOG_ERROR("accept failed");
        return;
    }
    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &clientAddr.sin_addr, ip, sizeof(ip));
    LOG_INFO("New connection fd=%d from %s:%d", clientFd, ip, ntohs(clientAddr.sin_port));
    addFd(epollFd, clientFd, false);
    {
        std::lock_guard lock(timer.mtx);
        timer.add(clientFd, TIMEOUT_MS, [clientFd]() {
            LOG_INFO("Connection timeout, close fd=%d", clientFd);
            close(clientFd);
        });
    }
}
功能:功能:接受客户端连接请求,并将该事件对应的fd加入内核中epoll管理的红黑树。

核心代码:handleRead(),一次读完客户端http请求

function:handleRead()
C++
void handleRead(int fd, const std::string& staticDir) {
            char buf[4096] = {0};
            int n = read(fd, buf, sizeof(buf) - 1);
            if (n <= 0) {
                {
                    std::lock_guard lock(timer.mtx);
                    timer.remove(fd);
                }
                close(fd);
                return;
            }
            HttpConn conn;
            conn.parseRequest(buf);
            LOG_INFO("fd=%d %s %s %s", fd, conn.method_.c_str(), conn.url_.c_str(), conn.version_.c_str());
            std::string response = conn.getResponse(staticDir);
            // 用 data() + size(),并且保证写完
            writeAll(fd, response.data(), response.size());
            {
                std::lock_guard lock(timer.mtx);
                timer.remove(fd);
            }
            close(fd);
        }
功能:处理写回http回复给服务器,使用data+size()保证一次写完,配合边缘触发模式

reactor事件循环

function:main()
C++
while (true) {
        int waitTime;
        {
            std::lock_guard lock(timer.mtx);
            waitTime = timer.getNextTick();
        }
        int nReady = epoll_wait(epollFd, events, MAX_EVENTS, waitTime);
        {
            std::lock_guard lock(timer.mtx);
            timer.tick();
        }
        for (int i = 0; i < nReady; i++) {
            int fd = events[i].data.fd;
            if (fd == listenFd) {
                handleAccept(listenFd, epollFd);
            } else if (events[i].events & EPOLLIN) {
                pool.addTask([fd]() {
                    handleRead(fd, STATIC_DIR);
                });
            }
        }
    }
功能:主线程由epoll管理并使用reactor事件循环;

已经修复的问题&&未来的改进点

function:handleRead()
C++
//
           
功能:处理写回http回复给服务器,使用data+size()保证一次写完,配合边缘触发模式