多路转接 | select,poll,epoll的区别以及各自的应用实现
创始人
2025-05-30 06:46:11

文章目录

    • 前言
    • select函数介绍
      • tcp多路转接代码实现
    • poll函数介绍
      • poll函数 vs poll系统调用
      • tcp多路转接代码实现
    • epoll函数介绍
      • epoll原理
      • epoll_item vs epitem
      • tcp多路转接代码实现

前言

IO可以分为两个步骤,等待+实际的读写。

  • 等待是指等待文件的某一事件就绪,如果文件始终没有就绪,IO就要阻塞
  • 实际的读写就是IO最重要的操作了,这部分所占的比重越大,IO效率越高效

比如调用read时,指定套接字文件始终没有数据可以读取(读事件没有就绪),那么read将一直阻塞,直到数据的到来

多路转接是一种高效的IO方式,可以同时监听多个套接字文件,但是不是一个一个的阻塞等待,而是等待多个文件,当有文件就绪时,立即进行IO操作。这样就可以使IO的等待时间重叠,提高效率,减少CPU的空闲时间。常见的多路转接技术有select,poll,epoll,它们都是系统调用接口,具体实现被系统封装与隐藏了。

select函数介绍

select是一个多路复用(转接)输入/输出模型,它可以让程序监听多个套接字,在其中的一个或多个套接字准备好读写或者发送异常时通知程序

select函数原型如下:

#include 
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout)
  • nfds:表示要监听的文件描述符最大值,如果要监听的文件描述符最大值为n,那么就要设置nfds为n + 1,表示要监听文件描述符处于[0, n + 1)间的左闭右开区间
  • readfds:指向一个fd_set结构体,用于存储监听的读事件文件描述符集合
  • writefds:指向一个fd_set结构体,用于存储监听的写事件文件描述符集合
  • exceptfds:指向一个fd_set结构体,用于存储发生异常的文件描述符集合
  • timeout:指向一个timeval结构体,用于设置select的超时时间
    • 如果为NULL,表示无限等待(阻塞式),只有监听的文件描述符集合中有一个或多个发生了事件,select才会返回,否则一直阻塞
    • 如果为0秒0微秒,表示不等待(非阻塞式),不论文件描述符集合中是否发生了事件,select都直接返回
    • 如果大于0秒0微妙,表示等待指定的时间,监听的文件描述符集合中有一个或多个发生了事件或者超过了指定时间,select才会返回

select出错返回-1,并设置errno。返回0表示没有事件发生。有事件发生时,返回值表示发生事件的文件描述符数量

设置了超时时间,select就会返回在这段时间内,发生事件的文件描述符数量吗? 答案是不一定,发生了一个或多个事件,select可能也会返回。所以select可能提前返回,并且发生事件的文件描述符数量是不确定的,可能为1,可能大于1。我们要用FD_ISSET宏对每个文件描述符进行判断并处理


