【Linux】多路IO复用技术③——epoll详解如何使用epoll模型实现简易的一对多服务器(附图解与代码实现)

在正式阅读本篇博客之前,建议大家先按顺序把下面这两篇博客看一下,否则直接来看这篇博客的话估计很难搞懂

多路IO复用技术①——select详解&如何使用select模型在本地主机实现简易的一对多服务器icon-default.png?t=N7T8http://t.csdnimg.cn/BiBib多路IO复用技术②——poll详解&如何使用poll模型在本地主机实现简易的一对多服务器icon-default.png?t=N7T8http://t.csdnimg.cn/EEzOf

在了解以上两篇博客讲解的内容后,我们正式开始本篇博客的相关内容讲解,第三种多路套接字监听技术——epoll模型

目录

EPOLL模型优化的第一部分

1.对拷贝开销的优化

2.对挂载开销的优化

3.对查找开销的优化

EPOLL模型的相关接口

相关结构体 struct epoll_event

相关函数

EPOLL模型优化的第二部分

4.监听方面的优化

EPOLL模型的优缺点

使用epoll模型实现简易一对多服务器的程序实现

程序构成           结果图示

EPOLL的两种监听模式


epoll模型是最优秀的套接字监听技术他几乎解决了select和poll模型中的所有缺点,并在其优点上进行了进一步的优化,接下来,我们就来了解一下epoll模型的进行多路套接字监听的原理吧

EPOLL模型优化的第一部分

我们知道,select和poll模型实现的监听原理其实都是按照以下这些步骤:

  1. 系统会将这个用户层的监听集合拷贝到内核层
  2. 系统会将内核层的该监听集合中监听的套接字放入IO设备等待队列,由IO设备等待队列来进行一次又一次的遍历来判断那些套接字中有数据需要处理
  3. 当IO设备等待队列发现有套接字处于就绪状态时,会传出就绪集合到内核层
  4. 系统通过select模型将该就绪集合由内核层拷贝到用户层,供用户使用

想要了解下面的内容,这里需要为大家简单讲解一种数据结构——红黑树  ,并希望大家能够记住红黑树的优点

红黑树的优势就在于非常适合用来存储有序的数据,并且增删查改的时间复杂度很低,只有O(log2n)

1.对拷贝开销的优化

在之前的博客中,我们讲过,随着select/poll模型的持续使用,会产生大量的拷贝开销和挂载开销

这些拷贝开销大致由两部分组成:

  1. 系统从用户层到内核层,内核层到用户层翻来覆去的层级转换
  2. 一旦监听集合更新,就要重新拷贝整个就绪集合,只要服务器不停,就会面临无止尽的拷贝

该如何减少第一部分的拷贝开销呢?

其实很简单粗暴,epoll模型的开发人员直接将监听集合定义在内核层。所以epoll模型的监听集合都是建立在内核层的

PS:这种监听集合实际上是红黑树,每一个叶子节点对应一个监听套接字

至于如何减少第二部分的拷贝开销,就和红黑树树这种数据结构息息相关了。

我们知道,select和poll模型都是采用数组作为作为监听集合,数组这种结构非常不便于查找其中哪些数据产生了变化,就导致当有新的套接字要放入监听集合中时,系统会将整个新的监听集合全部拷贝到内核层。

而红黑树就完美的避开了这一点,每当有新的套接字要放入监听树中时,我们只需要为这棵监听红黑树添加一个叶子节点就够了,而不是重建这棵树,这就导致了这方面的开销大大减少了

2.对挂载开销的优化

在对拷贝开销的优化方面,epoll模型的开发人员依旧采用了这种数据结构,我们来为大家简单的介绍一下原因:

我们知道,在select与poll模型中,数组这种结构非常不便于查找其中哪些数据产生了变化,所以每当有新的套接字放入监听集合中时,系统会将监听的套接字全部重新挂载到IO设备等待队列,这就导致挂载开销方面的开销非常大。

