您的位置:首页 > 科技 > IT业 > 【Linux】第十七章 多路转接(select+poll+epoll)

【Linux】第十七章 多路转接(select+poll+epoll)

2024/11/18 15:09:53 来源:https://blog.csdn.net/YQ20210216/article/details/141562058  浏览:    关键词:【Linux】第十七章 多路转接(select+poll+epoll)

文章目录

  • select
    • fd_set
    • struct timeval
    • 实例-select服务器
      • socket
      • 初始化
        • 服务器初始化
        • 位图初始化
      • 开始监听
      • 超时测试
      • 事件处理
    • select 的特点
  • poll
    • struct pollfd
      • 事件 events 和 revents
    • 实例-poll服务器
      • socket
      • 初始化
      • 开始监听
      • 事件处理
    • poll 的特点
  • epoll-重点
    • epoll_create
    • epoll_ctl
      • epoll_event
      • 事件
    • epoll_wait
    • epoll工作原理-重点
    • epoll服务器
      • socket
      • 初始化
      • 开始监听
      • 事件处理
    • epoll 特点
    • epoll工作方式
      • 水平触发 LT-Level Triggered
      • 边缘触发 ET-Edge Triggered
      • LT和ET
      • ET 和非阻塞


select

一个多路转接接口,同时监视多个文件描述符的上的事件是否就绪,核心工作就是等

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要监视的最大文件描述符+1,例如文件描述符有1,7,10,那么nfds设置为10+1=11
  • readfds:输入输出型参数,监视文件描述符的事件是否就绪,返回的读事件已经就绪。
  • writefds:输入输出型参数,监视文件描述符的事件是否就绪,返回的写事件已经就绪
  • exceptfds:输入输出型参数,监视文件描述符的异常事件是否就绪,返回的异常事件已经就绪
  • timeout:输入输出型参数,设置select的等待时间,返回时表示timeout的剩余时间
    • NULL/nullptr:永不超时
    • 0:非阻塞等待,仅检测文件描述符的状态,不管什么情况都会立即返回
    • 特定的时间值:在指定的时间内进行阻塞等待,有文件事件则返回,超出特定时间就会返回

返回值

  • 成功,则返回就绪的文件描述符个数

  • timeout时间耗尽,则返回0

  • 失败,则返回-1,同时错误码会被设置

    • EBADF:文件描述符为无效的或该文件已关闭

    • EINTR:此调用被信号所中断

    • EINVAL:参数nfds为负值

    • ENOMEM:核心内存不足

编写流程

  • select之前要进行所有参数重置FD_SET
  • 需要定义第三方数组保存合法fd更新fd,方便select批量处理
  • select之后要便利所有合法参数进行检测FD_ISSET

fd_set

一个位图结构,最多监控1024歌文件描述符

  • 输入:用户告诉操作系统,需要帮我监控那几个文件描述符,在需要监控的文件描述符上置 1
  • 输出:系统告诉用户,那些文件描述符的相关事件就绪了
void FD_CLR(int fd, fd_set *set);  // 清空位图中fd的位
int  FD_ISSET(int fd, fd_set *set);// 判断相关fd的位是否为真
void FD_SET(int fd, fd_set *set);  // 设置对应位置的fd的位
void FD_ZERO(fd_set *set);         // 清空整个位图

struct timeval

用来计算select 等待的时间是秒与微秒的和

struct timeval {long    tv_sec;         /* 秒 */long    tv_usec;        /* 微秒 */
};

实例-select服务器

  • 每次调用select函数之前都需要对readfds进行重新设置,定义fdsArray数组用于保存监听套接字和已经与客户端建立连接的套接字
  • 循环调用select函数,检测读事件是否就绪,如果就绪则执行对应的操作
  • 定义一个读文件描述符集readfds,并将fdsArray当中的文件描述符依次设置进readfds当中,表示让select帮我们监视这些文件描述符的读事件是否就绪
  • 就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接,并将该连接对应的套接字添加到fdsArray数组当中
  • 就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出

socket

先初始化服务器,完成套接字的创建、绑定和监听。

