cpp
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listen_sockfd) {
// --- 处理新连接 ---
struct sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int conn_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &client_len);
if (conn_sockfd < 0) {
perror("accept failed");
continue;
}
std::cout << "New connection from " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;
set_non_blocking(conn_sockfd); // 新连接的套接字也必须是非阻塞的
// 将新连接的套接字加入epoll监控
event.events = EPOLLIN | EPOLLET; // 使用边缘触发(ET)模式
event.data.fd = conn_sockfd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_sockfd, &event) < 0) {
perror("epoll_ctl add client failed");
close(conn_sockfd);
}
} else {
// --- 处理客户端数据 ---
int conn_sockfd = events[i].data.fd;
char buffer[1024];
ssize_t count = read(conn_sockfd, buffer, sizeof(buffer));
if (count < 0) {
// 非阻塞模式下,EAGAIN或EWOULDBLOCK表示数据已读完
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// std::cerr << "Read finished for fd " << conn_sockfd << std::endl;
} else {
perror("read failed");
}
} else if (count == 0) {
// 客户端关闭连接
std::cout << "Client " << conn_sockfd << " disconnected." << std::endl;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn_sockfd, nullptr);
close(conn_sockfd);
} else {
// 收到数据,回显给客户端
// 注意:在实际高性能场景中,write也可能需要处理EAGAIN
if (write(conn_sockfd, buffer, count) < 0) {
perror("write failed");
}
}
}
}
epoll 事件处理循环
这是整个 epoll 服务器的「业务核心」!前面所有代码(socket、bind、listen、epoll_create)都是为了这一段代码服务。
整段代码的核心作用(大白话)
c
遍历所有触发事件的 socket:
如果是【监听套接字】有动静 → 有新客户上门,接待连接
如果是【普通客户端】有动静 → 客户发消息了,读取并回复
这就是高并发服务器处理成千上万客户端的逻辑!
第一部分:遍历事件
c
for (int i = 0; i < nfds; ++i) {
nfds:epoll_wait返回的就绪事件数量- 作用:把这次所有触发了事件的 socket挨个处理一遍
第二部分:判断事件类型(核心分支)
c
if (events[i].data.fd == listen_sockfd) {
原理:
我们之前把监听套接字 加入了 epoll 监控。
当它触发 EPOLLIN 事件 = 内核队列里有新连接等待被接收!
所以进入这个 if,一定是来新客户端了!
分支1:处理新客户端连接
1. 接收连接(accept)
c
int conn_sockfd = accept(listen_sockfd, ...);
- 作用 :从内核队列里取出一个连接 ,创建一个通信套接字
- listen_sockfd:只负责接客
- conn_sockfd :专门用来和这个客户端收发数据(一对一专属)
2. 打印客户端信息
c
inet_ntoa(client_addr.sin_addr) // IP转字符串
ntohs(client_addr.sin_port) // 端口转主机序
- 打印:客户端从哪个IP、哪个端口连过来的
3. 设置非阻塞(超级重要)
c
set_non_blocking(conn_sockfd);
- 只要用 epoll(ET模式),所有套接字必须是非阻塞!
- 保证
read/write不会卡住服务器
4. 把新客户端加入 epoll 监控
c
event.events = EPOLLIN | EPOLLET; // 监控可读 + 边缘触发
event.data.fd = conn_sockfd; // 带上通信套接字
epoll_ctl(ADD ...); // 加入监控列表
重点:EPOLLET 边缘触发
- ET 模式 :epoll 最高性能模式
- 只在数据从无到有时通知一次
- 效率极高,是高并发服务器标配
至此:
新客户端连接成功!epoll 开始监控它的消息。
分支2:处理客户端数据(else)
进入 else = 不是新连接,是已连接客户端发数据来了!
1. 获取客户端套接字
c
int conn_sockfd = events[i].data.fd;
- 拿到和这个客户端通信的专属套接字
2. 读取数据
c
ssize_t count = read(conn_sockfd, buffer, sizeof(buffer));
- 从客户端读取消息到缓冲区
- 返回值
count:读到的字节数
3. 三种返回值处理
① count < 0:读取出错
c
if (errno == EAGAIN) {
// 正常!非阻塞ET模式下,数据读完了
} else {
// 真出错了
}
② count == 0:客户端断开连接
c
// 从epoll移除 + 关闭套接字
epoll_ctl(DEL ...);
close(conn_sockfd);
- 客户端关闭了连接,服务器清理资源
③ count > 0:读到有效数据
c
write(conn_sockfd, buffer, count);
- 回显服务器:收到什么,原样发回给客户端
完整流程示意图(秒懂)
- 客户端发起连接
- 监听套接字触发 EPOLLIN
- accept 接收,得到 conn_sockfd
- 将 conn_sockfd 加入 epoll
- 客户端发消息
- conn_sockfd 触发 EPOLLIN
- read 读取,write 回写
- 客户端断开 → close 清理
极简总结(封神级)
- if (listen_sockfd) :处理新连接,accept + 加入epoll
- else :处理客户端消息,read + write
- EPOLLET:高性能边缘触发
- 非阻塞:所有socket必须非阻塞
- 核心:一个线程,处理成千上万客户端,不卡顿!
整个服务器代码完整闭环
你现在已经看完了Linux高并发epoll服务器全部代码:
- socket → 创建套接字
- setsockopt → 端口复用
- bind → 绑定IP端口
- listen → 开始监听
- epoll_create1 → 创建监控
- epoll_ctl(ADD) → 加入监听socket
- epoll_wait → 等待事件
- for循环处理 → 你刚学的核心逻辑
这就是 Nginx、Redis、SRS 等高并发中间件的底层原理!
最终一句话总结
这段代码,让单线程服务器,能同时处理几万客户端连接和消息,这就是 epoll 的威力!