当前位置: 首页 > news >正文

Linux 网络通信epoll详解( 10 ) -【Linux通信架构系列 】

系列文章目录

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技术简介

(1)I/O多路复用技术用于监控多个TCP连接上的数据收发,而epoll就是一种在Linux上使用的I/O多路复用并支持高并发的典型技术。传统的select、poll也是I/O多路复用技术,但这2种技术受内部实现的限制,不支持高并发,如同时连入超过1000个客户端,性能就会明显下降。(epoll技术从linux内核2.6开始引入的)。

(2)epoll技术的性能,可以说非常惊艳,它是能够使单台计算机支撑数百万甚至数十万上百万并发的核心技术,远优于其他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对象中有很多成员,这里只关注其中的rbrrdlist。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上的关联事件。
    添加事件之后,当这种事件到来,系统会通知程序去处理。所谓添加事件,就是在红黑树上添加一个节点。每个客户端连入服务器之后,服务器都会创建一个对应的socket(accept函数的返回值)用于与客户端通信,因为操作系统会保证每个连入服务器的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功能

  • 阻塞一小段时间并等待事件发生,返回事件集合,既获取内核的时间通知。换句话说就是遍历双向链表,把双向链表中节点相关的数据复制出去,并从双向链表中删除该节点。因为所有有数据的socket(TCP连接)都在双向链表里记录着。
    参数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的编程难度很大。

客户端实例代码,方便下面运行结果演示:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/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”字符串。
如果该事件没有处理完,就会被一直触发。

代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<errno.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//参数:size(int 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//参数:epfd(int epfd) 表示用于注册监视对象的epoll例程的文件描述符//参数:op(int op)表示用于指定监视对象的添加、删除或更改等操作//参数:fd(int fd)表示需要注册的监视对象文件描述符//参数:event(epoll_event event)表示监视对象的事件类型epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);while(1){// --- epoll_wait: 等待文件描述符发生变化,成功时会返回文件描述符数,失败时返回-1 ----//参数:epfd(int epfd) 表示事件发生监视范围的epoll例程的文件描述符//参数:epevents(epoll_event events)表示指向缓冲区保存发生事件的文件描述符集合的结构体地址//参数:EPOLL_SIZE(int maxevents)表示第二个参数中可以保存的最大事件数//参数:-1(int 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-blocking)I/O,更改套接字属性。

代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<errno.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//参数:size(int 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//参数:epfd(int epfd) 表示用于注册监视对象的epoll例程的文件描述符//参数:op(int op)表示用于指定监视对象的添加、删除或更改等操作//参数:fd(int fd)表示需要注册的监视对象文件描述符//参数:event(epoll_event event)表示监视对象的事件类型epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);while(1){// --- epoll_wait: 等待文件描述符发生变化,成功时会返回文件描述符数,失败时返回-1 ----//参数:epfd(int epfd) 表示事件发生监视范围的epoll例程的文件描述符//参数:epevents(epoll_event events)表示指向缓冲区保存发生事件的文件描述符集合的结构体地址//参数:EPOLL_SIZE(int maxevents)表示第二个参数中可以保存的最大事件数//参数:-1(int 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函数时都会产生相应的事件。而事件也会累加,服务器端能承受吗?这在现实中不可能的(本省并不合理,因此是根本不想做的事)。

相关文章:

Linux 网络通信epoll详解( 10 ) -【Linux通信架构系列 】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the…...

java源码-List源码解析

Java中的List是一个接口&#xff0c;它定义了一组操作列表的方法。List接口的常见子类包括ArrayList、LinkedList和Vector等。 以下是Java中List接口及其常见方法的源码解析&#xff1a; 1. List接口定义 public interface List<E> extends Collection<E> { …...

Mybatis的动态SQL

动态 sql 是Mybatis的强⼤特性之⼀&#xff0c;能够完成动态的 sql 语句拼接。 动态 SQL 大大减少了编写代码的工作量&#xff0c;更体现了 MyBatis 的灵活性、高度可配置性和可维护性。 Mybatis里的动态标签主要有: <if><trim><where><set><forea…...

嵌入式系统中的GPIO控制:从理论到实践与高级应用

本文将探讨嵌入式系统中的GPIO(通用输入输出)控制,着重介绍GPIO的原理和基本用法。我们将使用一个实际的示例项目来演示如何通过编程配置和控制GPIO引脚。将基于ARM Cortex-M微控制器,并使用C语言进行编写。 GPIO是嵌入式系统中最常见且功能最强大的接口之一。它允许硬件工…...

7D透明屏的市场应用广泛,在智能家居中有哪些应用表现?

7D透明屏是一种新型的显示技术&#xff0c;它能够实现透明度高达70%以上的显示效果。这种屏幕可以应用于各种领域&#xff0c;如商业广告、展览展示、智能家居等&#xff0c;具有广阔的市场前景。 7D透明屏的工作原理是利用光学投影技术&#xff0c;将图像通过透明屏幕投射出来…...

