📌 核心结论(TL;DR):
Event Loop(事件循环) :epoll 服务器的灵魂。一个
while(1)循环,靠epoll_wait驱动。核心逻辑 :如果就绪的是
listen_fd,就accept并注册新连接;如果就绪的是conn_fd,就read处理业务。单线程高并发:不用开多线程,也不用多进程,单线程 + epoll 就能同时处理上万个并发连接。
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。

