一个基于 epoll 实现的多路复用 TCP 服务器程序,相比 select 和 poll 具有更高的效率
/*5 - 使用epoll实现多路复用 */
#include <stdio.h> // 标准输入输出函数库
#include <stdlib.h> // 标准库函数,包含exit等
#include <string.h> // 字符串处理函数
#include <unistd.h> // Unix标准函数,包含read, write等
#include <sys/socket.h> // 套接字相关函数
#include <sys/types.h> // 基本系统数据类型
#include <sys/epoll.h> // epoll相关函数和结构体
#include <fcntl.h> // 文件控制函数
#include <sys/stat.h> // 文件状态相关定义
#include <netinet/in.h> // 互联网地址族
#include <arpa/inet.h> // 提供IP地址转换函数
#include <signal.h> // 信号处理函数
#include <linux/input.h> // Linux输入设备相关定义(未实际使用)#define FD_CNT 1000 // 定义最大监控的文件描述符数量int main(int argc,char *argv[])
{if(argc!=2){ // 检查命令行参数数量是否正确printf("Usage:%s port\n",argv[0]); // 输出程序使用方法:程序名 端口号exit(0); // 正常退出}//1.创建socketint sockfd = socket(AF_INET,SOCK_STREAM,0); // 创建TCP套接字,AF_INET表示IPv4,SOCK_STREAM表示TCPif(sockfd==-1){ // 检查socket创建是否成功perror("socket"); // 输出错误信息exit(-1); // 异常退出}//允许地址复用int optval = 1; // 选项值,1表示启用// 设置套接字选项,允许地址复用,避免端口占用问题setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,&optval, sizeof(optval));//2.绑定ip和端口号(自己)struct sockaddr_in addr; // 定义IPv4地址结构体addr.sin_family = AF_INET; // 设置协议族为IPv4addr.sin_port = htons(atoi(argv[1])); // 将命令行参数的端口号转换为网络字节序addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口// 绑定套接字到指定地址和端口int res = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));if(res==-1){ // 检查绑定是否成功perror("bind"); // 输出错误信息exit(-1); // 异常退出}//3.监听res = listen(sockfd,10); // 开始监听连接,最大等待队列长度为10if(res==-1){ // 检查监听是否成功perror("listen"); // 输出错误信息exit(-1); // 异常退出}//准备epoll事件数组,用于存储epoll_wait返回的活动事件struct epoll_event evt_arr[FD_CNT] = {}; // 事件数组初始化int maxfd,i; // maxfd未实际使用,i用于循环char msg[1024] = {}; // 用于存储接收和发送的消息//1.创建epoll专用描述符// 参数为最大监控数量(>=1,Linux 2.6.8后忽略该参数)int epfd = epoll_create(1000);if(epfd==-1){ // 检查epoll创建是否成功perror("epoll_create"); // 输出错误信息exit(-1); // 异常退出}//2.添加要监控的事件struct epoll_event evt = { // 定义epoll事件结构体.events = EPOLLIN, // 监控读事件(有数据可读)};evt.data.fd = 0; // 绑定标准输入文件描述符(0)// 将标准输入添加到epoll监控res = epoll_ctl(epfd,EPOLL_CTL_ADD,0,&evt);if(res==-1){ // 检查添加是否成功perror("epoll_ctl"); // 输出错误信息exit(-1); // 异常退出}evt.data.fd = sockfd; // 绑定服务器监听套接字// 将监听套接字添加到epoll监控res = epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&evt);if(res==-1){ // 检查添加是否成功perror("epoll_ctl"); // 输出错误信息exit(-1); // 异常退出}//4.等待事件发生(客户端连接、消息或输入)while(1){ // 主循环,持续运行服务器//调用epoll_wait等待事件,超时时间2000ms(2秒)// 参数:epoll描述符、接收事件的数组、最大事件数、超时时间if((res = epoll_wait(epfd,evt_arr,FD_CNT,2000))<=0){printf("timeout!\n"); // 超时或出错时输出信息continue; // 继续下一次循环}//处理活动的描述符(遍历所有返回的事件)for(i=0;i<res;i++){//有键盘输入(标准输入有读事件)if(evt_arr[i].data.fd==0 && (evt_arr[i].events&EPOLLIN)){memset(msg,0,sizeof(msg)); // 清空消息缓冲区read(0,msg,sizeof(msg)); // 从标准输入读取数据printf("输入的内容为:%s\n",msg); // 输出读取到的内容}//1.有客户端连接请求(监听套接字有读事件)if(evt_arr[i].data.fd==sockfd && (evt_arr[i].events&EPOLLIN)){struct sockaddr_in cilent_addr; // 存储客户端地址socklen_t len = sizeof(cilent_addr); // 地址长度// 接受客户端连接,返回新的套接字描述符int newfd = accept(sockfd,(struct sockaddr *)&cilent_addr,&len);if(newfd==-1){ // 检查连接是否成功perror("accept"); // 输出错误信息exit(-1); // 异常退出}//将新连接的客户端描述符添加到epoll监控evt.data.fd = newfd; // 绑定新客户端套接字// 添加新客户端到epoll监控列表res = epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&evt);if(res==-1){ // 检查添加是否成功perror("epoll_ctl"); // 输出错误信息exit(-1); // 异常退出}// 输出客户端IP地址,表示有新客户端连接printf("%s到此一游!\n",inet_ntoa(cilent_addr.sin_addr));}//2.有客户端发送消息(其他套接字有读事件)// 排除标准输入和监听套接字,处理客户端消息if(evt_arr[i].data.fd!=0 && evt_arr[i].data.fd!=sockfd && (evt_arr[i].events&EPOLLIN)){res = read(evt_arr[i].data.fd,msg,sizeof(msg)); // 从客户端读取消息if(res<=0){ // 如果读取失败或客户端断开连接// 客户端断开连接,将其从epoll监控中删除res = epoll_ctl(epfd,EPOLL_CTL_DEL,evt_arr[i].data.fd,&evt_arr[i]);if(res==-1){ // 检查删除是否成功perror("epoll_ctl"); // 输出错误信息}close(evt_arr[i].data.fd); // 关闭套接字continue; // 继续下一次循环}//需要长时间通信可以开多任务printf("%s\n",msg); // 输出接收到的消息if(strcmp(msg,"byebye")==0){ // 如果客户端发送"byebye"// 客户端主动断开连接,从epoll监控中删除res = epoll_ctl(epfd,EPOLL_CTL_DEL,evt_arr[i].data.fd,&evt_arr[i]);if(res==-1){ // 检查删除是否成功perror("epoll_ctl"); // 输出错误信息}close(evt_arr[i].data.fd); // 关闭套接字continue; // 继续下一次循环}//原路发回消息(回声功能)write(evt_arr[i].data.fd,msg,res); // 将接收到的消息回发给客户端}}}close(sockfd); // 关闭监听套接字(实际不会执行到这里)return 0; // 程序正常退出(实际不会执行到这里)
}
程序功能说明:
这是一个基于 epoll
实现的多路复用 TCP 服务器程序,相比 select
和 poll
具有更高的效率,主要功能如下:
核心功能
TCP 服务器基础功能:
- 创建 TCP 套接字、设置地址复用、绑定端口、监听连接
- 支持多客户端并发连接,通过
epoll
机制实现高效 I/O 多路复用
多路复用监控:
- 使用
epoll
同时监控三类 I/O 事件:- 标准输入(键盘输入):读取并显示键盘输入内容
- 服务器监听套接字:接受新的客户端连接请求
- 已连接客户端套接字:接收客户端消息并提供回声服务(消息原路返回)
- 使用
连接管理:
- 新客户端连接时自动将其套接字添加到
epoll
监控列表 - 客户端断开连接(主动发送 "byebye" 或异常断开)时,自动从
epoll
中移除并关闭套接字 - 最大支持 1000 个并发连接(由
FD_CNT
定义)
- 新客户端连接时自动将其套接字添加到
技术特点
epoll 机制优势:
- 采用
epoll_create
创建专用描述符,epoll_ctl
管理监控对象,epoll_wait
等待事件 - 仅返回活动的文件描述符,无需遍历所有监控对象,效率更高(尤其在高并发场景)
- 事件驱动模式,避免
select/poll
的性能瓶颈(如文件描述符数量限制、每次需重置集合)
- 采用
事件处理流程:
- 主循环通过
epoll_wait
阻塞等待事件,超时时间设为 2 秒 - 收到事件后遍历活动事件数组,区分不同类型事件(键盘输入、新连接、客户端消息)
- 针对不同事件类型执行对应处理逻辑,实现单进程高效处理多任务
- 主循环通过
适用场景
- 高并发网络服务器开发(相比
select/poll
更适合大量客户端连接场景) - 演示 Linux 下高效 I/O 多路复用机制的实现
- 可作为回声服务器、聊天服务器等基础框架扩展
该程序展示了 epoll
在网络编程中的典型用法,是 Linux 平台下高性能服务器的常用技术方案。