[游戏开发][Unity] 打包Xcode工程模拟器+真机调试

苹果开发者账号 账号分三类&#xff0c;个人&#xff0c;公司&#xff0c;企业&#xff0c;价格99/99/299美金 新注册账号的基本设置按网上的教程来就行 我们公司是企业账号&#xff0c;我的苹果开发者账号是公司一个User 下面讲述一下一个全新的打包机处理流程 首先是要把…...

python 添加环境变量

1 查看是否设置环境变量 和 使用的python在哪里安装 import sys import os# 获取Python的安装目录 import os import syspython_path sys.executable # 这个是python.exe的路径python_path os.path.dirname(python_path) print("Python安装路径:", python_path)# …...

如何用DHTMLX组件为Web应用创建甘特图?(一)

dhtmlxGantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的所有需求&#xff0c;是最完善的甘特图图表库。甘特图仍然是项目管理应用程序中最需要的工具之一&#xff0c;DHTMLX Gantt组件提供了能提升研发甘特图功能所需的重要工具。 在这篇…...

网站SEO优化:提升搜索排名与流量引爆

导言&#xff1a; 在互联网时代&#xff0c;网站SEO&#xff08;搜索引擎优化&#xff09;是提高网站搜索排名、吸引流量、增加曝光的重要策略。通过优化网站结构、内容和链接等方面&#xff0c;让搜索引擎更好地理解和收录网站内容&#xff0c;从而为网站带来更多有价值的有机…...

Java lamda对List<JSONObject>里多个动态属性字段进行动态的降序或者升序

最近做到一个需求&#xff0c;需要把业务侧返回的数据&#xff08;格式为List<JSONObject>&#xff09;,然后根据前端传来的排序字段、以及升降序属性来排序并返回给前端。要对List<JSONObject>中的多个属性字段进行动态的升序或降序排序&#xff0c;我们可以根据需…...

Lua脚本解决多条命令原子性问题

Redis是一个流行的键值存储数据库&#xff0c;它提供了丰富的功能和命令。在Redis中&#xff0c;我们可以使用Lua脚本来编写多条命令&#xff0c;以确保这些命令的原子性执行。Lua是一种简单易学的编程语言&#xff0c;下面将介绍如何使用Redis提供的调用函数来操作Redis并保证…...

NAT详解(网络地址转换)

一句话说清楚它是干什么的&#xff1a; 网络地址转换&#xff1a;是指通过专用网络地址转换为公用地址&#xff0c;从而对外隐藏内部管理的IP地址&#xff0c;它使得整个专用网只需要一个全球IP就可以访问互联网&#xff0c;由于专用网IP地址是可以重用的&#xff0c;所以NAT大…...

【第一阶段】ktolin的函数

在Java中我们称之为方法&#xff0c;方法必须写在类里面&#xff0c;依赖于类。 在kotlin中函数写在类里面和外面都是可以的。称之为函数 class test{fun view(){} }fun main() {println("Hello, world!!!") }执行结果 Hello, world!!!main函数的返回值类型为Unit等…...

pytorch模型的保存与加载

1 pytorch保存和加载模型的三种方法 PyTorch提供了三种种方式来保存和加载模型&#xff0c;在这三种方式中&#xff0c;加载模型的代码和保存模型的代码必须相匹配&#xff0c;才能保证模型的加载成功。通常情况下&#xff0c;使用第一种方式&#xff08;保存和加载模型状态字…...

IDE /完整分析C4819编译错误的本质原因

文章目录 概述基本概念代码页标识符字符集和字符编码方案源字符集和执行字符集 编译器使用的字符集VS字符集配置 有何作用编译器 - 源字符集编译器 -执行字符集 Qt Creator下配置MSVC编译器参数动态库DLL字符集配置不同于可执行程序EXE总结 概述 本文将从根本原因上来分析和解…...

前端学习路线(2023)

这个前端学习路线看起来很详细和全面&#xff0c;涵盖了从基础知识到高级框架&#xff0c;从单机开发到全栈项目&#xff0c;从混合应用到原生应用&#xff0c;从性能优化到架构设计的各个方面。如果你能够按照这个路线学习和实践&#xff0c;我相信你一定能够成为一名优秀的前…...

景区如何对旅行社进行分销管理?

旅行社的买票能力强&#xff0c;一般景区会跟多家旅行社合作门票分销。其中卖票下单、价格设定、财务对账结算都出现了很多问题&#xff0c;导致对账困难&#xff0c;查询困难&#xff0c;甚至可能有偷票漏票的情况出现&#xff0c;给景区收入造成损失。那要怎么处理呢&#xf…...

四步从菜鸟到高手,Python编程真的很简单(送书第一期:文末送书2本)