而红黑树则完美的解决了这个问题,由于红黑树的特性,每当有对应新套接字的叶子节点被放入监听树中时,系统能够很轻松的就能找到对应新套接字的叶子节点是哪一个,然后只需要将对应的新套接字放入IO设备等待队列就可以了,在挂载开销方面的消耗大大减少

3.对查找开销的优化

我们知道,虽然数组的下标是有序的,方便我们进行遍历,但是数组中的内容并不一定是有序的,这就导致我们无法根据我们目标内容来直接找到其在数组中的哪个位置,而是需要去通过循环一次次的查找,但红黑树就完美地解决了这个问题

我们知道,当我们调用accept函数让服务器与多个客户端建立TCP链接时,其返回的int类型的套接字文件描述符,其实都是有序递增的,建立一个链接返回值就+1。由于红黑树在有序数据存储方面的优势,使得服务器在对于监听就绪和取消监听方面的效率非常之高。

其实epoll模型不止在这些方面进行了优化,其他部分需要大家了解了EPOLL模型的相关接口后,才能为大家进行讲解,所以别着急,我们先来了解下EPOLL模型的相关接口吧

EPOLL模型的相关接口

以下接口的头文件都是 #include <sys/epoll.h>

相关结构体 struct epoll_event

struct epoll_event 
{
    uint32_t events;  // epoll 事件类型,包括可读,可写等
    epoll_data_t data; // 用户数据,可以是一个指针或文件描述符等
    callback();  //这个函数与用户没有关系,我们是看不见的,只有当发现对应套接字就绪时才会调用该函数进行回调操作
};

typedef union epoll_data 
{
    void *ptr;   //指向任何类型的用户数据
    int fd;   //套接字文件描述符
    uint32_t u32;   //32位的无符号整数
    uint64_t u64;   //64位的无符号整数
} epoll_data_t;

其中,events字段表示要监听的事件类型,可以是以下值之一:

  • EPOLLIN:表示对应的文件描述符上有数据可读
  • EPOLLOUT:表示对应的文件描述符上可以写入数据
  • EPOLLRDHUP:表示对端已经关闭连接,或者关闭了写操作端的写入
  • EPOLLPRI:表示有紧急数据可读
  • EPOLLERR:表示发生错误
  • EPOLLHUP:表示文件描述符被挂起
  • EPOLLET:表示将epoll设置为边缘触发模式
  • EPOLLONESHOT:表示将事件设置为一次性事件

这个回调的原理是什么呢?其实很简单

其实负责监听这些套接字事件的设备就是网卡,所以最先知道套接字就绪的就是网卡设备,这时候开发人员就想了,那我能不能实现一个功能,创建一个结构体队列,直接将该队列与网卡进行绑定,形成一种回调关系。这样的话,当网卡监听到某套接字处于就绪状态时,就通过异步通知的方式来告诉服务器是哪个套接字就绪了,不需要再一次次的遍历来判断哪个套接字就绪

所以epoll模型监听套接字的过程就如下所示:

  1. 建立结构体(ep_item)队列,队列中的每一个结构体和一个套接字对应,将该队列与网卡进行绑定
  2. 建立一个就绪链表,存放对应就绪套接字的结构体epoll_events
  3. 监听到某一套接字就绪时,调用结构体epoll_events中的callback函数,进行拆包,将结构体ep_item中的结构体epoll_events拆分出来,并弹出到就绪链表中(双向链表)
  4. 将这个就绪链表中的内容拷贝到用户在用户层定义的结构体(epoll_event)数组,以便用户使用

不理解的话可以看下面的这个图

相关函数

先来介绍一下一会会用到的参数:

  • int epfd; // 返回的监听红黑树文件描述符
  • int epoll_max; //监听套接字的最大数量
  • int option; //需要执行的操作——添加、修改、删除
  • int sockfd; //需要添加,修改,删除的socket文件描述符
  • struct epoll_event * node; //挂在红黑树上的节点
  • int array_size; //存放对应就绪套接字的结构体的数组大小
  • struct epoll_event ready_array[array_size]; //存放对应就绪套接字的结构体的数组
  • int ready_max; //允许同时监听的最大套接字数量
  • int timeout; //工作模式

