长沙网站建设哪个公司好,公司做网络宣传哪个网站比较好,太原定制网站开发制作,phpcms v9农业网站模板系列文章目录
C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程
期待你的关注哦#xff01;#xff01;#xff01;
现在的一切都是为将来的梦想编织翅膀#xff0c;让梦想在现实中展翅高飞。 Now everything is for the…系列文章目录
C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程
期待你的关注哦
现在的一切都是为将来的梦想编织翅膀让梦想在现实中展翅高飞。 Now everything is for the future of dream weaving wings, let the dream fly in reality. Linux 网络通信epoll详解 系列文章目录一、epoll技术简介二、epoll工作原理2.1 epoll_create函数 - [ 创建一个epoll对象 ]2.1.1 epoll_create格式2.1.2 epoll_create功能2.1.3 epoll_create原理 2.2 epoll_ctl函数 - [ 向epoll对象添加/删除、修改一个socket管理的链接 ]2.2.1 epoll_ctl格式2.2.2 epoll_ctl功能2.2.3 epoll_ctl原理 2.3 epoll_wait函数 - [ 等待其管理连接上的I/O事件 ]2.2.1 epoll_wait格式2.2.2 epoll_wait功能2.2.3 epoll_wait原理 2.4 内核向双向链表增加节点 三、ET边缘触发、LT水平触发模式深入3.1 epoll实例 - 水平触发3.2 epoll实例 - 边缘触发3.3 水平触发和边缘触发孰优孰劣 一、epoll技术简介
1I/O多路复用技术用于监控多个TCP连接上的数据收发而epoll就是一种在Linux上使用的I/O多路复用并支持高并发的典型技术。传统的select、poll也是I/O多路复用技术但这2种技术受内部实现的限制不支持高并发如同时连入超过1000个客户端性能就会明显下降。epoll技术从linux内核2.6开始引入的。
2epoll技术的性能可以说非常惊艳它是能够使单台计算机支撑数百万甚至数十万上百万并发的核心技术远优于其他I/O模型或I/O函数如select、poll函数select和poll这类技术因为系统内部实现问题当并发客户端同时连入时数量超过1000~2000时性能就开始急剧下降但epoll技术完全没有这种问题性能不会随着并发数量的提高而出现明显下降。当然并发数高需要的内存也更大所以即便是并发数量的急剧提高对性能影响不大但是内存总是有限的换句话说并发数也总是有限制的不可能无限增加。
3 即使有10万个并发连接同一时刻有10万个客户端保持和服务器的连接这个10万个连接通常也不可能在同一时刻都在收发数据一般在同一时刻通常只有其中几十个或几百个连接在收发数据其他连接可能处于只在连接而没有收发数据的状态。如果以100ms为间隔判断一次可能这100ms内只有100个活跃连接有数据收发的连接把这100个活跃连接的数据放在一个专门的地方后续到这个专门的地方来只需要处理100条数据。处理起来是不是没有压力呀这就是epoll处理方式。而select和poll是依次判断这10万个连接上有没有发来的数据实际上有数据的只有100个连接有数据则处理。不难想象每次检查10万个连接与每次检查100个连接相比是巨大的资源和时间浪费所以并发数超过1000 ~ 2000的时候select和poll技术或者说这种函数、这种模型的性能将急剧下降。
4很多处理网络通信的服务器程序都是多进程每个进程对应一个客户端的连接的也有多线程每个线程对应一个客户端的连接的但是进程或者线程增多即使不计进程或者线程本身的消耗进程或线程之间的时间片/上下文的频繁切换也非常消耗性能的。而epoll技术是一种简单粗暴有效的技术采用事件驱动机制只在单独的进程或者线程里收集和处理事件没有进程或线程的切换消耗。
二、epoll工作原理
2.1 epoll_create函数 - [ 创建一个epoll对象 ]
当用户进程调用epoll_create时内核会创建一个struct eventpoll的内核对象并把它关联到当前进程的已打开文件列表中。
2.1.1 epoll_create格式
epoll_create函数格式如下 int epoll_create(int size);
2.1.2 epoll_create功能
创建一个epoll对象返回一个对象文件描述符来标识该epoll对象后续要通过操作该描述符来进行数据的收发 该对象最终要close关闭因为它是个描述符或者说是个句柄总是要关闭的 格式中的size要保证值大于0以免出现不可预料的问题
2.1.3 epoll_create原理 图2_1 epoll结构 源码中的找到该函数实现的源码 struct eventpoll *ep (struct eventpoll * )calloc(1, sizeof(struct eventpoll)); ** 生成一个eventpoll对象**想象系统生成一个结构体 eventpoll对象中有很多成员这里只关注其中的rbr和rdlist。000 ① rbr。可以将该成员理解成一颗红黑树根节点指针。 使用红黑树为了支持对海量连接的高效查找、插入、删除eventpoll内部使用一颗红黑树通过这棵树来管理用户进程下添加进来的所有socket连接。 红黑树是一种数据结构用于保存数据一般都是存键 / 值key / value对。红黑树的特点是能够极快快速地根据给的key键找到并取出value值。这里的key一般是个数字value代表的可能是一批数据。如果value是一个数据结构通过一个数字key在红黑树里查找就可以快速找到value一个结构体里面有一批数据。因为红黑树查找速度快效率高所以在epoll技术中引入了红黑树的。 ② rdlist。可以将该成员理解成代表一个双向链表的表头指针 就绪的描述符链表。当有连接就绪的时候内核会把就绪的连接放到rdlist链表里。这样应用进程只需要判断链表就能找出就绪连接而不是去遍历整颗树。 双向链表也是一种数据结构特点是顺序访问里面的节点速度非常快沿着它的链往下走遍历就可以。与上面的红黑树相比红黑树随机查找任意一个节点快双向链表顺序往下访问每个节点各有各的特点和用途。 ③wq。等待队列链表。 软中断数据就绪的时候会通过wq来找到阻塞在epoll对象上的用户进程。 总结一下epoll_create函数 1创建一个event_poll结构对象被系统保存起来。 2对象中的rbr成员被初始化成指向一颗红黑树的根有了这个根就可以向红黑树中插入节点或者说插入数据了。 3对象中的rdlist成员被初始化成指向一个双向链表的根有了这个根就可以向双向链表中插入节点数据。
接下来我们看下系统怎样使用eventpoll结构对象来处理高达百万的并发。
2.2 epoll_ctl函数 - [ 向epoll对象添加/删除、修改一个socket管理的链接 ]
2.2.1 epoll_ctl格式
epoll_create函数格式如下 int epoll_ctl(int efpd, int op, int socketid, struct epoll_event *event);
2.2.2 epoll_ctl功能 把一个socket及socket相关的事件添加到epoll对象描述符中已通过该epoll对象来监视该socket也就是该TCP连接上数据的来往情况当有数据来往时系统会通知程序。 我们会通过epoll_ctl函数把程序中需要关注感兴趣的事件整个系统约有7 ~ 8个事件添加到epoll对象描述符中当这些事件到来时系统会通知程序。 ① 参数efpd。 从epoll_create返回的epoll对象描述符。 ② 参数op。 一个操作类型宏定义 EPOLL_CTL_ADD添加sockid上的关联事件。 EPOLL_CTL_MOD修改sockid上的关联事件。 EPOLL_CTL_DEL删除sockid上的关联事件。 添加事件之后当这种事件到来系统会通知程序去处理。所谓添加事件就是在红黑树上添加一个节点。每个客户端连入服务器之后服务器都会创建一个对应的socketaccept函数的返回值用于与客户端通信因为操作系统会保证每个连入服务器的socket值都不重复所以系统就会以socket值为key把节点添加到红黑树中红黑树的key要求不能重复。 修改事件就是修改红黑树节点中的一些值。所以想要修改事件必须先调用EPOLL_CTL_ADD把事件添加到红黑树上。如原来添加epoll对象描述符中3个事件现在想修改成只关注2个事件这就需要调用EPOLL_CTL_MOD。 删除事件如原本关注3个事件现在想减少1个事件变成关注2个事件就需要调用EPOLL_CTL_MOD而不是EPOLL_CTL_DEL。EPOLL_CTL_DEL的真是动作是从红黑树中删除节点不是关闭这个TCP连接这会导致程序无法收到所有该TCP连接上的事件通知所以这一项只有在需要的时候才用。 ③ 参数sockid。 一个TCP连接。添加事件往红黑树中增加节点时就是用socketid作为key往红黑树中增加节点。 ④ 参数event。 向epoll_ctl函数传递信息。如要增加一些事件就可以通过event参数将具体事件传递进epoll_ctl函数。 事件类型 EPOLLIN需要读取数据的情况。 EPOKKOUT: 输出缓冲为空可以立即发送数据的情况。 EPOLLPRI: 收到OOB数据的情况。 EPOLLRDHUP: 断开连接或半关闭的情况这在边缘触发方式下非常有用。 EPOLLERR: 发生错误的情况。 EPOLLET: 以边缘触发方式得到事件通知。 EPOLLONESHOT: 发生一次事件后相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD再次设置事件。
2.2.3 epoll_ctl原理 ① 我们看下源码实现。如果传递进来的是一个EPOLL_CTL_ADD首先查找红黑树上是否已经有了该节点如果有了则直接返回没有程序往下走。确认红黑树没有该节点的情况下此时来生成一个epitem对象该epitem对象就是后续增加到红黑树中的一个节点。 图2_2_3就是即将向红黑树中插入一个节点该节点的key保存在成员sockfd中要增加的事件保存在成员event中然后将该节点插入红黑树中。对于红黑树来讲每个节点都要记录自己的左子树、右子树和父节点图中rbn成员本身又是个结构类型该结构中包含指向左子树、右子树、父节点的指针成员。如果将来多个用户连入服务器需要向这颗红黑树中加入很多节点这些节点彼此也要连接起来。 总之对于红黑树的每个节点通过rbn成员做到有父节点的就指向父节点有子节点的就指向子节点父节点、子节点都有就既指向父节点又指向子节点即可。 由谁向红黑树中增加节点呢 实际上是epoll_ctl(EPOLL_CTL_ADD)每个红黑树节点其实就代表一个TCP连接。 ② 如果传递进来的是一个EPOLL_CTL_MOD找到已存在的红黑树节点把该节点中的的一些数据event做一些修改。 ③ 如果传递进来的是EPOLL_CTL_DEL找到已存在的红黑树节点从红黑树中删除该节点释放对应的内存把某个节点从红黑树上删除之后该节点对应的TCP连接所发生的事件就没办法知道了。 总结EPOLL_CTL_ADD等价于往红黑树中增加节点EPOLL_CTL_MOD等价于修改红黑树的节点EPOLL_CTL_DEL等价于从红黑树中删除该节点。 所以每一个连入的客户端都应该调用epoll_ctl向红黑树增加一个红黑树节点如果有100万个并发连接红黑树上就会有100万个节点。 现在这100万个连接增加到红黑树中来了相关的程序感兴趣的事件也一起增加到了红黑树的节点中当某些TCP连接上发生这些事件比如连入、断开、有数据收发等时操作系统就会通知程序。 程序如何接收到这些操作系统的通知呢接下来我们看下epoll_wait函数。
2.3 epoll_wait函数 - [ 等待其管理连接上的I/O事件 ]
2.2.1 epoll_wait格式
epoll_create函数格式如下 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
2.2.2 epoll_wait功能
阻塞一小段时间并等待事件发生返回事件集合既获取内核的时间通知。换句话说就是遍历双向链表把双向链表中节点相关的数据复制出去并从双向链表中删除该节点。因为所有有数据的socketTCP连接都在双向链表里记录着。 ① 参数efpd 从epoll_create返回的epoll对象对象描述符。 ② 参数events 一个数组长度为maxevents表示此次调用epoll_wait函数最多可以收集到maxevents个已经就绪已经准备好的读写事件。实际的读写事件由本函数返回值决定换句话说返回的是有事件发生的TCP连接的数目但因为内存所限可能100个TCP上有事件发生但返回的数字却是80 —— 小于100。 ③ 参数timeout 阻塞等待的时长。 总体来说该函数就是到双向链表中去把此刻同时连入的连接中有事件发生的连接拿过来后续用read、write或send、recv之类的函数收发数据。某个socket只要在双向链表中该socket一定发生了某个/某些事件换句话说只有发生了某个/某些事件的socket才会在双向链表中出现。 这就是epoll高效的原因因为epoll每次只遍历发生事件的一小部分socket连接这些socket都在这个双向链表中而不用到全部socket连接中去逐个遍历以判断事件是否到来。 epitem是一个红黑树节点同时也是一个双向链表节点所以这个epitem节点设计得非常巧妙很通用既能做为红黑树的一个节点加到红黑树中也能作为双向链表的一个节点加到双向链表中所以通过epoll_wait函数到双向链表中取节点时取出来的依旧是epitem节点。 rdlink成员有2个指针这样就能够把epitem节点插入双向链表当中。 假如有3个TCP连接上都收到了事件那么这3个TCP连接肯定都待在双向链表里了当然它们同时也待在红黑树里。
2.2.3 epoll_wait原理
源码中的找到该函数实现的源码 ① while循环用于等待一小段时间如100ms。这一小段时间内发生的事件的节点socket连接就会被操作系统放到双向链表中。 ② 等待的时间到达后确定本次返回给调用本函数epoll_wait的调用者程序的事件数量 用一个while循环把这一批事件的信息返回给调用者程序。注意从双向链表中移除返回给调用者程序的节点节点始终在红黑树中存着但是否在双向链表中取决于该节点是否收到了事件。另外epitem结构中的rdy成员用于标记该节点是否存在于双向链表中所以当从双向链表中移除时rdy成员被设置为0。
2.4 内核向双向链表增加节点
epoll_wait函数实际上是去双向链表中取节点那么是谁把这些节点插入双向链表中的呢虽然是操作系统内核。操作系统什么时候向双向链表插入节点呢显然是某个TCP连接上有事件到来时这些事件是程序员用epoll_ctl登记到红黑树里面的操作系统就会向双向链表中插入节点。 写代码时哪些事件会使操作系统把节点插入双向链表去一般分4种情况。 1客户端完成三次握手时操作系统会向双向链表插入节点这时服务器往往要调用accept函数把该连接从已完成连接队列中取走。 2当客户端关闭连接时操作系统会向双向链表插入节点这时服务器也要调用close关闭对应的socket。 3当客户端发送来数据时操作系统会向双向链表插入节点这时服务器可以调用send或者recv来收数据。 4当可以发送数据时操作系统会向双向链表插入节点这时服务器可以调用send或者write向客户端发送数据。如果客户端接收数据慢服务器端发送数据块那么服务器就得等客户端收完一批数据后才能再发下一批以免客户端噎死
三、ET边缘触发、LT水平触发模式深入
LT是水平触发属于低速模式如果该事件没有处理完就会被一直触发。 ET边缘触发属于高速模式该事件通知只会出现1次。
一般认为ET的效率很高但是ET的编程难度很大。
客户端实例代码方便下面运行结果演示
#includestdio.h
#includestdlib.h
#includestring.h
#includeunistd.h
#includearpa/inet.h
#includesys/socket.h#define BUF_SIZE 1024
void error_handling(char *message);int main(int argc, char *argv[])
{int sock;char message[BUF_SIZE];int str_len;struct sockaddr_in serv_adr;FILE *readfp;FILE *writefp;if(argc ! 3){printf(Usage: %s IP port\n, argv[0]);exit(1);}sock socket(PF_INET, SOCK_STREAM, 0);if(sock -1)error_handling(socket() error);memset(serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family AF_INET;serv_adr.sin_addr.s_addr inet_addr(argv[1]);serv_adr.sin_port htons(atoi(argv[2]));if(connect(sock, (struct sockaddr *)serv_adr, sizeof(serv_adr)) -1)error_handling(connect() error!);elseputs(Connected...........);readfp fdopen(sock, r);writefp fdopen(sock, w);while(1){fputs(Input message(Q to quit): , stdout);fgets(message, BUF_SIZE, stdin);if(!strcmp(message, q\n) || !strcmp(message, Q\n))break;fputs(message, writefp);fflush(writefp);fgets(message, BUF_SIZE, readfp);printf(Message from server: %s, message);}fclose(writefp);fclose(readfp);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc(\n, stderr);exit(1);
}
3.1 epoll实例 - 水平触发
调用read函数后输入缓冲区中仍有数据需要读取。而且会因此注册新的事件并从epoll_wait函数返回时将循环输出“return epoll_wait”字符串。 如果该事件没有处理完就会被一直触发。
代码如下
#includestdio.h
#includestdlib.h
#includestring.h
#includeunistd.h
#includearpa/inet.h
#includesys/socket.h
#includesys/epoll.h
#includefcntl.h
#includeerrno.h
//为了验证边缘触发的工作方式将缓冲区设置为4字节
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char *buf);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc ! 2){printf(Usage : %s port \n, argv[0]);exit(1);}serv_sock socket(PF_INET, SOCK_STREAM, 0);memset(serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family AF_INET;serv_adr.sin_addr.s_addr htonl(INADDR_ANY);serv_adr.sin_port htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*)serv_adr, sizeof(serv_adr)) -1)error_handling(bind() error);if(listen(serv_sock, 5) -1)error_handling(listen() error);// --- epoll_create: 创建保存epoll文件描述符的空间成功时返回epoll文件描述符失败时返回-1//参数sizeint size 表示文件描述符保存空间的大小epfd epoll_create(EPOLL_SIZE);// 表示保存发生事件的文件描述符集合的结构体地址ep_events malloc(sizeof(struct epoll_event) * EPOLL_SIZE);//发生需要读取数据情况事件时event.events EPOLLIN; event.data.fd serv_sock;// --- epoll_ctl: 向epoll空间注册并销毁文件描述符成功时返回0失败时返回1//参数epfdint epfd 表示用于注册监视对象的epoll例程的文件描述符//参数opint op表示用于指定监视对象的添加、删除或更改等操作//参数fdint fd表示需要注册的监视对象文件描述符//参数eventepoll_event event表示监视对象的事件类型epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, event);while(1){// --- epoll_wait: 等待文件描述符发生变化成功时会返回文件描述符数失败时返回-1 ----//参数epfdint epfd 表示事件发生监视范围的epoll例程的文件描述符//参数epeventsepoll_event events表示指向缓冲区保存发生事件的文件描述符集合的结构体地址//参数EPOLL_SIZEint maxevents表示第二个参数中可以保存的最大事件数//参数-1int timeout表示以1/1000秒为单位的等待时间传递-1时一直等待直到发生的事件event_cnt epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);if(event_cnt -1){puts(epoll_wait() error);break;}//为观察事件发生数而添加的输出字符串的语句puts(return epoll_wait);for(i 0; i event_cnt; i){if(ep_events[i].data.fd serv_sock){adr_sz sizeof(clnt_adr);clnt_sock accept(serv_sock, (struct sockaddr *)clnt_adr, adr_sz);event.events EPOLLIN ;event.data.fd clnt_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, event);printf(connect client: %d \n, clnt_sock);}else{str_len read(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len 0) //close request!{epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);close(ep_events[i].data.fd);printf(closed client: %d \n, ep_events[i].data.fd);break;//read函数返回-1且errno值为EAGAIN时意味着读取了输入缓冲区中的全部数据}else{write(ep_events[i].data.fd, buf, str_len); //echo!}}}}close(serv_sock);close(epfd);return 0;
}void error_handling(char *buf)
{fputs(buf, stderr);fputc(\n, stderr);exit(1);
}图3_1 epoll水平触发运行结果 从运行结果可以看出每当收到客户端数据时都会注册该事件并因此多次调用epoll_wait函数。
3.2 epoll实例 - 边缘触发
边缘触发方式中接收数据时仅注册1次该事件函数。 就是因为这种特点一旦发生输入相关事件就应该读取输入缓冲区的全部数据。因此需要验证输入缓冲区是否为空。
read函数返回-1时变量errno中的值为EAGAIN时说明没有数据可读。 既然如此为何还需要将套接字变成非阻塞模式边缘触发方式下以阻塞方式工作的read write函数有可能引起服务器端的长时间停顿。因此边缘触发方式中一定要采用非阻塞read write函数有可能引起服务端的长时间停顿。因此边缘触发方式中一定采用非阻塞read write函数。
边缘触发必知的两点 (1) 通过errno变量验证错误原因。 (2) 为了完成非阻塞Non-blockingI/O更改套接字属性。
代码如下
#includestdio.h
#includestdlib.h
#includestring.h
#includeunistd.h
#includearpa/inet.h
#includesys/socket.h
#includesys/epoll.h
#includefcntl.h
#includeerrno.h
//为了验证边缘触发的工作方式将缓冲区设置为4字节
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc ! 2){printf(Usage : %s port \n, argv[0]);exit(1);}serv_sock socket(PF_INET, SOCK_STREAM, 0);memset(serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family AF_INET;serv_adr.sin_addr.s_addr htonl(INADDR_ANY);serv_adr.sin_port htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*)serv_adr, sizeof(serv_adr)) -1)error_handling(bind() error);if(listen(serv_sock, 5) -1)error_handling(listen() error);// --- epoll_create: 创建保存epoll文件描述符的空间成功时返回epoll文件描述符失败时返回-1//参数sizeint size 表示文件描述符保存空间的大小epfd epoll_create(EPOLL_SIZE);// 表示保存发生事件的文件描述符集合的结构体地址ep_events malloc(sizeof(struct epoll_event) * EPOLL_SIZE);//设置非阻塞模式setnonblockingmode(serv_sock);//发生需要读取数据情况事件时event.events EPOLLIN; event.data.fd serv_sock;// --- epoll_ctl: 向epoll空间注册并销毁文件描述符成功时返回0失败时返回1//参数epfdint epfd 表示用于注册监视对象的epoll例程的文件描述符//参数opint op表示用于指定监视对象的添加、删除或更改等操作//参数fdint fd表示需要注册的监视对象文件描述符//参数eventepoll_event event表示监视对象的事件类型epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, event);while(1){// --- epoll_wait: 等待文件描述符发生变化成功时会返回文件描述符数失败时返回-1 ----//参数epfdint epfd 表示事件发生监视范围的epoll例程的文件描述符//参数epeventsepoll_event events表示指向缓冲区保存发生事件的文件描述符集合的结构体地址//参数EPOLL_SIZEint maxevents表示第二个参数中可以保存的最大事件数//参数-1int timeout表示以1/1000秒为单位的等待时间传递-1时一直等待直到发生的事件event_cnt epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);if(event_cnt -1){puts(epoll_wait() error);break;}//为观察事件发生数而添加的输出字符串的语句puts(return epoll_wait);for(i 0; i event_cnt; i){if(ep_events[i].data.fd serv_sock){adr_sz sizeof(clnt_adr);clnt_sock accept(serv_sock, (struct sockaddr *)clnt_adr, adr_sz);//将accept函数创建的套接字改为非阻塞模式setnonblockingmode(clnt_sock);//向EPOLLIN添加EPOLLET标志将套接字事件注册方式改为边缘触发event.events EPOLLIN|EPOLLET;event.data.fd clnt_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, event);printf(connect client: %d \n, clnt_sock);}else{while(1){//边缘触发方式中发生事件时需要读取输入缓冲区中的所有数据因此需要循环调用read函数str_len read(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len 0){epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);close(ep_events[i].data.fd);printf(closed client: %d \n, ep_events[i].data.fd);break;//read函数返回-1且errno值为EAGAIN时意味着读取了输入缓冲区中的全部数据}else if(str_len 0){if(errno EAGAIN)break;}else{write(ep_events[i].data.fd, buf, str_len);}}}}}close(serv_sock);close(epfd);return 0;
}
//设置非阻塞模式
void setnonblockingmode(int fd)
{// --- int fcntl(int filedes, int cmd, . . .); ---//fcntl成功时返回cmd参数相关值失败时返回-1//参数int filedes 表示更改目标文件描述符//参数int cmd 表示函数调用的目的//从上述声明中可以看到fcntl具有可变参数形式。如果向第二个参数传递F_GETFL可以获得第一个参数所指的文件描述符属性int 型。//反之如果传递F_SETFL,可以更改文件描述符属性//将文件套接字改为非阻塞模式//获取之前设置的属性信息int flag fcntl(fd, F_GETFL, 0);//添加非阻塞O_NONBLOCK标志fcntl(fd, F_SETFL, flag|O_NONBLOCK); //此时调用read write 函数时无论是否存在数据都会形成非阻塞文件套接字
}void error_handling(char *buf)
{fputs(buf, stderr);fputc(\n, stderr);exit(1);
}
运行客户端、服务端的边缘触发方式结果如下
图3_2 epoll边缘触发运行结果 上述的运行结果需要注意的是客户端发送消息次数和服务端epoll_wait函数调用次数。客户端从请求连接到断开连接共发送5次数据服务端也相应产生了5个事件。
3.3 水平触发和边缘触发孰优孰劣
边缘触发的优点 可以分离接受数据和处理数据的时间点。 图3_3 理解边缘触发 运行流程如下
服务端分别从客户端A、B、C接收数据。服务端按照A、B、C的顺序重新组合收到的数据。组合的数据将发送任意主机。
为完成该过程若能按如下流程运行程序服务端的实现并不难。
客户端按照A、B、C的顺序连接服务器端并依序向服务器端发送数据。需要接收数据的客户端应在客户端A、B、C之前连接到服务器端并等待。
但现实中可能频繁出现如下这些情况换言之如下情况更符合实际。
客户端C和B正向服务器端发送数据但A尚未连接到服务器端。客户端A、B、C乱序发送数据。服务端已收到数据但要接收数据的目标客户端还未连接到服务器端。
因此即使输入缓冲区收到数据注册相应的事件服务器端也能决定读取和处理这些数据的时间点这样就给服务器端的实现带来巨大的灵活性。
条件触发中无法区分数据接收和处理吗 并非不可能但在输入缓冲区收到数据的情况下如果不读取延迟处理则每次调用epoll_wait函数时都会产生相应的事件。而事件也会累加服务器端能承受吗这在现实中不可能的本省并不合理因此是根本不想做的事。