一个基于 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.创建socket
    int sockfd = socket(AF_INET,SOCK_STREAM,0);  // 创建TCP套接字,AF_INET表示IPv4,SOCK_STREAM表示TCP
    if(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;       // 设置协议族为IPv4
    addr.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);         // 开始监听连接,最大等待队列长度为10
    if(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 服务器程序,相比 selectpoll 具有更高的效率,主要功能如下:

核心功能
  1. TCP 服务器基础功能

    • 创建 TCP 套接字、设置地址复用、绑定端口、监听连接
    • 支持多客户端并发连接,通过 epoll 机制实现高效 I/O 多路复用
  2. 多路复用监控

    • 使用 epoll 同时监控三类 I/O 事件:
      • 标准输入(键盘输入):读取并显示键盘输入内容
      • 服务器监听套接字:接受新的客户端连接请求
      • 已连接客户端套接字:接收客户端消息并提供回声服务(消息原路返回)
  3. 连接管理

    • 新客户端连接时自动将其套接字添加到 epoll 监控列表
    • 客户端断开连接(主动发送 "byebye" 或异常断开)时,自动从 epoll 中移除并关闭套接字
    • 最大支持 1000 个并发连接(由 FD_CNT 定义)
技术特点
  1. epoll 机制优势

    • 采用 epoll_create 创建专用描述符,epoll_ctl 管理监控对象,epoll_wait 等待事件
    • 仅返回活动的文件描述符,无需遍历所有监控对象,效率更高(尤其在高并发场景)
    • 事件驱动模式,避免 select/poll 的性能瓶颈(如文件描述符数量限制、每次需重置集合)
  2. 事件处理流程

    • 主循环通过 epoll_wait 阻塞等待事件,超时时间设为 2 秒
    • 收到事件后遍历活动事件数组,区分不同类型事件(键盘输入、新连接、客户端消息)
    • 针对不同事件类型执行对应处理逻辑,实现单进程高效处理多任务
适用场景
  • 高并发网络服务器开发(相比 select/poll 更适合大量客户端连接场景)
  • 演示 Linux 下高效 I/O 多路复用机制的实现
  • 可作为回声服务器、聊天服务器等基础框架扩展

该程序展示了 epoll 在网络编程中的典型用法,是 Linux 平台下高性能服务器的常用技术方案。

相关推荐
XINVRY-FPGA1 小时前
XCKU115-2FLVB2104E AMD Xilinx Kintex UltraScale FPGA
嵌入式硬件·计算机视觉·fpga开发·云计算·硬件工程·dsp开发·fpga
饶宇航1 小时前
嵌入式开发硬件——单片机
单片机·嵌入式硬件
red润2 小时前
let obj = { foo: 1 };为什么Reflect.get(obj, ‘foo‘, { foo: 2 }); // 输出 1?
开发语言·javascript·ecmascript
froginwe112 小时前
PHP MySQL Delete 操作详解
开发语言
Nep&Preception3 小时前
vasp计算弹性常数
开发语言·python
Ice__Cai3 小时前
Python 基础详解:数据类型(Data Types)—— 程序的“数据基石”
开发语言·后端·python·数据类型
lilv664 小时前
python中用xlrd、xlwt读取和写入Excel中的日期值
开发语言·python·excel
CC呢4 小时前
基于单片机万年历/时钟/语音时钟
单片机·嵌入式硬件·万年历·智能时钟
AOwhisky4 小时前
板块三章节3——NFS 服务器
运维·服务器·php