PS:一般情况下,epoll_max = ready_max = array_size

我们来分别介绍一下option和timeout参数的有效值:

option参数的有效值为:

  • EPOLL_CTL_ADD:添加与目标套接字对应的结构体(epoll_event)到epoll监听树上,开始监听该套接字
  • EPOLL_CTL_MOD:修改目标套接字的监听事件
  • EPOLL_CTL_DEL:将与目标套接字对应的结构体(epoll_event)从epoll监听树上摘除,也就是不再监听该套接字

timeout参数的有效值:

  • timeout = 0:表示非阻塞监听套接字相关事件
  • timeout = -1:表示阻塞监听套接字相关事件
  • timeout > 0:表示阻塞监听时间为timout(单位:毫秒),如果这段时间内没有监听到套接字相关事件,后续变为非阻塞监听
函数功能返回值
int epoll_create(epoll_max);建立监听红黑树

1.成功,返回值为epoll监听树对应的文件描述符

2.出错,返回 -1,并设置错误码

int epoll_ctl(epfd , option , sockfd , node);添加/删除节点或修改监听事件

1.成功,返回0

2.出错,返回 -1,并设置错误码

int epoll_wait(epfd , ready_array , ready_max , timeout);监听套接字相关事件

1.成功,返回值为就绪的套接字数目

2.如果在请求的超时毫秒内没有套接字准备就绪,返回0

3.出错,返回 -1,并设置错误码

epoll_create()的错误码如下所示:

  • EINVAL:epoll_max大小不为正。
  • EMFILE:遇到了每个用户对/ proc / sys / fs / epoll / max_user_instances施加的epoll实例数量的限制。
  • ENFILE:超过系统设定的进程最大创建的文件描述符数量。
  • ENOMEM:没有足够的内存来创建内核对象。

epoll_ctl()的错误码如下所示:

  • EBADF:epfd或fd不是有效的文件描述符。
  • EEXIST:option为EPOLL_CTL_ADD,并且提供的文件描述符fd已在该epoll实例中注册。
  • EINVAL:epfd不是epoll文件描述符,或者fd与epfd相同,或者此接口不支持请求的操作option。
  • ENOENT:option是EPOLL_CTL_MOD或EPOLL_CTL_DEL,并且fd未在该epoll实例中注册。
  • ENOMEM:没有足够的内存来处理请求的操作控制操作。
  • ENOSPC:尝试在主机上注册(EPOLL_CTL_ADD)新文件描述符时遇到了/ proc / sys / fs / epoll / max_user_watches施加的限制。
  • EPERM:目标文件fd不支持epoll。

epoll_wait()的错误码如下所示:

  • EBADF:epfd不是有效的文件描述符。
  • EFAULT:具有写许可权不能访问事件指向的存储区。
  • EINTR:在任何请求的事件发生或超时到期之前,信号处理程序中断了该调用;参见signal(7)。
  • EINVAL:epfd不是epoll文件描述符,或者epoll_max小于或等于零。

EPOLL模型优化的第二部分

4.监听方面的优化

由于epoll模型的自定义结构体队列、异步通知和回调函数的使用,导致用户不再需要遍历去查找就绪的套接字是哪些,直接对自己在用户层自定义的结构体数组进行遍历处理就可以了,大大减少了时间片方面的消耗,使得服务器的处理能力实现质的飞跃

基本了解了epoll在哪些方面做出了优化后,我们就可以来聊一聊epoll模型的优缺点了

EPOLL模型的优缺点

优点:

  1. 不存在重复的拷贝开销与挂载开销

  2. 自定义实现监听队列,采用异步回调方式,无需轮询,队列体积更小

  3. 不止返回就绪的套接字数量,还返回就绪的套接字是哪些

  4. 监听的事件种类丰富

  5. 可以为不同的套接字设置不同的监听事件,不像select模型只能批量设置监听事件

  6. 可以监听的socket数量不受1024的硬限制