至于说fd_set结构体,这是一个long int类型(长度与操作系统和编译器有关,32位及以前的系统,长度为4字节,64位系统,长度为8字节。可以用sizeof检查你的平台上long int的大小)的数组。用来存放文件描述符,每一比特位表示一个文件描述符,1/0表示该文件描述符的某一事件是否发生的状态。有以下4个宏可以处理fd_set结构体

  • FD_ZERO(fd_set* fdset):清除fdset的所有位(置0
  • FD_SET(int fd, fd_set* fdset):将fd添加到fdset中
  • FD_CLR(int fd, fd_set* fdset):将fd从fdset中清除
  • FD_ISSET(int fd, fd_set* fdset):检查fd是否在fdset集合中,如果在返回非0值,不在返回0

可以直接位操作fd_set,但推荐使用宏来操作fd_set,因为fd_set的内部实现因平台而异,直接位操作不仅破坏其封装性和可移植性,还可能引发错误

关于最后一个参数timeout,它是struct timeval类型的指针,以下是struct timeval结构体成员的具体信息

// tv_sec表示秒,tv_usec表示微秒
struct timeval {time_t      tv_sec;     /* seconds */suseconds_t tv_usec;    /* microseconds */
};

tcp多路转接代码实现

使用tcp协议的四个步骤:socket,bind,listen,accept。为提高IO效率,使用select函数,

  • 如果监听套接字暂时没有与其他主机建立tcp连接,监听套接字文件会处于LISTEN状态
  • 如果有主机三次握手成功了,监听套接字会从LISTEN状态变为READABLE状态,表示新连接的到来
  • 并且将该连接从半连接队列中删除,加入全连接队列

所以select可以根据监听套接字文件的状态变化,检测是否有读事件发生:如果文件从LISTEN->READABLE,就表示发生了读事件。除了LISTEN和READABLE状态,还有一些常见状态

  • WRITABLE:表示文件可写入
  • EXECUTABLE:表示文件可执行
  • CLOSED:表示文件已关闭
  • ERROR:表示文件发生错误

select根据文件的状态,来判断是否有读事件,写事件或者异常事件发生,以返回发生事件的数量

#include "Socket.hpp"
#include void usage(char *process_name)
{cout << "usage: " << process_name << " port"<< endl;
}// 历史套接字数组和它的长度,长度为1024,表示可以同时运行的套接字数量
// 其实select只能同时监听1024个套接字
int fd_array[sizeof(fd_set) * 8] = {0};
int arr_num = (sizeof(fd_array) / sizeof(fd_array[0]));
// 数组初始值
#define DFL -1
#define BUF_SIZE 1024// select监听到了事件的发生,调用HandlerEvent处理事件
void HandlerEvent(int listen_sock, fd_set& readfds)
{for (int i = 0; i < arr_num; ++i){// 跳过默认值,寻找需要监听的套接字if (fd_array[i] == DFL)continue;// 如果发生了读事件if (FD_ISSET(fd_array[i], &readfds)){if (fd_array[i] == listen_sock){// 有新连接了,判断是否能获取该连接int j = 0;for (j = 0; j < arr_num; ++j){if (fd_array[j] == DFL)break;}if (j == arr_num){cerr << "当前队列已满"  << endl;}else{// 处理事件uint16_t peer_port;string peer_ip;int server_sock = tcpSock::Accept(listen_sock, &peer_ip, &peer_port);if (server_sock < 0){cerr << errno << ": " << strerror(errno) << endl;// accept失败,暂时不管这个连接了continue;}// 将其添加到历史数组中fd_array[j] = server_sock;cout << peer_ip << "[" << peer_port << "] 连接..." << endl;}} // end of if (fd_array[i] == listen_sock)else{// 创建读缓冲区char read_buffer[BUF_SIZE] = {0};// 普通IO事件就绪,此时读取不会阻塞int ret = recv(fd_array[i], (void*)&read_buffer, sizeof(read_buffer) - 1, 0);// 读取出错if (ret < 0){cerr << errno << ": " << strerror(errno) << endl;// 注意,程序不要直接退出,应该关闭该服务套接字close(fd_array[i]);fd_array[i] = DFL;}// 对端关闭else if (0 == ret){cout << "peer close..." << endl;close(fd_array[i]);fd_array[i] = DFL;}// 读取成功else{   read_buffer[ret] = '\0';// 这里需要对读取的信息进行处理,暂时用打印替代cout << read_buffer;}}}}
}int main(int argc, char *argv[])
{// 判断调用者是否传入了端口号if (argc != 2){usage(argv[0]);exit(-1);}// 创建套接字int listen_sock = tcpSock::Socket();// 将用户传入的端口绑定到套接字上tcpSock::Bind(listen_sock, atoi(argv[1]));// 使监听套接字处于监听状态tcpSock::Listen(listen_sock);// 初始化历史套接字数组for (int i = 0; i < arr_num; ++i){fd_array[i] = DFL;}// 默认将listen套接字设置进fd数组fd_array[0] = listen_sock;// 不断地检测事件的发生while (true){fd_set readfds = {0};int max_fd = DFL;// 添加读事件监听集for (int i = 0; i < arr_num; ++i){// 默认值不需要监听,直接跳过,找要监听的套接字if (fd_array[i] == DFL)continue;// 设置套接字到监听事件集中FD_SET(fd_array[i], &readfds);// 需要维护select的第一个参数if (fd_array[i] > max_fd)max_fd = fd_array[i];}// 设置超时时间为5秒struct timeval timeout = {5, 0};// 只关心读事件int n = select(max_fd + 1, &readfds, nullptr, nullptr, &timeout);switch (n){case 0:cout << "没有事件发生,但超时了..." << endl;break;case -1:cerr << errno << ":" << strerror(errno) << endl;break;default:HandlerEvent(listen_sock, readfds);break;}}return 0;
}

poll函数介绍

poll也是一个用于实现多路转接的函数,其原型如下

#include 
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:struct pollfd类型的指针,可以想象成数组,存储了多个struct pollfd

  • nfds:表示fds的长度,需要监听的套接字数量,可以存在无效套接字

  • timeout:单位为毫秒,超时时间的设置

    • 如果为-1,表示非阻塞等待。无论监听的描述符集合中是否有事件发生,都立即返回
    • 如果为0,表示阻塞式等待。除非监听的描述符集合中有一个或多个事件发生,否则不会返回
    • 如果大于0,表示在指定的时间内,如果监听的描述符集合中有一个或多个事件发生,或者超过了指定时间,poll才返回
  • 函数执行成功,返回监听的描述符集合中,发生事件的描述符个数。如果超时且没有任何事件发生,就返回0。如果调用失败,poll返回-1,并设置errno

poll比select使用简单,select有两个主要问题

  • select监听的套接字有数量上限,最大为1024
  • 由于事件集是一个输入输出参数,每次处理完事件,需要重新设置事件集。因为内核修改了上次的事件集,当需要监听的文件描述符数量增多时,重新设置事件集将会是极大的开销

poll就是为解决select的这两个问题而生的。这里有一个结构体:struct pollfd

// pollfd结构
struct pollfd {int   fd;         /* file descriptor */short events;     /* requested events */short revents;    /* returned events */
};

该结构体指明了需要监听的文件描述符,需要监听的事件(用户告诉内核),以及监听到的事件(内核告诉用户)。events和revents字段可以用以下常量来指定或测试不同类型的事件

  • POLLIN:普通或优先数据可读
  • POLLRDNORM:普通数据可读
  • POLLRDBAND:优先数据可读
  • POLLOUT:普通或优先数据可写
  • POLLWRNORM:普通数据可写
  • POLLWRBAND:优先数据可写
  • POLLERR:发生错误
  • POLLHUP:发生挂起

关于这些字段或者更多的字段,需要用到时可以上网查。

poll函数 vs poll系统调用

这一小节所谈论的poll是指poll系统调用

poll系统调用是用户向内核发起的一种请求,使用poll系统调用可以监听多个文件描述符的状态,等待其中一个或多个就绪或超时。poll系统调用会调用poll函数,poll函数是文件描述符所属的设备或对象提供的一种检查状态的接口,它通常是一个设备驱动程序中实现的函数。poll系统调用在执行过程中,会遍历fds数组中的每个元素,并且调用其对应文件描述符的poll函数来检查其状态,将结果保存在revents域中。如果没有任何文件描述符就绪,poll系统调用会将当前进程挂起到等待队列中,并进入休眠状态。当有设备发生IO事件时,内核会唤醒等待队列中的进程,并重新检查fds数组中的每个元素。

不同类型的文件描述符,如套接字,终端,管道,可能含有不同的poll函数实现。但poll函数主要有两个功能

  • 一是返回文件描述符当前的状态,如可读,可写,异常等,用户根据其返回的状态进行相应的操作
  • 二是将当前进程注册到设备或对象的等待队列中,并指定当文件描述符状态发生变化时,需要执行的回调函数。当设备或对象发生IO事件时,就可以唤醒等待队列中的进程并执行回调函数

所以poll系统调用是用户和内核的一种交互方式,而poll函数则是poll系统调用执行过程中所需的一个接口函数

tcp多路转接代码实现

#include "Socket.hpp"
#include void usage(char *process_name)
{cout << "usage: " << process_name << " port"<< endl;
}#define FDS_SIZE  1024
struct pollfd fds[FDS_SIZE] = {0};
nfds_t fds_count = 1;
#define DFL -1
#define BUF_SIZE 1024// select监听到了事件的发生,调用HandlerEvent处理事件
void HandlerEvent(int listen_sock)
{for (int i = 0; i < fds_count; ++i){// 跳过默认值,寻找需要监听的套接字if (fds[i].fd == DFL)continue;// 如果发生了读事件if (fds[i].revents & POLLIN){// 有新连接了,判断是否能获取该连接if (fds[i].fd == listen_sock){int j = 0;for (j = 0; j < fds_count; ++j){if (fds[i].fd == DFL)break;}if (j == FDS_SIZE){cerr << "当前队列已满"  << endl;}else{// 处理事件uint16_t peer_port;string peer_ip;int server_sock = tcpSock::Accept(listen_sock, &peer_ip, &peer_port);if (server_sock < 0){cerr << errno << ": " << strerror(errno) << endl;// accept失败,暂时不管这个连接了continue;}// 将其添加到监听队列中fds[j].fd = server_sock;// 监听读事件fds[j].events |= POLLIN;fds[j].revents = 0;++fds_count;cout << peer_ip << "[" << peer_port << "] 连接..." << endl;}} // end of if (fds[i].fd == listen_sock)else// 普通IO事件就绪,此时读取不会阻塞{// 创建读缓冲区char read_buffer[BUF_SIZE] = {0};// 读取数据int ret = recv(fds[i].fd, (void*)&read_buffer, sizeof(read_buffer) - 1, 0);// 读取出错if (ret < 0){cerr << errno << ": " << strerror(errno) << endl;// 注意,程序不要直接退出,应该关闭该服务套接字close(fds[i].fd);fds[i].fd = DFL;}// 对端关闭else if (0 == ret){cout << "peer close..." << endl;close(fds[i].fd);fds[i].fd = DFL;}// 读取成功else{   read_buffer[ret] = '\0';// 这里需要对读取的信息进行处理,暂时用打印替代cout << "收到了: " << read_buffer;}}}}
}int main(int argc, char *argv[])
{// 判断调用者是否传入了端口号if (argc != 2){usage(argv[0]);exit(-1);}// 创建套接字int listen_sock = tcpSock::Socket();// 将用户传入的端口绑定到套接字上tcpSock::Bind(listen_sock, atoi(argv[1]));// 使监听套接字处于监听状态tcpSock::Listen(listen_sock);// 默认将listen套接字设置进fd数组fds[0].fd = listen_sock;fds[0].events |= POLLIN;fds[0].revents = 0;fds_count = 1;// 初始化fds数组for (int i = 1; i < FDS_SIZE; ++i){fds[i].fd = DFL;fds[i].events = 0;fds[i].revents = 0;}// 设置超时时间为2秒int timeout = 2000;// 不断地检测事件的发生while (true){// 只关心读事件int n = poll(fds, FDS_SIZE, timeout);switch (n){case 0:cout << "没有事件发生,但超时了..." << endl;break;case -1:cerr << errno << ":" << strerror(errno) << endl;break;default:HandlerEvent(listen_sock);break;}}return 0;
}

对比select,poll有以下的优点

  • poll没有了监听数量的限制,可以监听任意数量的套接字
  • poll用一组结构体数组表示要监听的文件与事件,而select用位图结构表示要监听的文件描述符集合,poll可以更详细的表示要监听的事件类型
  • poll只会修改每个结构体中的revents字段,而select会修改传入的位图参数,导致select每次都要重新设置位图

但是poll还是没有解决以下问题

  • poll仍需遍历整个文件描述符数组,并检测哪些文件发生了事件
  • poll仍是轮询检测地对文件进行监听,当文件描述符的数量增多时,不仅浪费cpu资源,还不能及时的响应事件的发生
  • poll仍需将文件描述符数组从用户态拷贝到内核态,这样的开销随着文件描述符数量的增加而线性增长

epoll函数介绍

epoll有三个主要接口:epoll_creat,epoll_ctl,epoll_wait

#include 
int epoll_create(int size);  

epoll_create用于创建一个epoll对象,返回其描述符,失败返回-1并设置errno。关于其唯一参数size:自从Linux2.6版本之后,可以忽略该参数,但是要将其设置为大于0的值

#include 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

对epoll描述符的控制接口,可以实现对epoll控制描述符的添加,删除,修改

  • epfd:表示epoll的文件描述符,由epoll_create返回
  • op:表示要执行的操作,可以是EPOLL_CTL_ADD(添加),EPOLL_CTL_MOD(修改),EPOLL_CTL_DEL(删除
  • fd:表示要监听的文件描述符
  • event:表示一个指向struct epoll_event类型的结构体,它包含了两个字段
    • events:是epoll要监听的具体事件,如EPOLLIN(可读),EPOLLOUT(可写)等等,使用epoll_ctl注册事件时,需具体明确告知
    • data:是一个联合体,可以存储用户的数据,如指针,文件描述符,整数等,这个参数用来在epoll_wait返回事件时,传递参数或表示事件源
  • epoll_ctl成功返回0,失败返回-1并设置errno

以下是struct epoll_event的具体成员

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 */epoll_data_t data;        /* User data variable */
};

events可以是下面几个宏的集合:

  • EPOLLIN:对应文件描述符可读
  • EPOLLOUT:对应文件描述符可写
  • EPOLLERR:对应文件描述符发生错误
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读
  • EPOLLHUP : 表示对应的文件描述符被挂断
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

收集在监控事件中发生的事件

  • epfd:由epoll_create返回的epoll文件描述符
  • events:指向struct epoll_event的指针,用来存储返回的事件
  • maxevents:表示用户期望捕获的事件个数,不能大于events数组大小
  • timeout:和poll一样,表示毫秒,具体看poll关于timeout的介绍
  • epoll_wait的返回值是待处理的事件,如果为0表示超时或者非阻塞时的返回。失败时返回-1并设置errno

epoll原理

select与poll对于事件的监听采用轮询检测的方式,而epoll不再使用这种低效且浪费资源的方式,转而使用回调函数。利用内核的事件通知机制,当监听的文件描述符发生状态变化时,通过回调函数将其加入到就绪链表中,然后用户可以通过就绪链表获取已经就绪的文件描述符

epoll在内核中主要涉及以下数据结构:

  • struct eventpoll:对应一个epoll实例(由epoll_create返回),包含了一个红黑树和一个就绪链表。红黑树用于存储所有被监听的文件描述符,就绪链表用于存储已经就绪的文件描述符
  • struct epitem:对应一个被监听的文件描述符,存储了epoll fd的相关信息。包含一个eppoll_entry结构体指针,epoll_entry结构体用于将epitem挂载到文件描述符对应的等待队列中
  • struct eppoll_entry:存储被监听文件描述符的等待队列信息,每个被监听的文件描述符都有一个等待队列,其存储了注册在该文件描述符的回调函数。当该文件描述符发生事件时,内核会调用这些回调函数。eppol_entry就是用来链接这些文件描述符和回调函数的
  • struct poll_table:表示轮询表,包含了一个函数指针_qproc和一个事件掩码_key,分别用来注册等待队列和返回可操作的事件。驱动程序不需要知道其内部细节,只需要将其作为参数传递给poll方法
  • ep_poll_callback:当某个被监听的文件描述符fd有事件发生,会触发该回调函数(也称唤醒函数),该函数会将对应的epitem加入到eventpoll对象的就绪链表中,并通过wake_up_locked唤醒正在睡眠的epoll_wait。该函数是在ep_insert函数中被注册到文件描述符的等待队列中的

epoll主要用三个结构来管理和存储fd:eventpoll,epitem,eppoll_entry。eventpoll是一个全局对象,对应一个epoll实例。其红黑树和双向链表分别用来存储所有注册到epoll的fd和所有就绪的fd。每个注册到epoll中的fd都有一个对应的epitem对象,它含有一个红黑树节点和一个双向链表节点,分别用来插入到eventpoll中的两个数据结构中,epitem包含了一个eppoll_entry的链表(其pwqlist结构就是用来链接eppoll_entry的)。eppoll_entry包含了等待队列头和等待队列节点(wait_queue_t),用来将fd挂载到设备驱动程序提供的等待队列上,并注册回调函数。

关于红黑树与就绪链表:

  • 红黑树是一颗平衡的二叉搜索树,用来存储所有被epoll监听的文件描述符和对应的事件(epitem)。每个epoll实例都有一颗红黑树,当用户调用epoll_ctl注册或删除文件描述符时,内核会对红黑树进行相关操作,以保证其平衡,从而提高查找效率
  • 就绪链表是一个双向链表,用来存储已经发生事件的文件描述符和对应的事件(epitem)。每个epoll实例都有一个就绪链表,当socket收到数据包或发生其他事件时,内核会通过回调函数将对应的epitem插入到就绪队列中,然后唤醒等待在epoll_wait的用户进程,用户进程可以通过epoll_wait获取事件

eppoll_entry主要用于ep_insert和ep_remove两个函数中,eppoll_entry在函数中的作用是:在等待队列中添加和删除文件描述符

  • ep_insert的实现流程:
    • 当用户调用epoll_ctl注册一个文件描述符时,内核会创建一个epitem对象,将其插入到eventpoll的红黑树中
    • 然后内核会调用该文件驱动的poll函数,并传入一个poll_table参数,该参数用于建立__pollwait回调函数
    • 该回调函数会调用poll_wait把当前进程注册到文件驱动的等待队列中,还会创建一个eppoll_entry结构(其中包含了epitem和ep_poll_callback的信息),将其插入到epitem->pwqlist中。
  • ep_remove的实现流程:
    • 内核会先从eventpoll的红黑树中删除对应的epitem
    • 在删除前会遍历epitem的pwqlist,对每个eppoll_entry节点,从文件的等待队列中删除它,并释放其内存
    • 接着检查被删除epitem是否在eventpoll的等待队列中,如果在,则删除
    • 最后释放epitem占用的资源,并减少文件描述符的引用计数

当设备发送IO事件时,设备驱动会遍历其等待队列头对应的链表,并调用每个节点上注册的回调函数

  • 如果注册的回调函数是__poll_wait,那么__poll_wait会检查该节点是否对应了某个epoll实例中的epitem
    • 如果不对应,那么__poll_wait什么都不会做
    • 如果对应,那么__poll_wait会在epoll实例的红黑树中找到该epitem,将其加入就绪队列
  • 如果注册的回调函数是ep_poll_callback,那么ep_poll_callback会将已产生事件与关系事件做对比,如果有交集,将对应epitem加入到就绪队列中

综上,__poll_wait和ep_poll_callback都有将epitem加入到就绪队列中的功能,不同的是:__poll_wait是在设备驱动中被调用,而ep_poll_callback是在epoll_wait中被调用。__poll_wait是在设备事件发生时被动添加epitem,而ep_poll_callback是在用户请求时主动检查并添加epitem


epoll_item vs epitem

struct epitem {struct rb_node rbn; /* 红黑树节点 */struct list_head rdllink; /* 双向链表节点 */struct epitem *next; /* 指向下一个epitem */struct epoll_filefd ffd; /* 文件描述符和文件指针 */struct eventpoll *ep; /* 所属的eventpoll指针 */struct epoll_event event; /* 事件类型和数据 */
};struct epoll_item {struct rb_node rbn;struct list_head rdllink;int nwait;struct list_head pwqlist;struct epitem *epi;
};

epitem和epoll_item是两个不同的结构

  • epitem表示一个被监听的文件描述符的结构体,它包含了文件描述符,事件类型,回调函数等信息
  • epoll_item表示一个epoll实例中,所有文件描述符的集合的结构体。包含了一个红黑树和一个链表,用来遍历所有的epitem
  • 所以说,epitem是epoll_item的元素,epoll_item是epitem的容器
  • 它两都是内核数据结构。用户调用epoll_ctl()传递一个epoll_event给内核空间,内核空间根据这个结构体创建或更新一个对应的epitem,并将其添加到epoll_item中。用户根据epoll_wait获取epoll_event信息,这个信息是从epitem中复制出来的
  • 或者说:struct epoll_item和eventpoll一样,都表示(关注)一个epoll的实例

epoll相对于select和poll的优势:

  • epoll使用内核结构存储和管理被监听的文件描述符信息,避免了重复的拷贝文件描述符集合
  • epoll使用红黑树存储注册的文件描述符,提高了查找和插入的效率
  • epoll采用回调函数(事件驱动)唤醒进程,避免了不必要的轮询检测,节省了cpu资源
  • epoll支持边沿触发与水平触发两种模式,方便用户根据需求具体的定制epoll

tcp多路转接代码实现

#include "Socket.hpp"
#include #define MAXEVENTS 1024
#define BUF_SIZE 1024#define CRT_FAL -1
#define CTL_FAL -2
#define WAIT_FAL -3
#define ACP_FAL -4class epoll_server
{
public:epoll_server(uint16_t port, int listen_sockfd = -1, int epoll_fd = -1): _listen_sockfd(listen_sockfd), _epoll_fd(epoll_fd), _port(port){}~epoll_server(){if (-1 != _listen_sockfd)close(_listen_sockfd);if (-1 != _epoll_fd)close(_epoll_fd);}// 监听套接字的初始化void init_server();// 使用epoll进行IOvoid run_server();private:// IO事件的处理void handler_event(struct epoll_event* revs, int n);private:// 监听套接字fd与epoll实例fdint _listen_sockfd;int _epoll_fd;// epoll_server绑定的端口号uint16_t _port;
};void epoll_server::init_server()
{// 创建sock,绑定端口并使之处于监听状态_listen_sockfd = tcpSock::Socket();tcpSock::Bind(_listen_sockfd, _port);tcpSock::Listen(_listen_sockfd);cout << "init_server done" << endl;
}void epoll_server::run_server()
{_epoll_fd = epoll_create(128);if (-1 == _epoll_fd){cerr << errno << ": " << strerror(errno) << endl;exit(CRT_FAL);}struct epoll_event ev = {0};ev.events = EPOLLIN;ev.data.fd = _listen_sockfd;// 注册文件到epoll实例int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, _listen_sockfd, &ev);if (-1 == ret){cerr << errno << ": " << strerror(errno) << endl;exit(CTL_FAL);}struct epoll_event revs[MAXEVENTS] = {0};int timeout = 2000; // 设置超时时间2秒while (true){int n = epoll_wait(_epoll_fd, revs, MAXEVENTS, timeout);switch (n){case -1:cerr << errno << ": " << strerror(errno) << endl;exit(WAIT_FAL);break;case 0:cout << "超时事件内没有事件发生..." << endl;break;default:handler_event(revs, n);break;}}
}void epoll_server::handler_event(struct epoll_event* revs, int n)
{for (int i = 0; i < n; ++i){// 发生了读事件if (revs[i].events & EPOLLIN){// 监听到一个新连接if (revs[i].data.fd == _listen_sockfd){string peer_ip;uint16_t peer_port;int server_sock = tcpSock::Accept(_listen_sockfd, &peer_ip, &peer_port);if (-1 == server_sock){cerr << errno << ": " << strerror(errno) << endl;exit(ACP_FAL);}// 向epoll实例中注册这个服务套接字struct epoll_event ev= {0};ev.events = EPOLLIN;ev.data.fd = server_sock;int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, server_sock, &ev);if (-1 == ret){cerr << errno << ": " << strerror(errno) << endl;exit(CTL_FAL);}cout << "与客户端[" << peer_ip << "]:" << peer_port << "连接成功" << endl;}// 监听到普通IO事件else{char read_buff[BUF_SIZE] = {0};recv(revs[i].data.fd, read_buff, BUF_SIZE, 0);cout << "普通IO:" << read_buff;}}// 发生了写事件,暂时不处理else{}}
}
// main.cc
#include "epoll_server.hpp"void usage(char *process_name)
{cout << "usage: " << process_name << " port"<< endl;
}int main(int argc, char* argv[])
{// 判断调用者是否传入了端口号if (argc != 2){usage(argv[0]);exit(-1);}epoll_server eserver(atoi(argv[1]));eserver.init_server();eserver.run_server();return 0;
}

相关内容

热门资讯

【实验报告】实验一 图像的... 实验目的熟悉Matlab图像运算的基础——矩阵运算;熟悉图像矩阵的显示方法࿰...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
大模型落地比趋势更重要,NLP... 全球很多人都开始相信,以ChatGPT为代表的大模型,将带来一场NLP领...
Linux学习之端口、网络协议... 端口:设备与外界通讯交流的出口 网络协议:   网络协议是指计算机通信网...
kuernetes 资源对象分... 文章目录1. pod 状态1.1 容器启动错误类型1.2 ImagePullBackOff 错误1....
STM32实战项目-数码管 程序实现功能: 1、上电后,数码管间隔50ms计数; 2、...
TM1638和TM1639差异... TM1638和TM1639差异说明 ✨本文不涉及具体的单片机代码驱动内容,值针对芯...
Qt+MySql开发笔记:Qt... 若该文为原创文章,转载请注明原文出处 本文章博客地址:https://h...
Java内存模型中的happe... 第29讲 | Java内存模型中的happen-before是什么? Java 语言...
《扬帆优配》算力概念股大爆发,... 3月22日,9股封单金额超亿元,工业富联、鸿博股份、鹏鼎控股分别为3.0...
CF1763D Valid B... CF1763D Valid Bitonic Permutations 题目大意 拱形排列࿰...
SQL语法 DDL、DML、D... 文章目录1 SQL通用语法2 SQL分类3 DDL 数据定义语言3.1 数据库操作3.2 表操作3....
文心一言 VS ChatGPT... 3月16号,百度正式发布了『文心一言』,这是国内公司第一次发布类Chat...
CentOS8提高篇5:磁盘分...        首先需要在虚拟机中模拟添加一块新的硬盘设备,然后进行分区、格式化、挂载等...
Linux防火墙——SNAT、... 目录 NAT 一、SNAT策略及作用 1、概述 SNAT应用环境 SNAT原理 SNAT转换前提条...
部署+使用集群的算力跑CPU密... 我先在开头做一个总结,表达我最终要做的事情和最终环境是如何的,然后我会一...
Uploadifive 批量文... Uploadifive 批量文件上传_uploadifive 多个上传按钮_asing1elife的...
C++入门语法基础 文章目录:1. 什么是C++2. 命名空间2.1 域的概念2.2 命名...
2023年全国DAMA-CDG... DAMA认证为数据管理专业人士提供职业目标晋升规划,彰显了职业发展里程碑及发展阶梯定义...
php实现助记词转TRX,ET... TRX助记词转地址网上都是Java,js或其他语言开发的示例,一个简单的...
【分割数据集操作集锦】毕设记录 1. 按要求将CSV文件转成json文件 有时候一些网络模型的源码会有data.json这样的文件里...
Postman接口测试之断言 如果你看文字部分还是不太理解的话,可以看看这个视频,详细介绍postma...
前端学习第三阶段-第4章 jQ... 4-1 jQuery介绍及常用API导读 01-jQuery入门导读 02-JavaScri...
4、linux初级——Linu... 目录 一、用CRT连接开发板 1、安装CRT调试工具 2、连接开发板 3、开机后ctrl+c...
Urban Radiance ... Urban Radiance Fields:城市辐射场 摘要:这项工作的目标是根据扫描...
天干地支(Java) 题目描述 古代中国使用天干地支来记录当前的年份。 天干一共有十个,分别为:...
SpringBoot雪花ID长... Long类型精度丢失 最近项目中使用雪花ID作为主键,雪花ID是19位Long类型数...
对JSP文件的理解 JSP是java程序。(JSP本质还是一个Servlet) JSP是&#...
【03173】2021年4月高... 一、单向填空题1、大量应用软件开发工具,开始于A、20世纪70年代B、20世纪 80年...
LeetCode5.最长回文子... 目录题目链接题目分析解题思路暴力中心向两边拓展搜索 题目链接 链接 题目分析 简单来说࿰...