setsockopt函数是让端口可以被复用,适用于服务器快速重启的情况

#include <stdio.h>
#include <vector>
#include <unistd.h>
#include <iostream>
#include <fstream>
#include <set>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include <vector>
#include <queue>
#include <pthread.h>
#include <ctime>
#include<semaphore.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>using namespace std;
class Sock
{
public://创建套接字static int SocketInit(){int sock  = socket(AF_INET, SOCK_STREAM, 0);if (sock  < 0){exit(1);}// 设置端口复用int opt = 1;setsockopt(sock , SOL_SOCKET, SO_REUSEADDR , &opt, sizeof(opt));return sock ;}//绑定static void Bind(int sock, int port){struct sockaddr_in local; memset(&local, 0, sizeof (local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;// 2.2 本地socket信息, 写入sock_对应的内核区域if (bind(sock, (const struct sockaddr *)&local, sizeof(local)) < 0){exit(2);}}//监听static void Listen(int sock, int backlog){if (listen(sock, backlog) < 0){exit(3);}}//获取连接static int Accept(int sock, std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int serviceSock = accept(sock, (struct sockaddr *)&peer, &len);if (serviceSock < 0){// 获取链接失败return -1;}if(clientport) *clientport = ntohs(peer.sin_port);if(clientip) *clientip = inet_ntoa(peer.sin_addr);return serviceSock;}
};

初始化

服务器初始化

运行并指明端口号

static void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 端口
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}// 初始化socket, 获取socket fd并绑定端口int listensock = Sock::SocketInit();Sock::Bind(listensock, atoi(argv[1]));Sock::Listen(listensock, 5); //位图初始化
}
位图初始化

数组用于保存监听套接字和已经与客户端建立连接的套接字,初始化为-1,第一个下标设置为listensock

	//位图初始化int fdsArray[sizeof(fd_set) * 8] = {0}; // 保存需要被监视读事件是否就绪的文件描述符,最多1024int fdsArraySz = sizeof(fdsArray) / sizeof(fdsArray[0]);// 将数组里面的文件描述符都初始化为默认值, 并将第一个下标设置为listensocketfor (int i = 0; i < fdsArraySz; i++){fdsArray[i] = -1;}fdsArray[0] = listensock;// 开始监听

开始监听

  • 传入select函数的参数都是输入输出参数,所以每次调用select函数都要对参数重新设置
  • 每次调用select函数之前,需要对readfds进行重新设置,并将fdsArray当中的文件描述符依次设置进readfds当中
  • 循环开始就调用select函数,检测读事件是否就绪
  • 如果读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接
	// 开始监听fd_set readfds;while (1){int maxFd = -1;FD_ZERO(&readfds);// 清空位图// 设置超时时间为5秒struct timeval timeout;timeout.tv_sec = 5;timeout.tv_usec = 0;// 遍历全局数组, 将有效的fd都添加进去, 并更新maxfdfor (int i = 0; i < fdsArraySz; i++){// 1. 过滤不合法的fdif (fdsArray[i] == -1)continue;// 2. 添加所有的合法的fd到readfds中, 方便select统一进行就绪监听FD_SET(fdsArray[i], &readfds);if (maxFd < fdsArray[i]){// 3. 更新出fd最大值maxFd = fdsArray[i]; }}// 调用select开始监听int sret = select(maxFd + 1, &readfds, nullptr, nullptr, &timeout);switch (sret){case 0: // 等待超时cout << "time out ... : " << endl;break;case -1: // 等待失败cerr << errno << " : " << strerror(errno) << endl;break;default:// 等待成功,正常的事件处理cout << "wait success: " << sret << endl;//HandlerEvent(listensock,readfds,fdsArraySz,fdsArray);break;}}

超时测试

运行服务器发现5秒超时跳出阻塞态

[aaa@VM-8-14-centos file]$ ./test 端口
time out ... :
time out ... :
time out ... :

使用 telnet 命令来链接当前服务,select 检测到 listensock 文件描述符就绪,会立刻返回

//第一个窗口
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
//第二个窗口
[aaa@VM-8-14-centos file]$ 
wait success: 1
wait success: 1

