epoll介绍与TCP服务器实战
发布时间:2026/8/14 12:21:17 作者:尧图编辑部 阅读量:1,286

一、epoll 简介epoll 是 Linux 提供的一种IO多路复用的机制即通过一个线程复用管理多个IO相比于 select/pollepoll具有以下优势高效数据结构使用红黑树 rbtree 来管理 interest list兴趣列表、双向链表来管理 ready list就绪列表返回就绪事件在 interest list 中的 fd IO就绪的时由内核将 fd 移动到 ready list 如果线程正在被 epoll_wait 阻塞则唤醒它。随后 epoll_wait 将写入 event 中一次最多返回 maxevents 个就绪事件支持边沿触发相比于 select/poll 同时支持水平触发LT和边沿触发ET1.1. 介绍与epoll有关的函数//创建epoll实例得到epfdintepoll_create(intsize)//控制事件操作红黑树//操作opEPOLL_CTL_ADD/MOD/DELintepoll_ctl(intepfd,intop,intfd,structepoll_event*event)//等待事件返回就绪集链表intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout)1.2. epoll event事件// epoll_event结构体structepoll_event{uint32_tevents;// 事件类型EPOLLIN等见下epoll_data_tdata;// 附加数据用户数据};fd 与 eventepoll_event ev{};ev.data.fdclient_fd;// 被监控对象哪个 fdev.eventsEPOLLIN;// 关注条件它可读时通知我epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,ev);在网络编程中事件主要是可读事件EPOLLIN有数据可以读了可写事件EPOLLOUT可以发送数据了错误事件EPOLLERR出错了挂起事件EPOLLHUP连接断开了1.3. epoll 水平触发 vs 边沿触发LT 是水平触发ET 是边沿触发是属于IO多路复用的概念select/poll 没有ETxx触发这种说法是IO多路复用特有的即fd就绪状态发生变化的时候应该以何种策略通知用户态程序然而边沿触发则是epoll的独特特性——select和poll仅有水平触发。下面我们以从read_buffer中读取数据为例子如read_buffer中如果到来了100字节数据水平触发策略下数据到来fd变为就绪状态自此fd只要处于仍就绪状态就一直通知epoll_wait这就得以实现“按需读取”假设我们一次实际上需要50字节那么read_buffer剩下的50字节下一次仍然会触发读。边沿触发策略下fd的就绪状态发生跳变的瞬间触发一次通知epoll_wait假设我们一次性读取了50字节那么剩下的50字节由于不涉及状态的跳变不再通知。这种情况下我们必须配合循环读取一次性将数据全部取出无法实现按需读取。通过设置 event 来设置 LT 和 ETev.eventsEPOLLIN|EPOLLRDHUP|0;//水平触发ev.eventsEPOLLIN|EPOLLRDHUP|EPOLLET;//边沿触发二、实例基于 epoll 的 TCP 服务器实现2.1 多端口监听使用epoll_create函数来创建一个epoll实例其参数大于0即可我们可取1这个实例用epfd去描述。然后使用socket函数并且指定参数SOCK_STREAM这样会自动选择建立TCP连接。当我们只需要去监听一个端口的时候在addr.sin_port当中指定我们想要的端口即可。这里为了提升TCP服务器的性能我们采用for循环创建多个端口porti并且将这些端口与sockfd绑定使用listen监听再将这些端口的事件都会被加入epoll实例中。if(argc2){printf(Param Error\n);return-1;}intportatoi(argv[1]);// startintsockfds[MAX_PORT]{0};// listen fdintepfdepoll_create(1);inti0;for(i0;iMAX_PORT;i){intsockfdsocket(AF_INET,SOCK_STREAM,0);structsockaddr_inaddr;memset(addr,0,sizeof(structsockaddr_in));addr.sin_familyAF_INET;addr.sin_porthtons(porti);// 8888 8889 8890 8891 .... 8987addr.sin_addr.s_addrINADDR_ANY;if(bind(sockfd,(structsockaddr*)addr,sizeof(structsockaddr_in))0){perror(bind);return2;}if(listen(sockfd,5)0){perror(listen);return3;}printf(tcp server listen on port : %d\n,porti);structepoll_eventev;ev.eventsEPOLLIN;ev.data.fdsockfd;epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,ev);sockfds[i]sockfd;}2.2. 主事件循环先声明一个事件数组总集进主事件循环。我们使用epoll_wait函数当事件就绪的时候EPOLLINEPOLLOUT需要处理这些事件指定timeout 5避免不必要的系统资源占用。我们判断是否遇到监听套接字listenfd如果是监听套接字则意味着这是一个新的连接使用accept函数接收这个新链接转化为客户端套接字。注意这里处理监听套接字是使用非阻塞的方式fcntl函数就是控制为非阻塞方式因为我们采用epoll边沿触发EPOLLET必须一次性读完全部数据。而后使用epoll_ctl函数将事件加入。当遇到客户端套接字clientfd的时候说明有客户端数据需要发送调用recv函数接收即可。注意这接收应该采用水平触发的方式一直有数据就一直触发一直接收。完毕后使用epoll_ctl删去。structepoll_eventevents[EPOLL_SIZE]{0};while(1){intnreadyepoll_wait(epfd,events,EPOLL_SIZE,5);// -1, 0, 5if(nready-1)continue;inti0;for(i0;inready;i){intsockfdislistenfd(events[i].data.fd,sockfds);if(sockfd){// listen 2structsockaddr_inclient_addr;memset(client_addr,0,sizeof(structsockaddr_in));socklen_tclient_lensizeof(client_addr);intclientfdaccept(sockfd,(structsockaddr*)client_addr,client_len);fcntl(clientfd,F_SETFL,O_NONBLOCK);intreuse1;setsockopt(clientfd,SOL_SOCKET,SO_REUSEADDR,(char*)reuse,sizeof(reuse));structepoll_eventev;ev.eventsEPOLLIN|EPOLLET;ev.data.fdclientfd;epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,ev);}else{intclientfdevents[i].data.fd;charbuffer[BUFFER_LENGTH]{0};intlenrecv(clientfd,buffer,BUFFER_LENGTH,0);if(len0){close(clientfd);structepoll_eventev;ev.eventsEPOLLIN;ev.data.fdclientfd;epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,ev);}elseif(len0){// disconnectclose(clientfd);structepoll_eventev;ev.eventsEPOLLIN;ev.data.fdclientfd;epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,ev);}else{printf(Recv: %s, %d byte(s), clientfd: %d\n,buffer,len,clientfd);}}}}return0;}