IO模型
IO模型:解决多客户端通信的核心策略
问题背景
网络服务器(UDP/TCP)需处理多客户端通信,核心痛点源于套接字默认阻塞特性,具体差异如下:
- UDP协议:无需连接,单个套接字即可与任意客户端通信,但默认阻塞——若客户端无消息,调用
recvfrom()会无限阻塞,无法处理其他任务。 - TCP协议:每成功建立一个客户端连接,服务端会生成一个新的“已连接套接字”;随着客户端增多,需高效管理大量并发套接字。
四种核心IO模型
多任务并发
- 核心逻辑:利用“一个客户端对应一个线程”的策略,规避套接字阻塞问题——新客户端连接成功后,立即创建独立线程处理其通信。
- 关键代码:
while(1)
{
// 阻塞等待新客户端连接(监听套接字)
int NewClient = accept(tcp_socket, (struct sockaddr *)&ClientAddr, &addrlen);
if (NewClient < 0)
{
perror("accept error");
return -1 ;
}
perror("accept ");
// 存储已连接套接字
ClientArr[Index++] = NewClient ;
// 启动新线程为该客户端服务
pthread_create( &tid , NULL , RecvMsgTask , (void *) NewClient );
pthread_detach(tid); // 强制分离线程,避免僵尸线程问题
}
- 特点:
- 优点:实现简单,客户端通信独立无干扰,调试便捷。
- 缺点:高并发场景下线程资源消耗大(内存、CPU上下文切换),线程数量上限受限。
非阻塞轮询
- 核心逻辑:将所有套接字设为“非阻塞模式”,通过循环持续检测每个套接字是否有数据到达,避免单个套接字阻塞导致整体卡死。
- 关键操作:通过
fcntl函数修改套接字状态为非阻塞:
// 获取套接字当前状态
long state = fcntl(sockfd, F_GETFL);
// 追加非阻塞标志
state |= O_NONBLOCK;
// 设置新状态
fcntl(sockfd, F_SETFL, state);
- 特点:
- 优点:无需多线程,避免线程资源浪费。
- 缺点:CPU需持续轮询所有套接字,即使无数据也占用高CPU,效率极低,仅适用于少量套接字场景。
异步信号
- 核心逻辑:利用系统信号(
SIGIO)触发IO事件——当套接字有数据时,内核自动发送SIGIO信号,服务端通过信号处理函数处理数据。
- 关键特性:
- 触发信号:仅
SIGIO(第29号信号),所有IO事件(连接、数据、断开)均触发同一信号。 - 适用场景:仅支持UDP协议(TCP事件类型多,无法通过单一信号区分;UDP事件简单,仅需处理数据接收)。
- 触发信号:仅
- 实现步骤:
- 设置
SIGIO信号的响应函数(信号处理函数); - 指定
SIGIO信号的属主进程(明确哪个进程接收信号):fcntl(sockfd, F_SETOWN, getpid()); // getpid()获取当前进程ID - 开启套接字的异步信号模式:
long state = fcntl(sockfd, F_GETFL); state |= O_ASYNC; // 追加异步标志 fcntl(sockfd, F_SETFL, state);
- 特点:
- 优点:无需轮询,CPU仅在有信号时工作,资源占用低。
- 缺点:仅支持UDP,TCP场景无法区分事件类型,实用性有限。
多路复用
- 核心逻辑:通过统一接口(如
select/poll/epoll)同时监听多个阻塞套接字,仅当至少一个套接字就绪(有数据/可写/异常)时才返回,实现“单线程处理多套接字”。
- 核心优势:兼顾“无多线程资源消耗”和“低CPU占用”,是高并发场景的基础模型。
多路复用深度解析
准备知识:套接字就绪状态
多路复用监听的套接字有三种就绪状态,需明确区分:
- 读就绪:套接字缓冲区有数据可读取(如客户端发送数据、断开连接)。
- 写就绪:套接字缓冲区未满,可写入数据(如发送缓存空闲)。
- 异常就绪:套接字收到紧急数据(非错误,如TCP紧急指针数据)。
基础多路复用:select
API接口定义
#include <sys/select.h>
int select(int nfds,
fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
struct timeval *_Nullable restrict timeout);
参数详解
| 参数名 | 含义 |
|---|---|
nfds |
所有监听集合中“最大文件描述符编号 + 1”(内核通过此值遍历集合) |
readfds |
读就绪监听集合(需检测“有数据可读”的套接字),NULL表示不监听 |
writefds |
写就绪监听集合(需检测“可写入数据”的套接字),NULL表示不监听 |
exceptfds |
异常就绪监听集合(需检测“紧急数据”的套接字),NULL表示不监听 |
timeout |
超时时间(struct timeval类型),NULL表示无限阻塞,0表示立即返回 |
- 超时时间结构体定义:
struct timeval { time_t tv_sec; // 秒 suseconds_t tv_usec; // 微秒(1秒=1e6微秒) };
返回值说明
- 成功:返回“就绪状态的套接字总数”(读+写+异常)。
- 失败:返回-1(如信号中断),并设置
errno。 - 超时:返回0(无任何套接字就绪)。
集合操作函数
fd_set是套接字集合类型,需通过专用函数操作(不可直接赋值):
| 函数原型 | 功能 |
|---|---|
void FD_ZERO(fd_set *set) |
清空集合(初始化必须调用) |
void FD_SET(int fd, fd_set *set) |
将套接字fd添加到集合set中 |
void FD_CLR(int fd, fd_set *set) |
将套接字fd从集合set中移除 |
int FD_ISSET(int fd, fd_set *set) |
判断fd是否在set中(就绪返回非0) |
使用示例:监听标准输入(0号描述符)
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
int main()
{
fd_set read_set; // 读监听集合
while (1)
{
FD_ZERO(&read_set); // 1. 清空集合
FD_SET(0, &read_set); // 2. 添加标准输入(0号描述符)到集合
printf("等待输入(5秒超时)...\n");
// 3. 设置超时时间:5秒
struct timeval timeout = {5, 0};
// 4. 调用select监听
int ret = select(0 + 1, &read_set, NULL, NULL, &timeout);
if (ret > 0)
{
// 5. 判断标准输入是否就绪
if (FD_ISSET(0, &read_set))
{
char buf[1024] = {0};
read(0, buf, sizeof(buf)); // 读取输入数据
printf("你输入了:%s", buf);
}
}
else if (ret == 0)
{
printf("超时!5秒内无输入\n");
}
else
{
perror("select error");
return -1;
}
}
return 0;
}
进阶多路复用:epoll(Linux专属)
什么是epoll?
epoll是Linux内核提供的高性能多路复用机制,专为高并发场景设计,解决select/poll的效率瓶颈。
核心特点
- 高效处理海量连接:
select/poll需遍历所有监听套接字,epoll通过“事件回调”直接返回就绪套接字,效率不随连接数增加而下降。 - 事件驱动:仅当套接字状态变化(就绪)时,才通知应用程序,无无效轮询。
- 两种触发模式:
- 水平触发(LT,Level Trigger):默认模式,只要套接字就绪(如缓冲区有数据),就持续通知,直到数据处理完。
- 边缘触发(ET,Edge Trigger):仅在套接字状态从“未就绪”变为“就绪”时通知一次,需一次性处理完所有数据(避免遗漏)。
- 无文件描述符上限:
select受限于FD_SETSIZE(默认1024),epoll仅受系统内存限制。
核心API
#include <sys/epoll.h>
// 1. 创建epoll实例(返回epoll文件描述符)
int epoll_create(int size);
// size:早期版本用于指定最大监听数,现在已忽略(需传>=1的值)
// 2. 控制epoll事件(添加/修改/删除监听套接字)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 3. 等待epoll事件(阻塞直到有套接字就绪)
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
epoll_ctl参数说明:
参数名 含义 epfdepoll_create返回的epoll实例描述符op操作类型: EPOLL_CTL_ADD(添加监听)EPOLL_CTL_MOD(修改事件)EPOLL_CTL_DEL(删除监听)fd需监听的套接字 event监听事件配置(如读就绪、边缘触发) -
epoll_event结构体:
struct epoll_event { uint32_t events; // 监听的事件类型(如EPOLLIN、EPOLLET) epoll_data_t data; // 关联数据(通常存储套接字fd) }; // 关联数据的联合体(常用fd字段) typedef union epoll_data { void *ptr; int fd; // 套接字描述符 uint32_t u32; uint64_t u64; } epoll_data_t; -
常见事件类型:
事件宏 含义 EPOLLIN读就绪(有数据可读) EPOLLOUT写就绪(可写入数据) EPOLLET边缘触发模式(默认LT) EPOLLONESHOT仅触发一次事件(需重新添加监听)
使用示例:TCP服务端(处理新连接+数据接收)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#define MAX_EVENTS 10 // 最大同时处理的就绪事件数
#define PORT 8888 // 监听端口
// 处理客户端数据
void handle_client_data(int client_fd) {
char buf[1024] = {0};
int n = read(client_fd, buf, sizeof(buf));
if (n <= 0) {
// 客户端断开或读错误,关闭套接字
close(client_fd);
printf("客户端断开连接(fd=%d)\n", client_fd);
return;
}
printf("收到客户端(fd=%d)数据:%s", client_fd, buf);
// 回声测试:将数据回发客户端
write(client_fd, buf, n);
}
// 处理新客户端连接
void accept_connection(int listen_fd, int epoll_fd) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
// 接收新连接
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &addr_len);
if (client_fd < 0) {
perror("accept error");
return;
}
printf("新客户端连接(fd=%d)\n", client_fd);
// 配置epoll事件:监听读就绪,LT模式
struct epoll_event ev;
ev.events = EPOLLIN; // 读就绪事件
ev.data.fd = client_fd;
// 将新客户端套接字添加到epoll监听
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
}
int main() {
// 1. 创建TCP监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket error");
return -1;
}
// 2. 绑定端口和IP
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
server_addr.sin_port = htons(PORT); // 端口转换为网络字节序
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind error");
close(listen_fd);
return -1;
}
// 3. 开始监听
if (listen(listen_fd, 5) < 0) { // 监听队列大小5
perror("listen error");
close(listen_fd);
return -1;
}
printf("TCP服务端启动,监听端口%d...\n", PORT);
// 4. 创建epoll实例
int epoll_fd = epoll_create(1); // size传1即可(忽略)
if (epoll_fd < 0) {
perror("epoll_create error");
close(listen_fd);
return -1;
}
// 5. 将监听套接字添加到epoll
struct epoll_event ev;
ev.events = EPOLLIN; // 监听读就绪(新连接触发)
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
// 6. 循环等待epoll事件
struct epoll_event events[MAX_EVENTS]; // 存储就绪事件
while (1) {
// 等待事件(超时-1表示无限阻塞)
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds < 0) {
perror("epoll_wait error");
break;
}
// 遍历所有就绪事件
for (int i = 0; i < nfds; i++) {
int curr_fd = events[i].data.fd;
// 情况1:监听套接字就绪(新连接)
if (curr_fd == listen_fd) {
accept_connection(listen_fd, epoll_fd);
}
// 情况2:客户端套接字就绪(有数据)
else {
handle_client_data(curr_fd);
}
}
}
// 7. 释放资源
close(epoll_fd);
close(listen_fd);
return 0;
}
epoll与select/poll对比
| 对比维度 | select | poll | epoll(Linux) |
|---|---|---|---|
| 最大连接数 | 有限(默认1024,受FD_SETSIZE限制) |
无限制(基于链表) | 无限制(仅受系统内存) |
| 效率 | 低(遍历所有监听套接字) | 中(遍历所有监听套接字) | 高(事件回调,仅遍历就绪套接字) |
| 触发模式 | 仅水平触发(LT) | 仅水平触发(LT) | 支持LT/ET(默认LT) |
| 内存开销 | 固定(fd_set大小) |
随连接数增加(链表) | 随连接数增加(动态内存) |
| 使用复杂度 | 简单 | 简单 | 中等(需理解事件模型) |
| 适用场景 | 少量并发(<1000) | 中量并发(1000-10000) | 高并发(>10000,如Nginx/Redis) |
拓展:IO模型的深度对比与实践选择
四种IO模型性能对比
| 模型 | 并发能力 | CPU占用 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 多任务并发 | 低-中 | 中 | 简单 | 客户端数量少(<100),需独立通信 |
| 非阻塞轮询 | 低 | 极高 | 简单 | 调试场景,或仅1-2个套接字 |
| 异步信号 | 中 | 低 | 中等 | UDP服务端(如物联网设备数据接收) |
| 多路复用(epoll) | 高 | 低 | 中等 | 高并发TCP/UDP服务(如服务器、网关) |
select的核心局限性(为何被epoll取代)
- 连接数限制:默认1024,修改需重新编译内核,成本高。
- 效率瓶颈:每次调用需遍历所有监听套接字,即使仅1个就绪,也需遍历全部。
- 集合重置:
select返回后,就绪集合会被修改(仅保留就绪套接字),下次调用需重新添加所有监听套接字,操作繁琐。 - 内存拷贝:每次调用
select,需将fd_set从用户态拷贝到内核态,高并发时拷贝开销大。
epoll LT与ET模式的实践差异
| 模式 | 触发逻辑 | 数据处理要求 | 优点 | 缺点 |
|---|---|---|---|---|
| 水平触发(LT) | 只要套接字就绪,就持续通知 | 可分多次读取数据,无需一次读完 | 实现简单,不易遗漏数据 | 若未及时处理,会重复通知,占用CPU |
| 边缘触发(ET) | 仅在“未就绪→就绪”状态变化时通知一次 | 必须一次性读完所有数据(循环读) | 仅通知一次,CPU占用低 | 实现复杂,漏读会导致数据丢失 |
- ET模式关键注意点:
- 套接字必须设为非阻塞(避免一次性读不完时阻塞);
- 读取数据需循环调用
read,直到返回-1且errno=EAGAIN(表示当前无数据)。
实际项目中IO模型的选择依据
- 并发量:
- 低并发(<100):多任务并发(实现快,调试简单)。
- 中并发(100-1000):select/poll(无需深入理解epoll,满足需求)。
- 高并发(>1000):epoll(Linux)/kqueue(BSD/Mac)(性能最优)。
- 协议类型:
- UDP:优先异步信号(简单高效),其次epoll。
- TCP:优先epoll(需处理大量连接和事件),避免异步信号。
- 开发成本:
- 快速迭代项目:多任务并发/select(开发快)。
- 长期维护项目:epoll(性能稳定,扩展性强)。
总结
IO模型的核心目标是高效处理多客户端并发通信,不同模型对应不同场景:
- 简单场景(少客户端):多任务并发、非阻塞轮询(优先前者)。
- UDP场景:异步信号(性价比最高)。
- 高并发场景(大量TCP连接):epoll(Linux)是唯一选择,也是Nginx、Redis等中间件的底层IO模型。
示例
select 多路复用示例:TCP 服务端(处理多客户端)
select 是最基础的多路复用接口,需手动维护监听集合,适合中低并发场景(<1000 连接)。
功能说明
- 监听套接字:接收新客户端连接
- 已连接套接字:接收客户端数据并回声(将数据回发)
- 处理客户端断开连接
- 每次
select前需重置监听集合(因select会修改集合)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#define PORT 65000 // 服务端端口
#define MAX_CLIENTS 100 // 最大支持客户端数(不超过 FD_SETSIZE 默认1024)
#define BUF_SIZE 1024 // 数据缓冲区大小
int main() {
int listen_fd, client_fds[MAX_CLIENTS]; // 监听套接字 + 已连接客户端套接字数组
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
fd_set read_fds; // select 读监听集合
int max_fd; // 监听的最大文件描述符(select 第一个参数需 max_fd+1)
int client_count = 0; // 当前已连接客户端数
// 1. 创建 TCP 监听套接字
if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket create failed");
exit(EXIT_FAILURE);
}
printf("监听套接字创建成功(fd=%d)\n", listen_fd);
// 2. 初始化客户端套接字数组(-1 表示未使用)
memset(client_fds, -1, sizeof(client_fds));
// 3. 配置服务端地址(绑定端口和IP)
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4 协议
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡(0.0.0.0)
server_addr.sin_port = htons(PORT); // 端口转换为网络字节序(主机→网络)
// 4. 绑定套接字与地址
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("服务端绑定端口 %d 成功\n", PORT);
// 5. 开始监听(监听队列大小为 5,即最多缓存 5 个未处理连接)
if (listen(listen_fd, 5) == -1) {
perror("listen failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("服务端启动成功,等待客户端连接...\n");
// 6. 循环处理 select 事件
while (1) {
// -------------------------- 关键:重置 select 监听集合 --------------------------
FD_ZERO(&read_fds); // 清空集合
FD_SET(listen_fd, &read_fds); // 添加监听套接字(检测新连接)
max_fd = listen_fd; // 初始化最大 fd 为监听套接字
// 将所有已连接的客户端套接字添加到监听集合
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_fds[i] != -1) { // 仅添加有效套接字
FD_SET(client_fds[i], &read_fds);
// 更新最大 fd(select 需传入 max_fd + 1)
if (client_fds[i] > max_fd) {
max_fd = client_fds[i];
}
}
}
// -------------------------- 调用 select 监听读事件 --------------------------
// 超时时间设为 NULL:无限阻塞,直到有事件触发
int ret = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
if (ret == -1) {
perror("select failed");
continue; // 忽略错误,继续循环
} else if (ret == 0) {
printf("select timeout(本示例无超时)\n");
continue;
}
// -------------------------- 处理就绪事件 --------------------------
// 1. 处理新客户端连接(监听套接字就绪)
if (FD_ISSET(listen_fd, &read_fds)) {
// 接收新连接(获取客户端套接字)
int new_client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (new_client_fd == -1) {
perror("accept failed");
continue;
}
// 检查是否超过最大客户端数
if (client_count >= MAX_CLIENTS) {
const char* msg = "服务端客户端数已达上限,无法连接\n";
send(new_client_fd, msg, strlen(msg), 0);
close(new_client_fd);
printf("客户端连接被拒绝:已达最大连接数 %d\n", MAX_CLIENTS);
continue;
}
// 将新客户端套接字加入数组
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_fds[i] == -1) {
client_fds[i] = new_client_fd;
client_count++;
break;
}
}
// 打印客户端信息(IP + 端口)
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
int client_port = ntohs(client_addr.sin_port);
printf("新客户端连接:IP=%s, Port=%d, fd=%d(当前连接数:%d)\n",
client_ip, client_port, new_client_fd, client_count);
}
// 2. 处理客户端数据(已连接套接字就绪)
for (int i = 0; i < MAX_CLIENTS; i++) {
int curr_client_fd = client_fds[i];
if (curr_client_fd == -1) continue; // 跳过无效套接字
// 检查当前客户端是否有数据可读
if (FD_ISSET(curr_client_fd, &read_fds)) {
char buf[BUF_SIZE] = {0};
// 读取客户端数据(返回值:实际读取字节数,0=客户端断开,-1=错误)
int recv_len = recv(curr_client_fd, buf, BUF_SIZE - 1, 0);
if (recv_len <= 0) {
// 客户端断开或读错误,关闭套接字并清理
close(curr_client_fd);
client_fds[i] = -1;
client_count--;
printf("客户端断开连接:fd=%d(当前连接数:%d)\n", curr_client_fd, client_count);
} else {
// 回声逻辑:将收到的数据回发给客户端
buf[recv_len] = '\0'; // 确保字符串结束
printf("收到客户端(fd=%d)数据:%s", curr_client_fd, buf);
send(curr_client_fd, buf, recv_len, 0); // 回发数据
}
}
}
}
// 理论上不会执行到这里(循环无限),仅作资源清理
close(listen_fd);
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_fds[i] != -1) close(client_fds[i]);
}
return 0;
}
epoll 多路复用示例:TCP 服务端(LT 模式,高并发)
epoll 是 Linux 专属高性能多路复用接口,支持海量连接,无需手动重置集合,适合高并发场景(如 Nginx/Redis 底层)。
功能说明
- 基于 水平触发(LT) 模式(默认,易实现)
- 自动管理监听事件(添加/删除客户端套接字)
- 处理新连接、客户端数据收发、断开连接
- 支持远超 1024 的并发连接(仅受系统内存限制)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#define PORT 65000 // 服务端端口
#define MAX_EVENTS 100 // 单次 epoll_wait 最多处理的事件数
#define BUF_SIZE 1024 // 数据缓冲区大小
// 处理客户端数据(回声逻辑)
void handle_client_data(int client_fd) {
char buf[BUF_SIZE] = {0};
int recv_len = recv(client_fd, buf, BUF_SIZE - 1, 0);
if (recv_len <= 0) {
// 客户端断开或读错误,关闭套接字
close(client_fd);
printf("客户端断开连接:fd=%d\n", client_fd);
} else {
// 回声:回发数据
buf[recv_len] = '\0';
printf("收到客户端(fd=%d)数据:%s", client_fd, buf);
send(client_fd, buf, recv_len, 0);
}
}
// 处理新客户端连接(添加到 epoll 监听)
void accept_new_connection(int listen_fd, int epoll_fd) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 接收新连接
int new_client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (new_client_fd == -1) {
perror("accept failed");
return;
}
// 打印客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
int client_port = ntohs(client_addr.sin_port);
printf("新客户端连接:IP=%s, Port=%d, fd=%d\n", client_ip, client_port, new_client_fd);
// -------------------------- 关键:将新客户端套接字添加到 epoll 监听 --------------------------
struct epoll_event ev;
ev.events = EPOLLIN; // 监听读事件(数据接收/断开)
ev.data.fd = new_client_fd; // 关联客户端套接字(通过事件传递)
// 调用 epoll_ctl 添加事件(EPOLL_CTL_ADD = 添加)
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_client_fd, &ev) == -1) {
perror("epoll_ctl add client failed");
close(new_client_fd);
return;
}
}
int main() {
int listen_fd, epoll_fd;
struct sockaddr_in server_addr;
struct epoll_event ev, events[MAX_EVENTS]; // 单个事件配置 + 事件数组(存储就绪事件)
// 1. 创建 TCP 监听套接字
if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket create failed");
exit(EXIT_FAILURE);
}
printf("监听套接字创建成功(fd=%d)\n", listen_fd);
// 2. 配置服务端地址并绑定
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("服务端绑定端口 %d 成功\n", PORT);
// 3. 开始监听
if (listen(listen_fd, 5) == -1) {
perror("listen failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("服务端启动成功,等待客户端连接...\n");
// 4. 创建 epoll 实例(参数 size 已忽略,传 1 即可)
if ((epoll_fd = epoll_create(1)) == -1) {
perror("epoll_create failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("epoll 实例创建成功(epoll_fd=%d)\n", epoll_fd);
// 5. 将监听套接字添加到 epoll 监听(检测新连接)
ev.events = EPOLLIN; // 监听读事件(新连接触发)
ev.data.fd = listen_fd; // 关联监听套接字
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
perror("epoll_ctl add listen failed");
close(epoll_fd);
close(listen_fd);
exit(EXIT_FAILURE);
}
// 6. 循环处理 epoll 就绪事件
while (1) {
// -------------------------- 调用 epoll_wait 等待事件 --------------------------
// 参数:epoll实例、事件数组、最大事件数、超时时间(-1=无限阻塞)
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait failed");
continue;
} else if (nfds == 0) {
printf("epoll_wait timeout(本示例无超时)\n");
continue;
}
// -------------------------- 遍历所有就绪事件 --------------------------
for (int i = 0; i < nfds; i++) {
int curr_fd = events[i].data.fd; // 获取事件关联的套接字
// 情况 1:监听套接字就绪 → 新客户端连接
if (curr_fd == listen_fd) {
accept_new_connection(listen_fd, epoll_fd);
}
// 情况 2:客户端套接字就绪 → 数据接收/断开
else {
// 处理数据后,无需手动重新添加事件(LT 模式会自动监听)
handle_client_data(curr_fd);
// 注意:客户端断开后,需从 epoll 中删除事件(否则会持续触发)
struct epoll_event del_ev;
del_ev.data.fd = curr_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, curr_fd, &del_ev);
}
}
}
// 资源清理(理论上不执行)
close(epoll_fd);
close(listen_fd);
return 0;
}
非阻塞式客户端(用 select 实现双向通信)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define BUF_SIZE 1024 // 数据缓冲区大小
#define SERVER_PORT 8888 // 服务端端口(需与服务端一致)
#define TIMEOUT 3 // select 超时时间(秒),避免无限阻塞
int main(int argc, char *argv[]) {
// 检查参数:需传入服务端IP
if (argc != 2) {
fprintf(stderr, "用法:%s <服务端IP>\n", argv[0]);
fprintf(stderr, "示例:%s 127.0.0.1\n", argv[0]);
exit(EXIT_FAILURE);
}
const char *server_ip = argv[1];
int client_fd;
struct sockaddr_in server_addr;
fd_set read_fds; // select 监听集合(标准输入 + 客户端套接字)
int max_fd; // 最大文件描述符(select 需 max_fd + 1)
// 1. 创建 TCP 客户端套接字
if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}
printf("客户端套接字创建成功(fd=%d)\n", client_fd);
// 2. 配置服务端地址并连接
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <= 0) {
perror("无效的服务端IP");
close(client_fd);
exit(EXIT_FAILURE);
}
if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("连接服务端失败");
close(client_fd);
exit(EXIT_FAILURE);
}
printf("成功连接服务端(IP:%s,端口:%d)\n", server_ip, SERVER_PORT);
printf("提示:输入任意内容发送(按回车),输入 'exit' 退出,实时接收服务端消息\n");
// 3. 初始化 select 相关参数
max_fd = (client_fd > 0) ? client_fd : 0; // 0 = 标准输入描述符
char send_buf[BUF_SIZE] = {0};
char recv_buf[BUF_SIZE] = {0};
// 4. 循环:用 select 监听事件
while (1) {
// -------------------------- 重置 select 监听集合 --------------------------
FD_ZERO(&read_fds); // 清空集合
FD_SET(0, &read_fds); // 添加标准输入(监听键盘输入)
FD_SET(client_fd, &read_fds); // 添加客户端套接字(监听服务端回声)
// 设置超时时间(避免 select 无限阻塞,每隔 TIMEOUT 秒唤醒一次)
struct timeval timeout = {TIMEOUT, 0}; // 秒 + 微秒
// -------------------------- 调用 select 监听 --------------------------
int ret = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select 失败");
break;
} else if (ret == 0) {
// 超时:无事件触发,打印提示(可选,避免客户端看起来“无响应”)
// printf("等待输入或服务端消息...(%d秒超时)\n", TIMEOUT);
continue;
}
// -------------------------- 处理就绪事件 --------------------------
// 1. 标准输入就绪 → 读取键盘输入并发送给服务端
if (FD_ISSET(0, &read_fds)) {
// 读取用户输入(fgets 会读取换行符,后续处理)
if (fgets(send_buf, BUF_SIZE, stdin) == NULL) {
perror("读取输入失败");
break;
}
// 处理输入:去掉换行符,判断是否退出
int send_len = strlen(send_buf);
if (send_buf[send_len - 1] == '\n') {
send_buf[send_len - 1] = '\0';
send_len--;
}
// 退出逻辑
if (strcmp(send_buf, "exit") == 0) {
printf("正在断开与服务端的连接...\n");
break;
}
// 发送数据(若输入为空,不发送)
if (send_len > 0) {
if (send(client_fd, send_buf, send_len, 0) == -1) {
perror("发送数据失败");
break;
}
printf("已发送:%s(字节数:%d)\n", send_buf, send_len);
memset(send_buf, 0, BUF_SIZE); // 清空缓冲区
}
}
// 2. 客户端套接字就绪 → 接收服务端回声并打印
if (FD_ISSET(client_fd, &read_fds)) {
int recv_len = recv(client_fd, recv_buf, BUF_SIZE - 1, 0);
if (recv_len <= 0) {
if (recv_len == 0) {
printf("服务端已断开连接\n");
} else {
perror("接收回声失败");
}
break;
}
recv_buf[recv_len] = '\0';
printf("\n收到服务端回声:%s(字节数:%d)\n", recv_buf, recv_len);
printf("请输入发送内容:"); // 提示用户继续输入
fflush(stdout); // 刷新输出缓冲区,确保提示立即显示
memset(recv_buf, 0, BUF_SIZE); // 清空缓冲区
}
}
// 5. 释放资源
close(client_fd);
printf("客户端已退出\n");
return 0;
}
epoll 边缘触发(ET)模式补充说明
若需使用 ET 模式(更高效率,仅触发一次状态变化),需修改两处代码:
- 添加事件时,在
events中追加EPOLLET标志:ev.events = EPOLLIN | EPOLLET; // 读事件 + 边缘触发 - 读取数据时需 循环读(直到
recv返回-1且errno=EAGAIN),避免漏读:void handle_client_data(int client_fd) { char buf[BUF_SIZE] = {0}; int recv_len; // 循环读:直到无数据可读(ET 模式需一次性读完) while ((recv_len = recv(client_fd, buf, BUF_SIZE - 1, 0)) > 0) { buf[recv_len] = '\0'; printf("收到客户端(fd=%d)数据:%s", client_fd, buf); send(client_fd, buf, recv_len, 0); // 回声 memset(buf, 0, BUF_SIZE); // 清空缓冲区 } // 处理客户端断开或错误 if (recv_len <= 0) { if (recv_len < 0 && errno != EAGAIN) { // 排除“暂时无数据”的错误 perror("recv failed"); } close(client_fd); printf("客户端断开连接:fd=%d\n", client_fd); } }
- ET 模式需将套接字设为非阻塞(避免循环读时阻塞),可通过
fcntl设置:// 给新客户端套接字设置非阻塞 int flags = fcntl(new_client_fd, F_GETFL, 0); fcntl(new_client_fd, F_SETFL, flags | O_NONBLOCK);
示例代码核心差异总结
| 对比维度 | select 示例 | epoll 示例(LT) |
|---|---|---|
| 集合维护 | 手动重置(每次 select 前 FD_ZERO/FD_SET) | 自动维护(epoll_ctl 添加/删除) |
| 最大连接数 | 受 FD_SETSIZE 限制(默认1024) | 无限制(仅受系统内存) |
| 事件处理效率 | 遍历所有监听套接字(低) | 仅遍历就绪套接字(高) |
| 适用场景 | 中低并发(<1000 连接) | 高并发(>1000 连接) |
| 代码复杂度 | 较高(需维护客户端数组+重置集合) | 较低(事件驱动,自动管理) |

浙公网安备 33010602011771号