事件处理

HandlerEvent函数,当读事件就绪后就调用该函数进行事件处理

  • 遍历fdsArray数组当中的文件描述符,依次判断各个文件描述符对应的事件是否就绪
  • 处理已有连接,调用accept函数将底层的连接获取上来。将对应的文件描述符添加到fdsArray数组当中,托管给select函数
  • 处理新连接,调用read函数读取客户端发来的数据,如果读取成功则将读到的数据在服务器端进行打印。如果调用read函数读取失败或者客户端关闭了连接,那么select服务器也应该调用close函数关闭对应的连接,但此时光光关闭连接也是不够的,还应该将该连接对应的文件描述符从fdsArray数组当中清除

打印数组中的文件描述符

//打印数组中的文件描述符
static void ShowArray(int *arr, int num)
{cout << "当前合法sock list: ";for (int i = 0; i < num; i++){if (arr[i] == -1)continue;elsecout << arr[i] << " ";}cout << endl;
}

遍历整个链接数组,并判断当前位置是否是有效的文件描述符,无效直接跳过

static void HandlerEvent(int listensock, fd_set &readfds, int fdsArraySz, int *fdsArray)
{for (int i = 0; i < fdsArraySz; i++)//遍历所有参数{if (fdsArray[i] == -1)continue; // 跳过无效的位置//listensock}
}

处理新连接

  • 判断是否有在 select 中监听该文件描述符
  • 开始进行 accept 获取新的链接
  • 不能直接 read/write, 而是应该通过数组交付给 select 帮我们监听事件
		//处理新连接if (fdsArray[i] == listensock){// 判断listensocket有没有事件监听if (!FD_ISSET(listensock, &readfds)){cerr << "listensocket not set in readfds" << endl;continue;}// 具有了一个新链接cout << "get new connection" << endl;string clientip;uint16_t clientport = 0;int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞if (sock < 0)return; // 出错了, 直接返回// 成功获取新连接cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;// 这里我们不能直接对这个socket进行独写, 因为新链接来了并不代表新数据一并过来了// 所以需要将新的文件描述符利用全局数组, 交付给select// select 帮我们监看socket上的读事件是否就绪int i = 0;for (i = 0; i < fdsArraySz; i++){if (fdsArray[i] == -1)break;}// 达到上限了if (i == fdsArraySz){cerr << "reach the maximum number of connections" << endl;close(sock);}else // 没有达到{fdsArray[i] = sock; // 新的链接, 插入到数组中, 下次遍历就会添加到select监看中ShowArray(fdsArray, fdsArraySz);}}// 处理已有连接

处理已有连接,当读事件就绪的时候,我们通过 read 读取已有的数据

		// 处理已有连接else{if (FD_ISSET(fdsArray[i], &readfds)){char buffer[1024];ssize_t s = read(fdsArray[i], buffer, sizeof(buffer)-1); // 不会阻塞if (s > 0)//读取成功{buffer[s] = 0;cout << "client[" << fdsArray[i] << "]# " << buffer << endl;}else if (s == 0) // 对端关闭{cout << "client[" << fdsArray[i] << "] quit, server close " << fdsArray[i] << endl;close(fdsArray[i]);fdsArray[i] = -1; // 去除对该文件描述符的select事件监听ShowArray(fdsArray, fdsArraySz);}else // 异常了{cout << "client[" << fdsArray[i] << "] error, server close " << fdsArray[i] << endl;close(fdsArray[i]);fdsArray[i] = -1; // 去除对该文件描述符的select事件监听ShowArray(fdsArray, fdsArraySz);}}}

测试

用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收

//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
wait success: 1
get new connection
new conn:127.0.0.1:54684 | sock: 4
当前合法sock list: 3 4 
wait success: 1
listensocket not set in readfds
client[4]# 123//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123

select 的特点

  • 占用资源少
  • 每次循环都得遍历整个数组,效率较低
  • select可监控的文件描述符数量太少

poll