缺点:

  1. 仅linux系统支持
  2. 红黑树的缺点

使用epoll模型实现简易一对多服务器的程序实现

程序构成

该服务器与客户端由以下几个程序共同组成:

  • func_2th_parcel.h:定义二次包裹的函数名
  • func_2th_parcel.c:对网络初始化相关的函数进行二次包裹
  • epoll_server.c:使用poll模型的服务器程序
  • client.c:客户端程序
/*************************************************************************
        > File Name: func_2th_parcel.h
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月18日 星期三 18时32分22秒
 ************************************************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/mman.h>
#include <time.h>
#include <ctype.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <poll.h>
#include <sys/epoll.h>
 
//socket函数的二次包裹
int SOCKET(int domain , int type , int protocol);
 
//bind函数的二次包裹
int BIND(int sockfd , const struct sockaddr* addr , socklen_t  addrlen);
 
//listen函数的二次包裹
int LISTEN(int sockfd , int backlog);
 
//send函数的二次包裹
ssize_t SEND(int sockfd , const void* buf , size_t len , int flags);
 
//recv函数的二次包裹
ssize_t RECV(int sockfd , void* buf , size_t len , int flags);
 
//connect函数的二次包裹
int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen);
 
//accept函数的二次包裹
int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen);
 
//网络初始化函数
int SOCKET_NET_CREATE(const char* ip , int port);
 
//服务端与客户端建立连接并返回客户端套接字文件描述符
int SERVER_ACCEPTING(int server_fd);
/*************************************************************************
        > File Name: func_2th_parcel.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月18日 星期三 18时32分42秒
 ************************************************************************/
 
#include <func_2th_parcel.h>
 
int SOCKET(int domain , int type , int protocol){
    int return_value;
    if((return_value = socket(domain , type , protocol)) == -1){
        perror("socket call failed!\n");
        return return_value;
    }
    return return_value;
}
 
