C IO复用select, epoll 简单总结
1. 文件描述符类型
REG :文件
DIR:目录
CHR :字符
BLK:块设备
UNIX:unix域套接字
FIFO :先进先出队列
IPv4:网际协议 (IP) 套接字
其中, 标准输入STDIN(0)和STDOUT输出(1), STDERR错误(2)为指定的值
2. IO复用模型
(1). select (在指定的一段时间内,轮询监听用户需要的文件描述符(用户添加到fd_set中的),当监听到的文件描述符传来可读、可写或异常事件发生时就会返回)
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds: 指定被监听文件描述符的总数,因为文件描述符通常从0开始计数,因此nfds通常为readfd,writefd,exceptfd这三个描述符集中的最大描述符编号加1;
readfd,writefd,exceptfd: 分别指向可读、可写、异常事件对应的文件描述符集合。应用程序调用select时,通过这三个参数传入需要监听的文件描述符,轮询等待有事件产生
timeout: 超时间设置; NULL表示一直阻塞直到某个文件描述符就绪; 非NULL表示超时时间后立即返回;
返回值: 大于0, 描述符有数据返回; 等于0超时; 小于0, select错误;
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待---读取
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
(2). epoll
#include <sys/epoll.h>
int epoll_create(int size);
函数功能: 创建epoll专用文件描述符的缓冲区。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数功能: 添加、删除、修改 监听文件描述符。
函数参数:
int epfd epoll专用的文件描述符
int op 操作命令。EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL
int fd 要操作文件描述符
struct epoll_event *event 存放监听文件描述符信息的结构体
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数功能: 等待事件发生。
函数参数:
int epfd epoll专用的文件描述符
struct epoll_event *events : 存放产生事件的文件描述结构体。
int maxevents :最大监听的数量.
int timeout :等待事件ms单位. <0 >0 ==0
返回值: 产生事件的数量。
typedef union epoll_data {
void *ptr;
int fd; //文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events EPOLLIN 输入事件 */
epoll_data_t data; /* User data variable */
};
(3). select和epoll区别:
select epoll
性能: 随着连接数增加, 急剧下降。处理成千上万并发连接数时, 连接数增加时, 性能基本上没有下降, 大量并发连接时,
性能很差 性能很好
连接数: 连接数量有限制,最大连接数不超过1024 连接数无限制
内在处理机制: 线性轮询 回调callback
开发复杂性 低 中
简单理解:
住校时,你的朋友来找你:
select版宿管阿姨,带着你的朋友挨个房间找,直到找到你
epoll版阿姨,会先记下每位同学的房间号, 你的朋友来时,只需告诉你的朋友你住在哪个房间,无需亲自带着你朋友满大楼逐个房间找人
如果来了10000个人,都要找自己住这栋楼的同学时,select版和epoll版宿管大妈,谁效率高?同理,高并发服务器中,轮询I/O是最耗时操作之一,epoll性能更高也是很明显。
select的调用复杂度O(n)。如一个保姆照看一群孩子,如果把孩子是否需要尿尿比作网络I/O事件,select就像保姆挨个询问每个孩子:你要尿尿吗?若孩子回答是,保姆则把孩子拎出来放到另外一个地方。当所有孩子询问完之后,保姆领着这些要尿尿的孩子去上厕所(处理网络I/O事件)
epoll机制下,保姆无需挨个询问孩子是否要尿尿,而是每个孩子若自己需要尿尿,主动站到事先约定好的地方,而保姆职责就是查看事先约定好的地方是否有孩子。若有小孩,则领着孩子去上厕所(网络事件处理)。因此,epoll的这种机制,能够高效的处理成千上万的并发连接,而且性能不会随着连接数增加而下降。
Epoll 使用
EpollServer.cpp
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <signal.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <pthread.h> #include <sys/select.h> #include <sys/time.h> #include <sys/epoll.h> #include <iostream> #include <string> #include <list> using namespace std; typedef struct _ClientFd { int m_fd = -1; // 连接句柄 string name = string(); // 名称 } ClientFd; // socket句柄 int sockfd; //消息结构体, 目前协定的消息接受发送的格式 struct MSG_DATA { char type; //消息类型. 0表示有聊天的消息数据 1表示好友上线 2表示好友下线 // char name[50]; //好友名称 // unsigned char buff[100]; //发送的聊天数据消息 string name; //好友名称 string buff; //发送的聊天数据消息 int number; //在线人数的数量 }; #define MAX_EPOLL_FD 100 struct epoll_event events[MAX_EPOLL_FD]; struct epoll_event event; int epfd; int nfd; struct MSG_DATA msg_data; // 存放客户端句柄列表 std::list<ClientFd*> clientFdList; // 添加描述符 void List_addFd(int fd) { ClientFd *pFd = new ClientFd(); pFd->m_fd = fd; clientFdList.emplace_back(pFd); } // 获取成员名称 void List_getName(struct MSG_DATA *msg_data,int client_fd) { auto iter = clientFdList.begin(); while( iter != clientFdList.end() ) { if ( (*iter)->m_fd == client_fd ) { msg_data->name = (*iter)->name; break; } iter++; } } // 删除描述符 void List_DelFd(int clientFd) { auto iter = clientFdList.begin(); while ( iter != clientFdList.end() ) { if ( (*iter)->m_fd == clientFd ) { iter = clientFdList.erase(iter); break; } ++iter; } } int List_GetCnt() { return clientFdList.size(); } // 保存文件 void List_SaveName(struct MSG_DATA *msg_data, int clientFd) { auto iter = clientFdList.begin(); while ( iter != clientFdList.end() ) { if ( (*iter)->m_fd == clientFd ) { msg_data->name = (*iter)->name; break; } ++iter; } } // 服务器转发消息 void Server_SendMsgData(struct MSG_DATA *msg_data,int clientFd) { printf("%d | %s\n", __LINE__, __FUNCTION__); auto iter = clientFdList.begin(); while ( iter != clientFdList.end() ) { if ( (*iter)->m_fd == clientFd ) { printf(""); write((*iter)->m_fd, msg_data, sizeof(struct MSG_DATA)); break; } ++iter; } } /*信号工作函数*/ void signal_work_func(int sig) { (void)sig; close(sockfd); exit(0); //结束进程 } // 函数入口 int main(int argc,char **argv) { if(argc!=2) { printf("./app <端口号>\n"); return 0; } signal(SIGPIPE,SIG_IGN); //忽略 SIGPIPE 信号--防止服务器异常退出 signal(SIGINT,signal_work_func); /*1. 创建socket套接字*/ sockfd=socket(AF_INET,SOCK_STREAM,0); /*2. 绑定端口号与IP地址*/ struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[1])); // 端口号0~65535 addr.sin_addr.s_addr=INADDR_ANY; //inet_addr("0.0.0.0"); //IP地址 if(bind(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr))!=0) { perror("服务器:端口号绑定失败.\n"); return -1; } /*3. 设置监听的数量*/ listen(sockfd,20); /*4. 等待客户端连接*/ int client_fd; struct sockaddr_in client_addr; socklen_t addrlen; int i; int cnt; /*5. 创建epoll相关的接口*/ epfd=epoll_create(MAX_EPOLL_FD); event.events=EPOLLIN; //监听的事件 event.data.fd=sockfd; //监听的套接字 epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event); while(1) { //等待事件发生, 有数据 nfd=epoll_wait(epfd,events,MAX_EPOLL_FD,-1); for(i=0;i<nfd;i++) { if(events[i].data.fd==sockfd) //表示有新的客户端连接上服务器 { client_fd=accept(sockfd,(struct sockaddr *)&client_addr,&addrlen); printf("连接的客户端IP地址:%s\n",inet_ntoa(client_addr.sin_addr)); printf("连接的客户端端口号:%d\n",ntohs(client_addr.sin_port)); //保存已经连接上来的客户端 // List_AddNode(list_head,client_fd); // 添加新连接客户端的句柄 List_addFd(client_fd); //将新连接的客户端套接字添加到epoll函数监听队列里 event.data.fd=client_fd; //监听的套接字 epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&event); } else //表示客户端给服务器发送了消息-----实现消息的转发 { //读取客户端发送的消息, 保存在msg_data中 cnt = read(events[i].data.fd,&msg_data,sizeof(struct MSG_DATA)); if(cnt<=0) //表示当前客户端断开了连接 { //获取名称 // List_GetName(list_head,&msg_data,events[i].data.fd); //删除节点 // List_DelNode(list_head,events[i].data.fd); //获取名称 List_getName(&msg_data, events[i].data.fd); //删除节点 List_DelFd(events[i].data.fd); msg_data.type=2; // //将断开连接的客户端套接字从epoll函数监听队列里删除调用 event.data.fd = events[i].data.fd; //监听的套接字 epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&event); close(event.data.fd); } if(msg_data.type==1) //好友上线的时候保存一次名称 { //保存名称 List_SaveName(&msg_data,events[i].data.fd); } //转发消息给其他好友 msg_data.number = List_GetCnt(); //当前在线好友人数 Server_SendMsgData(&msg_data,events[i].data.fd); } } } //退出进程 signal_work_func(0); return 0; }
EpollClient.cpp
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <signal.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <pthread.h> #include <sys/select.h> #include <sys/time.h> #include <poll.h> #include <sys/epoll.h> //消息结构体 struct MSG_DATA { char type; //消息类型. 0表示有聊天的消息数据 1表示好友上线 2表示好友下线 char name[50]; //好友名称 int number; //在线人数的数量 unsigned char buff[100]; //发送的聊天数据消息 }; struct MSG_DATA msg_data; #define MAX_EVENTS 2 struct epoll_event ev, events[MAX_EVENTS]; int epollfd; int nfds; //文件接收端 int main(int argc,char **argv) { if(argc!=4) { printf("./app <IP地址> <端口号> <名称>\n"); return 0; } int sockfd; //忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出 signal(SIGPIPE,SIG_IGN); /*1. 创建socket套接字*/ sockfd=socket(AF_INET,SOCK_STREAM,0); /*2. 连接服务器*/ struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535 addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址 if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0) { printf("客户端:服务器连接失败.\n"); return 0; } /*3. 发送消息表示上线*/ msg_data.type=1; strcpy(msg_data.name,argv[3]); write(sockfd,&msg_data,sizeof(struct MSG_DATA)); int cnt; int i; //创建专用文件描述符 epollfd = epoll_create(10); //添加要监听的文件描述符 ev.events = EPOLLIN; ev.data.fd = sockfd; epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev); ev.events = EPOLLIN; ev.data.fd = 0; //标准输入文件描述符 epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev); while(1) { //监听事件 nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1); if(nfds) { for(i=0;i<nfds;i++) { if(events[i].data.fd==sockfd) //判断收到服务器的消息 { cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA)); if(cnt<=0) //判断服务器是否断开了连接 { printf("服务器已经退出.\n"); goto SERVER_ERROR; } else if(cnt>0) { if(msg_data.type==0) { printf("%s:%s 在线人数:%d\n",msg_data.name,msg_data.buff,msg_data.number); } else if(msg_data.type==1) { printf("%s 好友上线. 在线人数:%d\n",msg_data.name,msg_data.number); } else if(msg_data.type==2) { printf("%s 好友下线. 在线人数:%d\n",msg_data.name,msg_data.number); } } } else if(events[i].data.fd==0) //表示键盘上有数据输入 { gets(msg_data.buff); //读取键盘上的消息 msg_data.type=0; //表示正常消息 strcpy(msg_data.name,argv[3]); //名称 write(sockfd,&msg_data,sizeof(struct MSG_DATA)); } } } } SERVER_ERROR: close(sockfd); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)