#include <poll.h>
int poll(struct pollfd *fds,  nfds_t nfds,  int timeout);
  • fds:一个poll函数监视的结构列表,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合

  • nfds:表示fds数组的长度

  • timeout:表示poll函数的超时时间,单位是毫秒

    • -1:阻塞等待

    • 0:非阻塞等待,仅检测文件描述符的状态,不管什么情况都会立即返回

    • 特定的时间值:在指定的时间内进行阻塞等待,有文件事件则返回,超出特定时间就会返回

返回值

  • 调用成功,则返回有事件就绪的文件描述符个数

  • timeout时间耗尽,则返回0

  • 调用失败,则返回-1

    • EFAULT:fds数组不包含在调用程序的地址空间中。

    • EINTR:此调用被信号所中断。

    • EINVAL:nfds值超过RLIMIT_NOFILE值。

    • ENOMEM:核心内存不足。

struct pollfd

struct pollfd {int   fd;         /* 文件描述符 fd */short events;     /* 用户告诉内核需要监看的事件 events */short revents;    /* 内核返回的就绪事件 revents */
};

事件 events 和 revents

事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux不支持)
POLLPRI高优先级数据可读,比如TCP带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入
POLLERR错误
POLLHUP挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件
POLLNVAL文件描述符没有打开

实例-poll服务器

socket

先初始化服务器,完成套接字的创建、绑定和监听。直接用select代码

初始化

不需要自己维护一个 int 文件描述符数组了,定义一个fds数组,该数组当中的每个位置都是一个struct pollfd结构

static void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 端口
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}// 初始化socket, 获取socket fd并绑定端口int listensock = Sock::SocketInit();Sock::Bind(listensock, atoi(argv[1]));Sock::Listen(listensock, 5);// 位图初始化struct pollfd fdsArray[1024];for (int i = 0; i < 1024; i++){fdsArray[i].fd = -1;fdsArray[i].events = 0;fdsArray[i].revents = 0;}//服务器刚开始运行时只需要监视监听套接字的读事件fdsArray[0].fd = listensock;fdsArray[0].events = POLLIN;// 开始监听}

开始监听

poll服务器就不断调用poll函数监视读事件是否就绪

	// 开始监听while (1){// 设置超时时间为5毫秒int timeout = 5;// 调用select开始监听int sret = poll(fdsArray, 1024, timeout);switch (sret){case 0: // 等待超时cout << "time out ... : " << endl;break;case -1: // 等待失败cerr << errno << " : " << strerror(errno) << endl;break;default:// 等待成功,正常的事件处理cout << "wait success: " << sret << endl;HandlerEvent(listensock, fdsArray);break;}}

事件处理

遍历fds数组,跳过无效的位置,判断处理新连接还是旧连接

static void HandlerEvent(int listensock, struct pollfd *fds)
{for (int i = 0; i < 1024; i++) // 遍历所有参数{if (fds[i].fd == -1)continue; // 跳过无效的位置// 处理新连接}
}

处理新连接,调用accept函数将底层建立好的连接获取上来,并将获取到的套接字添加到fds数组当中

		// 处理新连接if (fds[i].fd == listensock && fds[i].revents == POLLIN){// 具有了一个新链接cout << "get new connection" << endl;string clientip;uint16_t clientport = 0;int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞if (sock < 0)return; // 出错了, 直接返回// 成功获取新连接cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;int i = 0;for (i = 0; i < 1024; i++){if (fds[i].fd == -1)break;}// 达到上限了if (i == 1024){cerr << "reach the maximum number of connections" << endl;close(sock);}else // 没有达到{fds[i].fd = sock; // 将sock添加到数组中fds[i].events = POLLIN;fds[i].revents = 0;}}// 处理已有连接

处理旧连接,调用read函数读取客户端发来的数据,调用read函数失败则poll服务器也直接关闭对应的连接,并将该连接对应的文件描述符从fds数组当中清除

		// 处理已有连接else{if (fds[i].revents == POLLIN){char buffer[1024];ssize_t s = read(fds[i].fd, buffer, sizeof(buffer) - 1); // 不会阻塞if (s > 0)// 读取成功{buffer[s] = 0;cout << "client:" << buffer << endl;}else if (s == 0) // 对端关闭{cout << "client quit" << endl;close(fds[i].fd);fds[i].fd = -1;fds[i].events = 0;fds[i].revents = 0;}else // 异常了{cerr << "read error" << endl;close(fds[i].fd);fds[i].fd = -1;fds[i].events = 0;fds[i].revents = 0;}}}