int BIND(int sockfd , const struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = bind(sockfd , addr , addrlen)) == -1){
        perror("bind call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
int LISTEN(int sockfd , int backlog){
    int return_value;   
    if((return_value = listen(sockfd , backlog)) == -1){
        perror("listen call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
ssize_t SEND(int sockfd , const void* buf , size_t len , int flags){
    ssize_t return_value;
    if((return_value = send(sockfd , buf , len , flags)) == -1){
        perror("send call failed!\n");
        return return_value;
    }
    return return_value;
}
 
ssize_t RECV(int sockfd , void* buf , size_t len , int flags){
    ssize_t return_value;   
    if((return_value = recv(sockfd , buf , len , flags)) == -1){
        perror("recv call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = connect(sockfd , addr , addrlen)) == -1){
        perror("connect call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = accept(sockfd , addr , &addrlen)) == -1){
        perror("accept call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
int SOCKET_NET_CREATE(const char* ip , int port){
    int sockfd;
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET , ip , &addr.sin_addr.s_addr);
    sockfd = SOCKET(AF_INET , SOCK_STREAM , 0);
    BIND(sockfd , (struct sockaddr*)&addr , sizeof(addr));
    LISTEN(sockfd , 128);
    return sockfd;
}
 
int SERVER_ACCEPTING(int server_fd)
{
    int client_sockfd;
    struct sockaddr_in client_addr;
    char client_ip[16];
    char buffer[1500];
    bzero(buffer , sizeof(buffer));
    bzero(&client_addr , sizeof(client_addr));
    socklen_t addrlen = sizeof(client_addr);
    client_sockfd = ACCEPT(server_fd , (struct sockaddr*)&client_addr , addrlen);
    bzero(client_ip , 16);
    //将客户端的IP地址转成CPU可以识别的序列并存储到client_ip数组中
    inet_ntop(AF_INET , &client_addr.sin_addr.s_addr , client_ip , 16);
    sprintf(buffer , "Hi , %s welcome tcp test server service..\n" , client_ip);
    printf("client %s , %d , connection success , client sockfd is %d\n" , client_ip , ntohs(client_addr.sin_port) , client_sockfd);
    SEND(client_sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL);
    return client_sockfd;
}
/*************************************************************************
        > File Name: epoll_server.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月25日 星期三 18时53分30秒
 ************************************************************************/

#include <func_2th_parcel.h>

#define MAX_EPOLL 100000

int main(void)
{
    int server_sockfd;//服务器套接字文件描述符
    int epfd;//监听树文件描述符
    struct epoll_event ready_array[MAX_EPOLL];//由于存放epoll返回的就绪结构体
    struct epoll_event node;//向监听树中放入的节点
    int ready_num;//处于就绪状态的套接字的数量
    int client_sockfd;//客户端套接字文件描述符
    char rw_buffer[1500];//读写缓冲区
    int flag;
    int recv_len = 0;//接受到的数据的长度
    int i;
    bzero(rw_buffer , sizeof(rw_buffer));//清空读写缓冲区
    server_sockfd = SOCKET_NET_CREATE("192.168.79.128" , 6060);//进行网络初始化
    //初始化epoll结构体
    node.data.fd = server_sockfd;
    node.events = EPOLLIN;//监听读事件
    //初始化监听树,并做错误处理
    if((epfd = epoll_create(MAX_EPOLL)) == -1)
    {
    	perror("epoll_create call failed\n");
    	exit(-1);
    }
    //向监听树中添加服务器套接字结构体,并做错误处理
    if((epoll_ctl(epfd , EPOLL_CTL_ADD , server_sockfd , &node)) == -1)
    {
    	perror("epoll_ctl call failed\n");
    	exit(-1);
    }
    printf("epoll_server wait TCP connect\n");
    while(1)
    {
    	//获取处于就绪状态的套接字数量
        if((ready_num = epoll_wait(epfd , ready_array , MAX_EPOLL , -1)) == -1)
        {
            perror("epoll_wait call failed\n");
            exit(0);
        }
        i = 0;
        while(ready_num)
        {
            //辨别就绪,如果是服务端套接字就绪
            if(ready_array[i].data.fd == server_sockfd)
            {
                client_sockfd = SERVER_ACCEPTING(ready_array[i].data.fd);//与客户端建立TCP链接
                node.data.fd = client_sockfd;
                if(epoll_ctl(epfd , EPOLL_CTL_ADD , client_sockfd , &node) == -1)
                {
                	perror("epoll_ctl failed\n");
                	exit(-1);
                }
            }
            //如果是客户端套接字就绪
            else
            {  
                recv_len = RECV(ready_array[i].data.fd , rw_buffer , sizeof(rw_buffer) , 0);
                flag = 0;
                //如果recv_len = 0,就说明与客户端套接字对应的客户端退出了,将对应客户端套接字移出套接字存储数组与监听集合
                if(recv_len == 0)
                {
                    perror("某一客户端与本服务器断开链接\n");
                    printf("客户端%d 与本服务器断开链接,关闭其对应的套接字并停止监听\n" , ready_array[i].data.fd);
                    //关闭该套接字
                    close(ready_array[i].data.fd);
                    //将其对应的节点从监听树上摘下来
                    epoll_ctl(epfd , EPOLL_CTL_DEL , ready_array[i].data.fd , NULL);
                    //注意,这个两个步骤不能搞反
                }
                //进行业务处理:小写字母转大写字母
                printf("服务器已接收到客户端%d 发来的信息 : %s,现在对其进行处理\n" , ready_array[i].data.fd , rw_buffer);
                while(recv_len > flag)
                {
                    rw_buffer[flag] = toupper(rw_buffer[flag]);
                    flag++;
                }
                printf("服务器已对客户端%d 发来的信息完成处理,处理后的数据为 : %s\n" , ready_array[i].data.fd , rw_buffer);
                SEND(ready_array[i].data.fd , rw_buffer , recv_len , MSG_NOSIGNAL);
                bzero(rw_buffer , sizeof(rw_buffer));
                recv_len = 0;
            }
            ready_num--;
            i++;
        }
    }
    close(server_sockfd);
    printf("server shutdown\n");
    return 0;
}
/*************************************************************************
        > File Name: client.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月19日 星期四 18时29分12秒
 ************************************************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <time.h>
 
//服务器实现大小写转换业务
 
int main()
{
    //1.定义网络信息结构体与读写缓冲区并初始化
    struct sockaddr_in dest_addr;
    char buffer[1500];
    bzero(&dest_addr , sizeof(dest_addr));
    bzero(buffer , sizeof(buffer));
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(6060);
    //字符串ip转大端序列
    inet_pton(AF_INET , "192.168.79.128" , &dest_addr.sin_addr.s_addr);
    int sockfd = socket(AF_INET , SOCK_STREAM , 0);
    int i;
    //2.判断连接是否成功
    if((connect(sockfd , (struct sockaddr*) &dest_addr , sizeof(dest_addr))) == -1)
    {
        perror("connect failed!\n");
        exit(0);
    }
    recv(sockfd , buffer , sizeof(buffer) , 0);
    printf("%s" , buffer);
    bzero(buffer , sizeof(buffer));
    //3.循环读取终端输入的数据
    while( (fgets(buffer , sizeof(buffer) , stdin) ) != NULL)
    {
        i = strlen(buffer);
        buffer[i-1] = '\0';
        //向服务端发送消息
        send(sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL);
        //接收服务端发来的消息
        recv(sockfd , buffer , sizeof(buffer) , 0);
        //打印服务端发来的信息
        printf("response : %s\n" , buffer);
        //清空读写缓冲区,以便下一次放入数据
        bzero(buffer , sizeof(buffer));
    }
    //4.关闭套接字,断开连接
    close(sockfd);
    return 0;
}

结果图示

EPOLL的两种监听模式

1.EPOLLLT水平触发模式,也被称之为负责模式,如果不进行特殊设置,默认情况下对套接字都是以此模式监听。

在此模式下,当epoll监听到某一套接字就绪时,就会先发送处理通知给上层。之后每隔一段时间就来查看该套接字缓冲区中的数据有没有被处理完。

如果没有处理完,就会继续发送处理通知给上层,一天不处理完就发一天,一周处理不完就发一周,直到缓冲区中的数据被全部处理完毕。这就是所说的负责模式

PS:在此模式下,如果缓冲区中的数据没有被处理完,epoll_wait()无法开启下一轮监听。意思就是如果数据没处理完,就去调用epoll_wait()函数的话,会立即返回,返回值为未处理的就绪事件数量

优点:能够保证套接字缓冲区中的所有数据被处理完毕

缺点:开销比较大

适用场景:该模式适合处理有效期较短、或需要被紧急处理的数据

2.EPOLLET边缘触发模式,也被称之为不负责模式,使用该模式需要进行手动设置

在此模式下,当epoll监听到某一套接字就绪时,指挥发送一次处理通知给上层。之后无论该套接字缓冲区中的数据有没有被处理完,都与epoll无关,epoll可以立即进入新一轮的监听

优点:开销比较小

缺点:存在隐患,不能够保证套接字缓冲区中的所有数据被处理(完毕),用户要自行保证数据读取完毕

PS:边缘触发模式一般结合非阻塞读取套接字缓冲区的数据

想要设置边缘触发模式也很简单,按照下面的步骤来就可以了

int sockfd;//套接字文件描述符

struct epoll_event node;
node.data.fd = sockfd;
node.events = EPOLLIN|EPOLLET;//监听读事件,监听模式切换为边缘触发模式

以上就是本篇博客的全部内容了,大家有什么地方没有看懂的话,可以在评论区留言给我,咱要力所能及的话就帮大家解答解答

今天的学习记录到此结束啦,咱们下篇文章见,ByeBye!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/117126.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Web3游戏的十字路口:沿用传统IP还是另起炉灶?

人们经常问我对 Web3 游戏有什么看法。因此&#xff0c;我想以书面形式概述一下我目前的想法。 让我先澄清一下&#xff1a;我不是专家。这不是一篇深入探讨游戏世界精细指标如 MAU 或 D14 等的全面分析。请把这看作是我根据个人交流和研究&#xff0c;这反映我在游戏领域关注…

java代码检查

目录 jacoco 引入依赖 构建配置修改 单元测试 生成报告 查看报告 报告说明 1. Instructions 2. Branches 3. Cyclomatic Complexity 4. Lines 5. Methods 6. Classes sonar7.7 基础环境 需要下载软件 解压文件并配置 运行启动 jacoco 引入依赖 <dep…

jenkins安装以及基本配置

一、docker 1.安装docker 联网安装命令如下 curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun或者也可以使用国内 daocloud 一键安装命令&#xff1a; curl -sSL https://get.daocloud.io/docker | sh2.启动docker systemctl start docker二、docker…

pycharm更改远程服务器地址

一、问题描述 在运行一些项目时&#xff0c;我们常需要在pycharm中连接远程服务器&#xff0c;但万一远程服务器的ip发生了变化&#xff0c;该如何修改呢&#xff1f;我们在file-settings-python interpreter中找到远程服务器&#xff0c;但是发现ip是灰色的&#xff0c;没有办…

Azure 机器学习 - 使用 Visual Studio Code训练图像分类 TensorFlow 模型

了解如何使用 TensorFlow 和 Azure 机器学习 Visual Studio Code 扩展训练图像分类模型来识别手写数字。 关注TechLead&#xff0c;分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验&#xff0c;同济本复旦硕&#xff0c;复旦机器人智能实验室成员…

curl(七)上传和下载

一 上传 ① -T | --upload 上传 ​1、向ftp服务器 传一个文件&#xff1a;curl -T localfile -u name&#xff1a;passwd ftp://upload_site&#xff1a;port/path/2、向http服务器上传文件curl -T localfile http://www.wzj.com/wzj.html注意: 这时候使用的协议是HTTP的PUT…

基于STM32设计的室内环境监测系统(华为云IOT)_2023

一、设计需求 基于STM32+华为云物联网平台设计一个室内环境监测系统,以STM32系列单片机为主控器件,采集室内温湿度、空气质量、光照强度等环境参数,将采集的数据结果在本地通过LCD屏幕显示,同时上传到华为云平台并将上传的数据在Android移动端能够实时显示、查看。 【1…

5.数据表基本操作

目录 1.创建数据表 创建数据表的语法格式&#xff1a; 查看当前数据库的表&#xff1a; 主键 1.单字段主键 (1)在定义列的同时指定主键&#xff0c;语法规则如下&#xff1a; (2)在定义完所有列之后指定主键。 2.多字段联合主键 外键&#xff1a; 非空约束&#xff1…

react_11

MobX 介绍 需求&#xff0c;组件0 改变了数据&#xff0c;其它组件也想获得改变后的数据&#xff0c;如图所示 这种多个组件之间要共享状态数据&#xff0c;useState 就不够用了&#xff0c;useContext 也不好用了 能够和 react 配合使用的状态管理库有 MobX Redux 其中…

黑猫带你学NandFlash第3篇:NAND寻址(行列地址和block/page/LUN之间的关系)

本文依据不同型号NandFlash spec及个人工作经验整理而成,如有错误请留言。 文章为付费内容,已加入原创侵权保护,禁止私自转载及抄袭。 文章所在专栏:《黑猫带你学:NandFlash详解》 本文大约2000字,主要讲解:nand flash如何物理寻址、多plane又是如何寻址、相关计算公式等…

Spring Data Redis + RabbitMQ - 基于 string 实现缓存、计数功能(同步数据)

目录 一、Spring Data Redis 1.1、缓存功能 1.1.1、分析 1.1.2、案例实现 1.1.3、效果演示 1.2、计数功能&#xff08;Redis RabbitMQ&#xff09; 1.2.1、分析 1.2.2、案例实现 一、Spring Data Redis 1.1、缓存功能 1.1.1、分析 使用 redis 作为缓存&#xff0c; M…

ArmSom---I2C开发指南

1. 简介 RK3588从入门到精通 本⽂介绍在rockchip平台下如何配置i2c接口的方法并且添加调试验证i2c外设的例子 开发板&#xff1a;ArmSoM-W3 Kernel&#xff1a;5.10.160 OS&#xff1a;Debian11 2. i2c接口概述 i2c 总线控制器通过串行数据&#xff08;SDA&#xff09;线…

【hcie-cloud】【1】华为云Stack解决方案介绍、华为文档获取方式 【上】

文章目录 华为文档获取方式前言云计算发展背景国家政策、社会发展驱动数字经济开启新时代深化数字化转型提升效率&#xff0c;国家数字主权云进入落地阶段从Cloud-Based到Cloud-Native&#xff0c;两种模式长期并存适合政企智能升级的云华为云Stack&#xff0c;政企智能升级首选…

ks ios快手极速版、概念版、创作版sig sig3

风控&#xff1a; 快手除了正版下&#xff0c;还有极速版&#xff0c;概念版&#xff0c;创作版。 然而看了下概念版还是风控比正版要低的多~ 抓包&#xff1a; 对于快手的ios版本抓包很简单&#xff0c;直接vpn转发抓包方式即可&#xff0c;不可用代理抓包&#xff1b;这个…

[SSD综述 1.4] SSD固态硬盘的架构和功能导论

依公知及经验整理,原创保护,禁止转载。 专栏 《SSD入门到精通系列》 <<<< 返回总目录 <<<< ​ 前言 机械硬盘的存储系统由于内部结构, 其IO访问性能无法进一步提高,CPU与存储器之间的性能差距逐渐扩大。以Nand Flash为存储介质的固态硬盘技术的发展,…

redis rdb aof

appendonly yes # appendfsync always appendfsync everysec # appendfsync no E:\Document_Redis_Windows\redis-2.4.5-win32-win64\64bit appendonly.aof

基于单片机的养殖场温度控制系统设计

博主主页&#xff1a;单片机辅导设计 博主简介&#xff1a;专注单片机技术领域和毕业设计项目。 主要内容&#xff1a;毕业设计、简历模板、学习资料、技术咨询。 文章目录 主要介绍一、控制系统设计二、系统方案设计2.1 系统运行方案设计2.1.1 羊舍环境温度的确定 三、 系统仿…

ElasticSearch高级功能详解与原理剖析

ES数据预处理 Ingest Node Elasticsearch 5.0后&#xff0c;引入的一种新的节点类型。默认配置下&#xff0c;每个节点都是Ingest Node&#xff1a; 具有预处理数据的能力&#xff0c;可拦截lndex或Bulk API的请求对数据进行转换&#xff0c;并重新返回给Index或Bulk APl 无…

项目实战:优化Servlet,把所有围绕Fruit操作的Servlet封装成一个Servlet

1、FruitServlet 这些Servlet都是围绕着Fruit进行的把所有对水果增删改查的Servlet放到一个Servlet里面&#xff0c;让tomcat实例化一个Servlet对象 package com.csdn.fruit.servlet; import com.csdn.fruit.dto.PageInfo; import com.csdn.fruit.dto.PageQueryParam; import c…

(景行锐创) 高性能计算平台 Pytorch 深度学习环境超详细教程

文章目录 前言1. 账号申请2. 登录高算平台网站3. 安装 Xshell&#xff0c;Xftp 软件4. 连接高算平台5. 安装 Anaconda6. 安装 CUDA7. 配置 cuDNN8. 安装 torch 和 torchvision9. 提交作业测试10. 解压与压缩文件11. 其他结语 前言 目前一些学校为了便于师生进行大规模的计算任…