Linux 学习笔记(六十):单线程的逆袭——epoll 高并发服务器实战

📌 核心结论(TL;DR):

  1. Event Loop(事件循环) :epoll 服务器的灵魂。一个 while(1) 循环,靠 epoll_wait 驱动。

  2. 核心逻辑 :如果就绪的是 listen_fd,就 accept 并注册新连接;如果就绪的是 conn_fd,就 read 处理业务。

  3. 单线程高并发:不用开多线程,也不用多进程,单线程 + epoll 就能同时处理上万个并发连接。

  4. LT 模式最稳:默认的水平触发模式,只要数据没读完就会一直通知,极大降低了代码编写难度。

一、 从理论走向实战

之前我们学过了 epoll 的底层原理和 LT/ET 模式。今天我们基于 LT(水平触发)模式,编写一个真正的单线程高并发字典服务器。

二、 核心代码架构

cpp 复制代码
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
    int fd = events[i].data.fd;
    if (fd == listen_fd) {
        // 1. 新连接到达,accept 并注册到 epoll
        int conn_fd = accept(listen_fd, NULL, NULL);
        ev.data.fd = conn_fd;
        epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
    } else {
        // 2. 客户端数据到达,读取、处理、回发
        int n = read(fd, buf, sizeof(buf));
        if (n > 0) {
            // 查字典业务逻辑...
            write(fd, response, len);
        } else if (n == 0) {
            // 3. 客户端断开,从 epoll 移除并 close
            epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
            close(fd);
        }
    }
}

三、 运行效果

(插入多个 telnet 客户端同时成功查字典的截图)

通过该架构,我们仅用一个主线程,就实现了同时处理多个客户端连接的需求。没有线程切换的上下文开销,也没有惊群问题(单线程处理 accept)。

四、 扩展与思考

单线程的算力终究有瓶颈(比如如果字典查询极慢,会卡住后续连接)。这就引出了后面的大杀器:如何把 epoll 封装成面向对象的 Reactor 模式,甚至使用"主从 Reactor + 线程池"架构,彻底榨干多核 CPU。

自我实战操作

咱们这回写一个 C++ 版本,带上一个简单的"英译汉"字典功能。

bash 复制代码
cd ~/linux_study/network_test
mkdir epoll_dict && cd epoll_dict
第一步:准备字典文件
bash 复制代码
cat > dict.txt << 'EOF'
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
hello: 你好
world: 世界
EOF
第二步:编写服务器代码 epoll_dict_server.cc

(代码较长,直接复制。重点关注 while(1) 中如何处理 listen_fd 和 conn_fd)

cpp 复制代码
#include <iostream>
#include <string>
#include <map>
#include <fstream>
#include <sstream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define PORT 9999
#define MAX_EVENTS 1024
#define BUF_SIZE 1024

// 加载字典
std::map<std::string, std::string> LoadDict(const std::string& path) {
    std::map<std::string, std::string> dict;
    std::ifstream in(path);
    std::string line;
    while (std::getline(in, line)) {
        size_t pos = line.find(":");
        if (pos != std::string::npos) {
            std::string key = line.substr(0, pos);
            std::string val = line.substr(pos + 2); // 跳过冒号和空格
            dict[key] = val;
        }
    }
    return dict;
}

int main() {
    auto dict = LoadDict("dict.txt");

    // 1. 创建基础 TCP 服务端
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_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);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listen_fd, 10);

    // 2. 创建 epoll
    int epfd = epoll_create(1);
    struct epoll_event ev, events[MAX_EVENTS];
    
    // 3. 注册 listen_fd
    ev.events = EPOLLIN; // LT 模式
    ev.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

    std::cout << "epoll 字典服务器启动,监听端口 " << PORT << "..." << std::endl;

    // 4. 事件循环
    while (true) {
        // 阻塞等待就绪事件
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (nfds < 0) { perror("epoll_wait"); break; }

        for (int i = 0; i < nfds; i++) {
            int fd = events[i].data.fd;

            if (fd == listen_fd) {
                // 处理新连接
                struct sockaddr_in client_addr;
                socklen_t len = sizeof(client_addr);
                int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
                if (conn_fd < 0) continue;

                std::cout << "新连接接入, fd: " << conn_fd << std::endl;
                
                // 把新连接加入 epoll
                ev.events = EPOLLIN;
                ev.data.fd = conn_fd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);

            } else {
                // 处理已连接客户端发来的数据
                char buf[BUF_SIZE] = {0};
                int n = read(fd, buf, sizeof(buf) - 1);
                
                if (n > 0) {
                    buf[n] = '\0';
                    std::string word(buf);
                    // 去除可能带来的换行符
                    word.erase(word.find_last_not_of(" \n\r\t") + 1);

                    std::string response;
                    if (dict.find(word) != dict.end()) {
                        response = word + " 的意思是: " + dict[word] + "\n";
                    } else {
                        response = "词典中未查到该单词\n";
                    }

                    std::cout << "收到查询: " << word << " -> 回应: " << response;
                    write(fd, response.c_str(), response.size());
                } 
                else if (n == 0) {
                    // 客户端断开
                    std::cout << "客户端断开, fd: " << fd << std::endl;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                }
            }
        }
    }

    close(listen_fd);
    close(epfd);
    return 0;
}
第三步:编译测试
bash 复制代码
g++ epoll_dict_server.cc -o epoll_dict_server
./epoll_dict_server

👉 测试 :在本地终端输入 telnet 127.0.0.1 9999。输入 apple 回车,看看是不是立刻回了"苹果"。

👉 并发测试 :开 3、4 个终端同时 telnet 连上去。你会发现,服务器单线程竟然可以同时伺候所有人!谁发数据就处理谁,其他没发数据的连接处于"静默"状态,完全不耗 CPU。

相关推荐
AR-26710-30 分钟前
Linux Day32——实操五类资源告急排查
linux·运维
careathers1 小时前
【数据结构】基于比较的排序算法 - 交换排序
数据结构·算法·排序算法
StarsGreen1 小时前
libva中的图像格式说明
linux
H.莓飛2 小时前
【C++】【STL】vector_OJ题
linux·服务器·开发语言·数据结构·c++·visual studio
Rabitebla2 小时前
【Linux操作系统】进程篇(十一):进程程序替换——从底层原理到 exec 函数族
linux·服务器·c++·笔记·学习·算法
tellmewhoisi2 小时前
深度学习0:数学知识:正态分布1
人工智能·深度学习·算法
-梅2 小时前
算法(1) 动态规划
算法·动态规划
沫璃染墨2 小时前
从零入门计算机网络系列(十五):TCP可靠传输机制详解——从序号与确认应答到乱序处理
服务器·网络·c++·tcp/ip·tcp
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:Bellman-Ford与SPFA含负权最短路及负环检测全解析
c++·python·算法·bfs·信息学竞赛