测试,用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收

//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
time out ... : 
wait success: 1
get new connection
new conn:127.0.0.1:54684 | sock: 4
time out ... : 
wait success: 1
client:123//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123

poll 的特点

  • poll 没有最大文件描述符限制
  • poll 无需用户额外维护一个单独的文件描述符数组,直接沿用该结构体数组即可
  • 需要遍历检测,数据多的时候遍历时间长

epoll-重点

epoll_create

创建 epoll 文件句柄

#include <sys/epoll.h>
int epoll_create(int size);
  • size的值必须设置为大于0的值
  • 错误的时候返回 -1 并设置 errno, 正确的时候返回文件描述符
  • 错误的时候返回 -1 并设置 errno, 正确的时候返回文件描述符

epoll_ctl

对 epoll 中需要监看的文件描述符进行设置

#include <sys/epoll.h>
int epoll_ctl(int epfd,  int op,  int fd,  struct epoll_event *event);
  • epfd:epoll_create 的返回值

  • op:表示具体的动作

    • EPOLL_CTL_ADD:将新的文件描述符添加到 epfd 中

    • EPOLL_CTL_MOD:修改已有文件描述符的监听事件

    • EPOLL_CTL_DEL:删除已有文件描述符

  • fd:目标文件描述符

  • event:需要监视该文件描述符上的哪些事件

返回值

  • 函数调用成功返回0,调用失败返回-1

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_data_t data;        /* 联合体结构,一般选择使用该结构当中的fd,表示需要监听的文件描述符 */
};

事件

events 可以是下面的这些选项

事件说明
EPOLLIN对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
EPOLLOUT对应的文件描述符可以写;
EPOLLPRI对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR对应的文件描述符发生错误;
EPOLLHUP对应的文件描述符被挂断;
EPOLLET将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的;
EPOLLONESHOT只监听一次事件, 当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要手动再次把这个 socket 加入到 EPOLL 队列里;

epoll_wait

收集在 epoll 监控的事件中,已经就绪的事件

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd:epoll_create 的返回值
  • events:是一个输出型参数,将已经就绪的事件拷贝到events数组当中
  • maxevents:events数组中的元素个数,不可以超过 epoll_create 的 size;
  • timeout:超时时间,也是毫秒(0 非阻塞,-1 阻塞 特定的时间值)

返回值

  • 成功返回 IO 事件就绪的文件描述符数量,0 代表超时,负数代表失败

epoll工作原理-重点

image-20230826160246637

调用 epoll_create 时,在底层创建一个epoll模型,内部包含了就绪队列和一个红黑树

  • 调用 epoll_ctl 的时候,红黑树存放需要监视的事件
  • 添加到红黑树中的事件都会与设备的网卡驱动程序建立回调消息,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中
  • 调用 epoll_wait的时候,就绪队列存放需要返回给用户的事件,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可,获取就绪事件的时间复杂度是 O(1)

epoll服务器

socket

先初始化服务器,完成套接字的创建、绑定和监听。直接用select代码

初始化

初始化socket和epoll初始化

static void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 8080
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}// 初始化socket, 获取socket fd并绑定端口int listensock = Sock::SocketInit();Sock::Bind(listensock, atoi(argv[1]));Sock::Listen(listensock, 5);// epoll初始化int epfd;epfd = epoll_create(256);if (epfd < 0){std::cerr << "epoll_create error" << std::endl;exit(5);}// 开始监听
}