&#x1f341;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支持&#xff0c;我…...

Thread类的常用结构(java))

1 构造器 public Thread() :分配一个新的线程对象。public Thread(String name) :分配一个指定名字的新的线程对象。public Thread(Runnable target) :指定创建线程的目标对象&#xff0c;它实现了Runnable接口中的run方法public Thread(Runnable target,String name) :分配一…...

CSS :nth-child

CSS :nth-child :nth-child 伪类根据元素在同级元素中的位置来匹配元素. CSS :nth-child 语法 值是关键词 odd/evenAnB最新的 [of S] 语法权重 浏览器兼容性 很简单的例子, 来直觉上理解这个伪类的意思 <ul><li class"me">Apple</li><li>B…...

国内好用的企业级在线文档有哪些?

在当今数字化时代&#xff0c;企业级在线文档已经成为了现代办公环境中不可或缺的一部分。它不仅能够提高工作效率&#xff0c;还能够实现多人协同编辑&#xff0c;满足团队协作的需求。那么&#xff0c;在国内市场上&#xff0c;哪些企业级在线文档产品备受企业青睐呢&#xf…...

P1217 [USACO1.5] 回文质数 Prime Palindromes

题目描述 因为 151 151 151 既是一个质数又是一个回文数&#xff08;从左到右和从右到左是看一样的&#xff09;&#xff0c;所以 151 151 151 是回文质数。 写一个程序来找出范围 [ a , b ] ( 5 ≤ a < b ≤ 100 , 000 , 000 ) [a,b] (5 \le a < b \le 100,000,000…...

【STM32MP1系列】DDR内存测试用例

DDRDDR内存测试 一、uboot下测试DDR内存二、Linux内核下测试DDR内存1、使用memtester测试DDR内存2、使用stressapptest测试DDR内存三、Buildroot中构建memtester软件包四、搭建stressapptest软件包五、注意事项一、uboot下测试DDR内存 输入bdinfo查看DDR起始地址以及大小: b…...

【CAS6.6源码解析】调试Rest API接口

CAS的web层默认是基于webflow实现的&#xff0c;ui和后端是耦合在一起的&#xff0c;做前后端分离调用和调试的时候不太方便。但是好在CAS已经添加了支持Rest API的support模块&#xff0c;添加相应模块即可。 文章目录 添加依赖并重新build效果 添加依赖并重新build 具体添加…...

C++设计模式之模板方法、策略模式、观察者模式

面向对象设计模式是”好的面向对象设计“&#xff0c;所谓”好的面向对象设计“指的是可以满足”应对变化&#xff0c;提高复用“的设计。 现代软件设计的特征是”需求的频繁变化“。设计模式的要点是”寻求变化点&#xff0c;然后在变化点处应用设计模式&#xff0c;从而更好地…...

【计算机网络 02】物理层基本概念 传输媒体 传输方式 编码与调制 信道极限容量 章节小结

第二章 -- 物理层 2.1 物理层基本概念2.2 物理层下的传输媒体2.3 传输方式2.4 编码与调制2.5 信道极限容量2.6 章节小结 2.1 物理层基本概念 2.2 物理层下的传输媒体 传输媒体也称为传输介质或传输媒介&#xff0c;他就是数据传输系统中在发送器和接收器之间的物理通路 传输媒…...

无涯教程-jQuery - serialize( )方法函数

serialize()方法将一组输入元素序列化为数据字符串。 serialize( ) - 语法 $.serialize( ) serialize( ) - 示例 假设无涯教程在serialize.php文件中具有以下PHP内容- <?php if( $_REQUEST["name"] ) {$name$_REQUEST[name];echo "Welcome ". $na…...

一套不错的基于uniapp实现的投票类小程序/H5

最近作者心血来潮&#xff0c;想做一个热点话题投票&#xff0c;话题相关的资讯跟踪类的小程序&#xff0c;方便自己发布一些大家比较关心的话题。 基于以上需求&#xff0c;说干就干&#xff0c;首先需要定义一个需求&#xff1a; 1、支持热门话题投票、排行榜&#xff08;日…...

Mac代码编辑器sublime text 4中文注册版下载

Sublime Text 4 for Mac简单实用功能强大&#xff0c;是程序员敲代码必备的代码编辑器&#xff0c;sublime text 4中文注册版支持多种编程语言&#xff0c;包括C、Java、Python、Ruby等&#xff0c;可以帮助程序员快速编写代码。Sublime Text的界面简洁、美观&#xff0c;支持多…...

django------模糊查询

1.常用模糊查询的方法 queryset中支持链式操作 bookBook.objects.all().order_by(-nid).first() 只要返回的是queryset对象就可以调用其他的方法,直到返回的是对象本身 大于、大于等于、小于、小于等于&#xff1a; # __gt 大于> # __gte 大于等于> # __lt 小于< …...