大同网站建设优化推广,网站建设包括哪些流程,莱芜话题济南在线,网站空间费价格目录
I/O多路转接 - poll
poll 函数
poll 服务器
poll 服务器
poll 的优点
poll 的缺点
I/O 多路转接 - epoll
epoll 的相关系统调用
epoll_create 函数
epoll_ctl 函数
epoll_wait 函数
epoll 工作原理
epoll 服务器
编辑 epoll 的优点#xff08;与 sele… 目录
I/O多路转接 - poll
poll 函数
poll 服务器
poll 服务器
poll 的优点
poll 的缺点
I/O 多路转接 - epoll
epoll 的相关系统调用
epoll_create 函数
epoll_ctl 函数
epoll_wait 函数
epoll 工作原理
epoll 服务器
编辑 epoll 的优点与 select 的缺点对应
select、poll 和 epoll 的不同之处
epoll 的工作方式
水平触发Level TriggeredLT
边缘触发Edge TriggeredET ET 工作模式下如何进行读写 I/O多路转接 - poll
• poll 系统调用也可以让程序同时监视多个文件描述符上的事件是否就绪和 select 的定位是一样的适用场景也是一样的。
poll 函数 参数说明 • fds表示监视的结构列表每一个元素包含三个部分内容文件描述符监视的事件集合就绪的事件集合 • nfds表示 fds 数组的长度 • timeout表示poll 函数的超时事件单位是毫秒ms timeout 的取值 • -1poll 调用后进行阻塞等待直到某个被监视的文件描述符上的某个事件就绪 • 0poll 调用后进行非阻塞等待无论被监视的文件描述符上的事件是否就绪poll 检测后立马返回 • 特定的时间值poll 调用后在指定的时间内进行阻塞等待如果被监视的文件描述符上一直没有事件就绪则在该时间后 poll 进行超时返回 poll 返回值说明 • 如果函数调用成功则返回有事件就绪的文件描述符的个数 • 如果 timeout 时间耗尽则返回0 • 如果函数调用失败则返回 -1同时错误码被设置可能会被设置为如下 • EFAULTfds 数组不包含在调用程序的地址空间中 • EINTR此调用被信号所中断 • EINVALnfds 值超过RLIMIT_NOFILE 值 • ENOMEM核心内存不足 struct pollfd 结构 • fd特定的文件描述符若设置为负值则忽略events字段并且revents字段返回0 • events需要监视的文件描述符上的哪些事件 • reventspoll 函数返回时告知用户该文件描述符上的哪些事件已经就绪 events 和 revents 的取值
事件描述是否可作为输入是否可作为输出POLLIN数据包括普通数据和优先数据可读是是POLLRDNORM普通数据可读是是POLLRDBAND优先级带数据可读Linux不支持是是POLLPRI高优先级数据可读比如TCP带外数据是是POLLOUT数据包括普通数据和优先数据可读是是POLLWRNORM普通数据可写是是POLLWRBAND优先级带数据可写是是POLLRDHUPTCP连接被对方关闭或者对方关闭了写操作它由GNU引入是是POLLERR错误否是POLLHUP挂起比如管道的写端被关闭后读端描述符上将收到POLLHUP事件否是POLLNVAL文件描述符没有打开 否 是
这些取值实际上都是以宏的方式进行定义的二进制序列当中只要一个比特位是1且1的位置各不相同 • 在调用 poll 函数之前将要监视的事件设置进入到 events 成员中
• poll 函数返回后可以通过运算符检测在 revents 成员中是否包含特定的事件得知对应的描述符的特定事件是否就绪 poll 服务器
该服务器也只是读取客户端发来的数据然后进行打印即可成员变量需要包含监听套接字和端口号这两个服务器绑定时将IP地址设置为 INADDR_ANY之前对Sock操作等进行了封装。
• 在初始化 poll 服务器的时候依次进行套接字的创建绑定监听等工作
• 析构函数中可以选择调用 close 函数对监听套接字等进行关闭
#pragma once
#include iostream
#include string
#include unistd.h
#include sys/time.h
#include sys/types.h
#include functional
#include poll.h#include sock.hppclass pollServer{public:pollServer(func_t f, int port): _func(f), _listensock(-1), _port(port){}~pollServer(){if (_listensock){close(_listensock);}if (_rfds){delete[] _rfds;}}private:int _port;int _listensock;};初始化及运行服务器 • 在 poll 服务器运行之前先初始化服务器定义一个 fds 结构体数组该数组中的每个位置都是一个struct pollfd 结构先将每个位置初始化为无效并将监听套接字添加到 fds 数组中服务器开始运行时只需要监视监听套接字的读事件即可。 • 运行服务器不断调用 poll 函数监视读事件是否就绪如果 poll 函数的返回值大于0则说明 poll 函数调用成功此时已经有文件描述符的读事件就绪了接下来就是对就绪事件的处理逻辑返回值等于0说明 timeout 时间耗尽超时了继续进行下一次的 poll 调用即可返回值为-1说明 poll 调用失败根据错误码进一步判断是否继续调用 poll 函数。 void pollInit(int pos){_rfds[pos].fd defaultnum;_rfds[pos].events 0;_rfds[pos].revents 0;}void InitServer(){_listensock Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);_rfds new struct pollfd[num];for (int i 0; i num; i){pollInit(i);}// 第一个位置添加为 _listensock套接字_rfds[0].fd _listensock;_rfds[0].events POLLIN;}void Start(){// 等 处理数据 select返回的是fd的个数int timeout -1; // 毫秒for (;;){int n poll(_rfds, num, timeout);switch (n){case 0:// 超时logMessage(NORMAL, time out ...);break;case -1:logMessage(WARNING, select errno code: %d, select errno message: %s, errno, strerror(errno));break;default:// 走到这里说明有fd就绪了需要进行处理但是只有 listensock 就绪logMessage(NORMAL, have event ready!\n);// 进行业务逻辑处理Handerevent();break;}}}
事件处理
当 poll 检测到有文件描述符的读事件就绪时就会在其对应的 struct pollfd 结构体中的 revents 成员中添加读事件并返回后面对就绪事件进行处理
• 遍历 rfds 数组中的每个struct pollfd 结构如果该结果当中的 fd 有效且 revents 当中包含读事件说明该文件描述符的读事件就绪对该文件描述符进一步的判断是监听套接字还是与客户端建立连接的套接字
• 如果是监听套接字的读事件就绪就调用 accept 函数将底层建立好的连接获取上来并添加到 rfds 数组中下一次调用 poll 函数时需要监视该套接字的读事件
• 如果是与客户端建立连接对应的读事件就绪则调用 read 函数读取客户端发来的数据并将读到的数据在服务端进行打印
• 如果在调用 read 函数时发现客户端将连接关闭或 read 函数失败则 poll 服务器也直接关闭对应的连接并将连接对应的文 rfds 数组中移除下一次调用 poll 函数时不需要再监视该套接字的读事件
void Print(){std::cout fd list: ;for (int i 0; i num; i){if (_rfds[i].fd ! defaultnum){std::cout _rfds[i].fd ;}}std::cout std::endl;}void Accpeter(int listensock){// 获取新连接后直接添加进入到 _fdarray 数组中logMessage(NORMAL, Accpeter begin ...\n);uint16_t clientPort 0;std::string clientIP;int sock Sock::Accpet(listensock, clientPort, clientIP);if (sock 0){return;}int i 0;for (; i num; i){if (_rfds[i].fd ! defaultnum){continue;}else{break;}}// 找到位置if (i num){// 说明已经满了logMessage(WARNING, server is full,please wait ...\n);close(sock);}else{_rfds[i].fd sock;_rfds[i].events POLLIN; // 只考虑读事件_rfds[i].revents 0;}// 进行打印Print();logMessage(NORMAL, Accpeter end ...\n);}void Recver(int pos){// 通过sock这个fd进行接受数据logMessage(NORMAL, Recver begin ...\n);char buffer[1024];ssize_t n recv(_rfds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 这里读取不会阻塞只有sock就绪了才进来if (n 0){buffer[n - 1] 0;logMessage(NORMAL, client# %s, buffer);}else if (n 0){// 读取错误close(_rfds[pos].fd);pollInit(pos);logMessage(WARNING, recv error# %s, strerror(errno));return;}else // 0{// client 退出了close(_rfds[pos].fd);pollInit(pos);logMessage(NORMAL, client quit,me too ...);return;}// 此时数据都在buffer当中处理 requeststd::string response _func(buffer);write(_rfds[pos].fd, response.c_str(), response.size());logMessage(NORMAL, Recver end ...\n);}// 处理逻辑void Handerevent(){// 判断是listensock还是普通sock的for (int i 0; i num; i){if (_rfds[i].fd defaultnum){continue; // 后面需要进行置空不能break}if (_rfds[i].fd _listensock _rfds[i].revents POLLIN){// 需要进行accpetAccpeter(_rfds[i].fd);}else if (_rfds[i].revents POLLIN) // 普通读事件{// 其他fd 而且就绪Recver(i);}else{}}}因为 rfds 数组的大小是固定设置的在获取新连接并添加到数组中时可能会因为数组已满而导致添加失败只需要将 poll 服务器获取上来的连接套接字进行关闭即可。 poll 服务器 先实例化一个 pollServer 对象再初始化服务器和运行服务器
#include iostream
#include string
#include memory#include error.hpp
#include pollServer.hppvoid Usage(std::string arg)
{std::cout \n Usage: \n\t arg port \n\t std::endl;
}std::string transmition(const std::string request)
{return request;
}// ./main 8080
int main(int argc, char *argv[])
{if (argc ! 2){Usage(argv[0]);}std::unique_ptrServer::pollServer us(new Server::pollServer(transmition, atoi(argv[1])));us-InitServer();us-Start();return 0;
}
在调用 poll 服务器时此时 timeout 被设置为 -1运行服务器如果没有客户端发送连接请求服务器就会调用 poll 函数后进行阻塞等待 适用 telnet 工具当客户端进行连接 poll 服务器请求此时 poll 函数检测到监听套接字的读事件就绪后就立即进行业务逻辑处理 poll 的优点
• struct pollfd 结构当中包含了 events 和 revents相当于select 函数的输入输出型参数进行分离不再适用 select 参数 - 值 传递的方式接口适用更简单
• poll 并没有最大数量限制但是数量过大后性能也会下降
poll 的缺点
poll 中监听的文件描述符数量太多时
• 和 select 函数一样poll 返回后需要轮询 pollfd 来获取就绪的描述符
• 每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中
• 同时连接的大量客户端在一起时刻可能只有很少的处于就绪状态因此随着监视的描述符数量的增长其效率也会线性下降 I/O 多路转接 - epoll
epoll 也是系统提供的一个多路转接接口。
• epoll 在命名上比 poll 多了一个e这个 e 可以理解为 extendepoll 也是为了同时监视多个文件描述符上的事件是否就绪而对 poll 的改进。
epoll 的相关系统调用
epoll 有三个相关的系统调用epoll_create、epoll_ctl、epoll_wait 。
epoll_create 函数
用于创建一个 epoll 模型 • size自从Linux 2.6.8 之后size 参数是被忽略的但是 size 的值必须设置为大于 0 的值 • 用完之后必须调用 close 函数 返回值说明
• epoll 模型创建成功返回其对应的文件描述符否则返回 -1同时错误码被设置。
epoll_ctl 函数
epoll_ctl 用于向指定的 epoll 模型中注册事件 参数说明 • epfd指定的 epoll 模型 • op表示具体的动作用三个宏来表示 • fd需要监视的文件描述符 • event需要监视该文件描述符的哪些事件 返回值说明
• 函数调用成功返回0调用失败返回 -1同时错误码被设置。
struct epoll_event 结构如下 struct epoll_event 结构中有两个成员第一个成员 events 表示的是需要监视的事件第二个成员 data 是一个联合体一般选择使用该结构中的 fd 成员表示需要监听的文件描述符。 这些也都是宏的定义方式二进制序列中有且只有一个比特位1并且1的位置各不相同。 epoll_wait 函数 用于收集监视的事件中已经就绪的事件 参数说明 • epfd指定的 epoll 模型 • events内核会将已经就绪的事件拷贝到 events 数组中events 不能是空指针内核只负责将就绪事件拷贝到该数组中不负责在用户态中分配内存空间 • maxeventsevents 数组中的元素个数该值不能大于创建 epoll 模型时传入的 size 值 • timeout表示 epoll_wait 函数的超时时间单位是毫秒ms这里的 timeout 事件与 poll 一致。 返回值说明
• 如果函数调用成功则返回有事件就绪的文件描述符的个数
• 如果 timeout 时间耗尽则返回0
• 如果函数调用失败则返回 -1同时错误码被设置可能会被设置为如下 epoll 工作原理 当某一个进程调用 epoll_create 函数时Linux 会创建一个 eventpoll 的结构体也就是 epoll 模型eventpoll 结构体当中的成员 rbr 和 rdlist 与 epoll 的使用方式密切相关
struct eventpoll{...//红黑树的根节点这棵树中存储着所有添加到epoll中的需要监视的事件struct rb_root rbr;//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件struct list_head rdlist;...
}• 每一个epoll对象有一个独立的eventpoll结构体用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件 • 这些事件会挂载在红黑树中如此重复添加的事件就可以通过红黑树而高效的识别出来红黑树的插入时间复杂度是 log NN是树的高度 • 而所添加到 epoll 中的事件都会与设备网卡驱动程序建立回调关系响应的事件发生时会调用这个回调方法 • 这个回调方法在内核中叫 ep_poll_callback会将发生的事件添加到 rdlist 双链表中就绪队列 • 在 epoll 中对于每一个事件都会建立一个epitem结构体 struct epitem{struct rb_node rbn; //红黑树节点struct list_head rdllink; //双向链表节点struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所属的eventpoll对象struct epoll_event event; //期待发生的事件类型
}ffd 记录的是指定文件描述符event 记录的是该文件描述符对应的事件
• 对于当中的 rbn 成员来说ffd 与 event 的含义是需要监视 ffd 上的 event 事件是否就绪
• 对于 rdlink 成员来说ffd 与 event 的含义是ffd上的event事件已经就绪 • 对于 epoll 来说操作系统不需要主动进行事件都检测当红黑树中监视的事件就绪时会自动调用对应的回调方法将就绪的事件添加到就绪队列中 • 当调用 epoll_wait 函数获取就绪事件时只需要关注底层就绪队列是否为空如果不为空将就绪队列当中的就绪事件拷贝给用户即可 epoll 服务器
这里的 epoll 服务器也只是读取客户端发来的数据并进行打印。
类中成员除了包含监听的套接字和端口号之外还需要包含 epoll 模型对应的文件描述符
#include iostream
#include unistd.h
#include sys/epoll.h
#include functional
#include string#include sock.hpp
#include error.hppnamespace Server
{static const int defaultPort 8080;static const int size 128;static const int defaultnum 64;static const int defaultvalue -1;using func_t std::functionstd::string(const std::string );class epollServer{public:epollServer(func_t func, uint16_t port defaultPort, int num defaultnum): _port(port), _listensock(defaultvalue), _num(num), _epfd(defaultvalue), _func(func){}~epollServer(){if (_listensock ! defaultvalue){close(_listensock);}if (_erfds){delete[] _erfds;}if (_epfd ! defaultvalue){close(_epfd);}}private:uint16_t _port;int _listensock;int _epfd;int _num; // 就绪事件的空间大小struct epoll_event *_erfds; // 就绪事件func_t _func;};
}
初始化及运行服务器
• 在运行服务器之前需要需要依次调用封装Sock类中的函数创建套接字绑定和设置监听状态并创建 epoll 模型再调用 epoll_ctl 将监听套接字添加到 epoll 模型当中表示服务器开始运行时只需要监视监听套接字的读事件
• epoll 服务器不断调用 epoll_wait 函数监视是否有读事件就绪如果 epoll_wait 函数的返回值大于0说明已经有文件描述符的读事件就绪接下来就是对就绪事件的逻辑操作
• epoll_wait 函数的返回值等于0说明 timeout 时间耗尽准备继续下一次的 epoll_wait 调用返回值等于 -1需要根据错误码进一步判断是否继续调用 epoll_wait 函数 void InitServer(){// 1.创建套接字_listensock Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);// 2.创建epoll模型_epfd epoll_create(size);if (_epfd 0){logMessage(ERROR, create epoll 模型 error);exit(EPOLL_CREATE_ERR);}// 3.将listensock添加进入epoll中struct epoll_event ev;ev.events EPOLLIN;ev.data.fd _listensock;epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock, ev);// 申请就绪空间的大小_erfds new struct epoll_event[_num];logMessage(NORMAL, Init epoll success!);}void Start(){int timeout -1;for (;;){int n epoll_wait(_epfd, _erfds, _num, timeout); // 就绪事件全部存储在 _erfds 中switch (n){case 0: // 超时logMessage(NORMAL, time out ...\n);break;case -1:logMessage(FATAL, EPOLL_WAIT EORROR\n);break;default:logMessage(NORMAL, have a event readys\n);Handlerevents(n);break;}}}
注意
• 只要底层有就绪事件没有处理epoll 会一直通知用户本质原因是实际没有对底层就绪的数据进行读取。 事件处理
如果底层就绪队列中有就绪事件epoll_wait 函数会将底层就绪队列中的事件拷贝到定义的 _erfds
数组中epoll 服务器再进行对就绪事件的处理 • 根据 epoll_wait 的返回值来判断操作系统向 _erfds 数组中拷贝了多少个 struct epoll_event 结构进而对这些文件描述法上的事件进行处理 • 对于该结构当中的 events 当中包含读事件则说明该文件描述符对应的读事件就绪接下来还需要进一步判断该文件描述符是监听套接字还是与客户端建立连接的套接字 • 如果是监听套接字的读事件就绪就调用 accept 函数将底层建立好的连接获取上来并调用 epoll_ctl 函数将获取的套接字添加到 epoll 模型中表示下一次调用 epoll_wait 函数时需要检视该套接字 • 如果是与客户端建立的连接对应的事件就绪则调用 recv 函数读取客户端发来的数据并将读取到的数据在服务端打印 • 如果在调用 recv 函数时发现客户端将连接关闭或者 recv 函数调用失败则 epoll 服务器也直接关闭对应的连接则调用 epoll_ctl 函数将对应连接的文件描述符从 epoll 模型中移除表示下一次调用 epoll_wait 函数时无需监视该套接字的读事件 void Handlerevents(int readyNum){logMessage(DEBUG, Handlerevents begin ...\n);for (int i 0; i readyNum; i){uint32_t event _erfds[i].events;int sock _erfds[i].data.fd;if (sock _listensock (event EPOLLIN)){uint16_t clientPort;std::string clientIP;// 监听套接字就绪int fd Sock::Accpet(sock, clientPort, clientIP);if (fd 0){logMessage(NORMAL, Accept fail ...\n);continue;}// 将fd添加到 epoll 模型中struct epoll_event ev;ev.events EPOLLIN;ev.data.fd fd;epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, ev);}else if (event EPOLLIN){// 普通事件就绪char buffer[2048];int n recv(sock, buffer, sizeof(buffer) - 1, 0);if (n 0){buffer[n] 0;logMessage(DEBUG, client#: %s\n, buffer);std::string response _func(buffer);send(sock, response.c_str(), response.size(), 0);}else if (n 0){// 客户端退出 先将sock从epoll中移除再关闭sockepoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);close(sock);logMessage(NORMAL, client quit,me too ...\n);}else{epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);close(sock);logMessage(WARNING, recv error ...\n);}}}}
在调用 epoll_wait 函数时将 timeout 值设置为了 -1运行服务器后如果没有客户端发来的连接请求服务器就会在 epoll_wait 函数后进行阻塞等待 使用 telnet 工具连接 epoll 服务器后epoll 服务器调用 epoll_wait 函数在检测到监听套接字的读事件就绪后就会调用 accept 函数获取建立好的连接 使用 ls /proc/PID/fd 命令查看当前 epoll 服务器文件描述的使用情况文件描述符0、1、2 是默认打开的分别是 标准输入标准输出标准错误3号文件描述符是监听套接字4号文件描述符是服务器创建的 epoll 模型5号文件描述是 telnet 工具建立连接的客户端 epoll 的优点与 select 的缺点对应
• 接口使用起来更方便虽然被拆分成了三个函数但使用起来更方便高效不需要每次循环设置关注的关键描述符也做到了输入输出参数分离开
• 数据拷贝轻量只在新增监视事件都时候调用 epoll_ctl 时将数据从用户拷贝到内核而 select 和 poll 每次都需要重新将需要监视的事件从用户拷贝到内核。此外调用 epoll_wait 获取就绪事件时只会拷贝就绪的事件不会进行不必须的拷贝操作
• 事件回调机制避免使用遍历而是使用回调函数的方式将就绪的文件描述符结构加入到就绪队列中epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪这个操作的时间复杂度是O(1)即使文件描述符数量很多效率也不会受到影响
• 没有数量限制监视的文件描述符数量无上限只要内存允许就可以一直向红黑数中新增节点
注意 网上说epoll 使用了内存映射机制 • 内存映射机制内核直接将就绪队列通过 mmap 的方式映射到用户态避免了拷贝内存这样的额外性能开销 • 这种说法是不准确的实际操作系统并没有做任何的映射机制操作系统是不相信任何人的不会让用户进程直接访问到内核的数据用户只能通过系统调用来获取内核的数据 • 用户要获取内核当中的数据势必需要将内核的数据拷贝到用户空间 select、poll 和 epoll 的不同之处
• 在使用 select 和 poll 时都需要借助第三方数组来维护之前的文件描述符以及需要监视的事件这个第三方数据时用户自己维护的对用户的增删改操作都是用户自己进行
• epoll 的使用不需要用户自己维护第三方数据底层的红黑树就充当了这个数组的功能该红黑树的增删改操作都是内核来维护的用户只需要调用 epoll_ctl 函数让内核对红黑树进行对应的操作即可
• 使用多路转接接口时数据流都有两个方向一个是告知内核一个是告知用户select 和 poll 将这两个事件都交给了同一个函数来完成而 epoll 在接口层面上将这两件事情分离了调用 epoll_ctl 完成用户告知内核epoll_wait 完成内核告知用户 epoll 的工作方式
epoll 有两种工作模式一种是水平触发工作模式一种是边缘触发工作模式
水平触发Level TriggeredLT
• 只要底层有事件就绪epoll 就会一直通知用户epoll 默认状态下就是 LT 的工作模式select 和 poll 也是
• 当 epoll 检测底层读事件就绪时可以不立即处理或者只处理一部分因为只要底层数据没有处理完下一次 epoll 还会通知用户事件就行
• 支持阻塞读写和非阻塞读写
边缘触发Edge TriggeredET
• 只有底层就绪事件数量由无到有或由有到多发生变化时从会通知用户
• epoll 检测到底层读事件就绪时必须立即进行处理而且必须全部处理完毕有可能此后底层再也没有事件就绪epoll 就再也不会通知用户进行事件处理此时相当于没有处理完的数据丢失了
• ET 模式下 epoll 通知用户的次数比LT模式下少因此ET的性能比LT性能高Nginx 就是采用ET模式使用的 epoll
• 只支持非阻塞读写
如果将 epoll 服务器修改为 ET 工作模式就需要在初始化服务器添加事件时设置 EPOLLET 选项 void InitServer(){// 1.创建套接字_listensock Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);// 2.创建epoll模型_epfd epoll_create(size);if (_epfd 0){logMessage(ERROR, create epoll 模型 error);exit(EPOLL_CREATE_ERR);}// 3.将listensock添加进入epoll中struct epoll_event ev;ev.events EPOLLIN | EPOLLET; //添加 EPOLLET 选项ev.data.fd _listensock;epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock, ev);// 申请就绪空间的大小_erfds new struct epoll_event[_num];logMessage(NORMAL, Init epoll success!);}
此时注释掉事件处理逻辑函数并运行服务器此时因为服务器工作模式是 ET 的所以只通知用户及时取走数据一次 ET 工作模式下如何进行读写
在ET工作模式下当写事件必须一次向发送缓冲区写满否则可能再也没有机会进行读写读事件前面以及说过了必须一次性读取完。
• 当底层读事件就绪时循环调用 recv 函数进行读取直到某次调用 recv 读取时实际读取到的字节数小于期望读取到字节数说明本次底层数据已经读取完毕
• 有可能最后一次调用 recv 函数读取时刚好读取的字节数和期望的字节数相等此时底层数据也读取完毕如果再调用 recv 函数读取就会因为底层没有数据而被阻塞住
• 如果服务器是单进程的recv 被阻塞住此时该数据再也就绪相当于服务器挂掉了因此 ET 工作模式下循环调用 recv 函数进行读取时必须将对应的文件描述设置为非阻塞
• 调用 send 函数写数据时也是一样的原理必须将对应的文件描述符设置为非阻塞必须的必