开始监听

  • 在epoll服务器开始死循环调用epoll_wait函数之前,需要先调用epoll_ctl将监听套接字添加到epoll模型当中
  • epoll服务器就不断调用epoll_wait函数监视读事件是否就绪,并返回就绪的文件描述符数量
  • epoll_wait函数的返回值等于0,则说明timeout时间耗尽,此时直接准备进行下一次epoll_wait调用即可
	// 开始监听struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = listensock;epoll_ctl(epfd, EPOLL_CTL_ADD, listensock, &ev);while (1){struct epoll_event revs[64];int sret = epoll_wait(epfd, revs, 64, -1);//返回就绪fd个数switch (sret){case 0: // 等待超时cout << "time out ... : " << endl;break;case -1: // 等待失败cerr << errno << " : " << strerror(errno) << endl;break;default:// 等待成功,正常的事件处理cout << "wait success: " << sret << endl;HandlerEvent(revs, sret);break;}}

事件处理

遍历所有文件描述符,说明该文件描述符对应的读事件就绪,判断该文件描述符是监听套接字还是与客户端建立的套接字

static void HandlerEvent(struct epoll_event revs[], int sret, int listensock, int epfd)
{for (int i = 0; i < sret; i++) // 遍历所有文件描述符{int fd = revs[i].data.fd; // 就绪的文件描述符// 处理新连接}
}

处理新连接,调用accept函数将底层建立好的连接获取上来,调用epoll_ctl函数将获取到的套接字添加到epoll模型当中

		// 处理新连接if (fd == listensock && revs[i].events == EPOLLIN){// 具有了一个新链接cout << "get new connection" << endl;string clientip;uint16_t clientport = 0;int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞if (sock < 0)return; // 出错了, 直接返回// 成功获取新连接cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;//将获取到的套接字添加到epoll模型中,并关心其读事件struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sock;epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);}// 处理已有连接

处理旧连接,调用read函数读取客户端发来的数据,调用read函数失败则调用epoll_ctl函数将该连接对应的文件描述符从epoll模型中删除

		// 处理已有连接else{if (revs[i].events == EPOLLIN){char buffer[1024];ssize_t s = read(fd, buffer, sizeof(buffer) - 1); // 不会阻塞if (s > 0)												 // 读取成功{buffer[s] = 0;cout << "client:" << buffer << endl;}else if (s == 0) // 对端关闭{cout << "client quit" << endl;close(fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);}else // 异常了{cerr << "read error" << endl;close(fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);}}}

测试,用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收

//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
wait success: 1
get new connection
new conn:127.0.0.1:48782 | sock: 5
wait success: 1
client:123//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123

epoll 特点

  • 数据拷贝轻量:调用epoll_ctl处理文件描述符和事件,select和poll每次都需要重新将需要监视的事件从用户拷贝到内核
  • 事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中
  • 没有数量限制:文件描述符没有数量限制
  • 线程安全:有mutex 锁
  • 接口使用方便,虽然拆分了 3 个函数,但是每个函数的功能非常明确

epoll工作方式

水平触发 LT-Level Triggered

image-20240824174039249
  • 只要底层有事件就绪,epoll就会一直通知用户,只有所有的数据都被处理完毕,epoll 才不会继续通知
  • epoll默认状态下就是LT工作模式
  • select和poll其实就是工作是LT模式下的
  • 支持阻塞和非阻塞读写

边缘触发 ET-Edge Triggered

image-20240824174045934
  • 只有底层就绪事件数量发生变化的时候,epoll才会通知用户
  • 只有当电平由低变高的那一瞬间才会触发,将epoll改为ET工作模式,则需要在添加事件时设置EPOLLET选项
  • 在ET工作模式下,底层就绪事件发生变化的时候会通知用户,只有一次机会,所以收到事件后必须立即处理
  • ET 模式下 epoll_wait 返回的次数更少,所以 ET 的性能远高于 LT
  • 只支持非阻塞读写

LT和ET

  • ET 模式下,事件就绪就必须一次性处理完数据
  • ET 的代码复杂度会增加

ET 和非阻塞

为什么 ET 必须要将文件描述符设置成非阻塞呢?

答:ET模式只有在变化的时候才会通知用户,如果是阻塞模式,客户端通知服务器取数据,然后服务器只取部分数据,剩下缓冲区中数据没有取出,文件描述符不会返回给客户端,客户端没有收到应答而不会继续发送数据,所以必须要采用循环读取 + 非阻塞的方式来将缓冲区读完


版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com