IO多路复用技术

news2024/9/29 19:16:30

IO多路复用

一、概念

IO多路复用技术 是一种 网络通信 的方式,通过这种方式可以同时检测多个 文件描述符这个过程是阻塞的),一旦检测到某一个文件描述符(状态是 可读 或者 可写 的)是就绪的,那么就会解除阻塞,然后我们就可以基于这些已经就绪的文件描述符进行网络通信了。

通过这种方式,服务端即使是在 单线程/进程 的情况下也能实现并发,支持多个连接。

常用的 IO多路复用 的方式有 : selectpollepoll

二、IO多路复用 与 传统的多线程/进程方式 对比

传统的 多线程/进程 并发

主线程 / 父进程 调用 accept() 监测是否有客户端的连接到来:

  • 如果没有客户端连接请求到来,那么当前 线程/进程 就会阻塞在这里;
  • 如果有客户端连接请求到来,那么就先解除阻塞,建立新连接;

子线程 / 子进程 调用 send() / write() , read() / recv() 和客户端建立的连接通信 :

  • 调用 read() / recv() 接收客户端发送过来的通信数据,如果 读缓冲区 中没有数据,那么当前 线程/进程 会阻塞,直到 读缓冲区 中有了数据,阻塞就会自动解除;
  • 调用 send() / write() 向客户端发送数据,如果 写缓冲区 的容量已经满了,那么当前 线程/进程 会阻塞,直到 写缓冲区 有了空间,阻塞就会自动解除;

IO多路复用并发

使用 IO多路复用函数委托内核 检测所有客户端的文件描述符 (主要是用于 监听通信 的两类),这个过程会导致 进程/线程 的阻塞;如果内核检测到有就绪的文件描述符就会解除阻塞,然后将这些已经就绪的文件描述符传出去。

然后根据被传出的文件描述符的类型不同,做不同的处理:

1.用于监听的文件描述符 lfd :用于和客户端建立连接

  • 此时再调用 accept() 和客户端建立新连接是不会阻塞的,因为此时用于监听的文件描述符 lfd 是就绪的,也就是它对应的 读缓冲区 中有连接请求;

2.用于通信的文件描述符 cfd :调用通信函数 和 已经建立连接的客户端进行通信

  • 调用 read() / recv() 不会阻塞,因为此时用于通信的文件描述符 cfd 是就绪的,它对应的 读缓冲区 中有数据;
  • 调用 send() / write() 不会阻塞,因为此时用于通信的文件描述符 cfd 是就绪的,它对应的 写缓冲区 中有多余的容量;

3.对这些文件描述符继续进行下一轮检测,一直循环下去…

与 多线程/进程 技术相比,IO多路复用的优势在与不用频繁的进行 线程/进程的创建和销毁,不用管理 线程/进程,极大的减少了资源的消耗。

三、三种IO多路复用的方式

1.select

select 是跨平台的,同时支持 LinuxWindowsMacOS 。我们通过调用 select() 这个函数就可以委托内核帮助我们检测 文件描述符 的状态,也就是检测这些文件描述符对应的 读写缓冲区 的状态:

  • 读缓冲区: 检测里面有没有数据,如果有的话,说明其对应的文件描述符已经就绪了;
  • 写缓冲区: 检测 写缓冲区 有没有多余的容量可以写,如果有就说明这个 写缓冲区 对应的文件描述符已经就绪了;
  • 读写异常: 检测 读写缓冲区 是否有异常,如果有的话说明该缓冲区对应的文件描述符已经就绪了;

内核检测完毕文件描述符的状态之后,已经就绪的文件描述符会通过 select() 的三个参数传出,这三个参数都是一个 集合,我们得到之后就能对其进行处理。

1.函数原型
#include <sys/select.h>
struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval * timeout);

nfds : 委托内核检测的这三个集合中 最大的文件描述符 + 1

  • 内核需要遍历集合这三个集合中的文件描述符,这个值就是循环结束的条件;
  • Windows 中这个参数是无效的,直接指定为 − 1 -1 1 即可;

readfds:文件描述符的集合,内核只检测这个集合中的文件描述符对应的 读缓冲区

  • 这是一个传入传出参数,读集合一般情况下都是需要检测的,这样才知道是通过那个文件描述符接收数据;

wtitedfs:文件描述符的集合,内核只检测这个集合中的文件描述符对应的 写缓冲区

  • 这是一个传入传出参数,如果不需要这个参数可以指定为 NULL

exceptdfs:文件描述符的集合,内核只检测这个集合中的文件描述符是否有异常状态

  • 这是一个传入传出参数,如果不需要这个参数可以指定为 NULL

timeout :超时时长,用来强制解除 select() 的阻塞的

  • 如果指定为 NULL 的话,select() 检测不到就绪的文件描述符就会一直阻塞;
  • 等待固定时长:select() 检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,返回 0 0 0
  • 不等待:直接将该参数的结构体指定为 0 0 0select() 函数就不会阻塞;

返回值:

  • 大于 0 0 0 ,成功。直接返回集合中已经就绪的文件描述符的总个数;
  • 等于 − 1 -1 1,失败;
  • 等于 0 0 0,超时。没有检测到就绪的文件描述符;

需要使用到的一些函数:

// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0        
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int  FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);
2.实现细节

select() 函数中第2、3、4个参数都是 fd_set 类型,它表示一个文件描述符的集合,这个类型的数据有 128 128 128 个字节,也就是 1024 1024 1024 个标志位,和内核中文件描述符表中的文件描述符个数是一样的。

sizeof(fd_set) = 128 字节 * 8 = 1024 bit      // int [32]

这块内存中的每一个bit 和 文件描述符表中的每一个文件描述符是一一对应的关系,这样就可以使用最小的存储空间将要表达的意思描述出来了。

下图中的 fd_set 存储了要委托内核要检测的 读缓冲区的文件描述符集合

  • 如果集合中的 标志位为 0 0 0 ,代表 不检测 这个文件描述符的状态;
  • 如果集合中的 标志位为 1 1 1 ,代表 检测 这个文件描述符的状态;

在这里插入图片描述
内核在遍历这个 读集合 的过程中,如果被检测的文件描述符对应的读缓冲区中 没有数据,内核将修改这个文件描述符在读集合 fd_set 中对应的标志位,改为 0 0 0,如果有数据那么这个标志位的值不变,还是 1 1 1

在这里插入图片描述
select() 函数解除阻塞之后,被内核修改过的 读集合 通过参数传出,此时集合中只要标志位的值为 1 1 1,那么它对应的文件描述符肯定是就绪的,我们就可以基于这个文件描述符和客户端建立新连接或者通信了。

3.处理流程

1.创建监听的套接字 lfd = socket()
2.将 监听的套接字 和 本地的 ip端口 绑定 bind()
3.给监听的套接字设置监听 listen()
4.创建一个文件描述符集合 fd_set,用于存储需要检测读事件的所有的文件描述符
通过 FD_ZERO() 初始化;

  • 通过 FD_SET() 将监听的文件描述符放入检测的读集合中
  • 循环调用 select() ,周期性的对所有的文件描述符进行检测

5.select() 解除阻塞返回,得到内核传出的满足条件的就绪的文件描述符集合
6.通过 FD_ISSET() 判断集合中的标志位是否为 1 1 1

  • 如果这个文件描述符是 监听的文件描述符,调用 accept() 和客户端建立连接。将得到的新的通信的文件描述符,通过 FD_SET() 放入到检测集合中
  • 如果这个文件描述符是通信的文件描述符,调用通信函数和客户端通信
    • 如果客户端和服务器断开了连接,使用 FD_CLR() 将这个文件描述符从检测集合中删除
    • 如果没有断开连接,正常通信即可

7.重复第6步

在这里插入图片描述

4.实现

客户端代码:

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

int main(){

    //1.创建用于通信的文件描述符 cfd
    int cfd = socket(AF_INET,SOCK_STREAM,0);
    if(cfd == -1){
        perror("socket");
        return -1;
    }

    printf("1.成功创建了用于通信的文件描述符 : %d\n",cfd);

    //2.连接服务器
    unsigned short port = 10000;
    
    //你自己的服务器 或者 虚拟机的 ip地址
    const char* ip = "10.0.8.14";

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET,ip,&addr.sin_addr.s_addr);

    int ret = connect(cfd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret == -1){
        perror("connet");
        return -1;
    }
    printf("2.成功连接了服务器 , ip : %s , port : %d\n",ip,port);

    //3.开始通信
    char send_buf[1024];
    char recv_buf[1024];

    int cnt = 0;

    while(1){
        memset(send_buf,0,sizeof send_buf);
        memset(recv_buf,0,sizeof recv_buf);

        sprintf(send_buf,"hello i love you : %d",cnt++);

        //发送数据
        send(cfd,send_buf,strlen(send_buf) + 1,0);
        //接收数据
        int len = recv(cfd,recv_buf,sizeof(recv_buf),0);

        if(len > 0){
            printf("服务端 : %s\n",recv_buf);
        }
        else if(len == 0){
            printf("服务端已经断开了连接...\n");
            break;
        }
        else{
            perror("recv");
            break;
        }

        sleep(1);
    }

    close(cfd);

    return 0;
}
1.服务端基础版本
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <strings.h>
#include <string.h>
#include <sys/select.h>



int main()
{

    // 1.创建用于监听的文件描述符
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket");
        return -1;
    }

    // 2.绑定 ip 和 端口号
    unsigned short port = 10000;

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(port);
    saddr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (ret == -1)
    {
        perror("bind");
        return -1;
    }

    // 3.设置监听
    ret = listen(lfd, 128);
    if (ret == -1)
    {
        perror("listen");
        return -1;
    }
    printf("设置监听成功...\n");

    // 4.获取连接
    fd_set readset ,temp;
    FD_ZERO(&readset);


    //把用于监听的文件描述符 lfd , 加入到 readset 读集合中
    FD_SET(lfd,&readset);
    int addr_len = sizeof(struct sockaddr_in);
    int maxfd = lfd;

    
    char buf[1024];
    char* str = "ok";

    while(1){
         temp = readset;

        int ret = select(maxfd + 1,&temp,NULL,NULL,NULL);

        //检测 用于监听的文件描述符 lfd 读缓冲区是否有数据 , 也就是是否有新的连接到来
        if(FD_ISSET(lfd,&temp)){

            struct sockaddr_in addr;
            int cfd = accept(lfd,(struct sockaddr*)&addr,&addr_len);

            //将用于通信文件描述符 cfd 也加入到 读集合中
            FD_SET(cfd,&readset);

            //更新 maxfd
            maxfd = cfd > maxfd ? cfd : maxfd;
            printf("获取连接成功 , 客户端 ip : %s  , port : %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
        }

        for(int i = 0;i <= maxfd;i++){
            //用于通信的文件描述符 cfd 的读缓冲区有数据了 , 也就是有客户端发消息过来了
            if(i != lfd && FD_ISSET(i,&temp)){
                
                memset(buf,0,sizeof buf);
                int len = read(i,buf,sizeof buf);
                printf("客户端 : %s\n",buf);

                if(len > 0){
                    write(i,str,strlen(str) + 1);
                }
                else if(len == 0){
                    //客户端已经关闭了连接
                    printf("客户端已经关闭了连接...\n");
                    FD_CLR(i,&readset);
                    close(i);
                }
                else{
                    perror("read");
                }
            }
        }
    }



    close(lfd);

    return 0;
}
2.服务端多线程版本
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <pthread.h>
#include <ctype.h>

pthread_mutex_t mutex;

char buf[1024];

typedef struct fdinfo
{
    int fd;
    int *maxfd;
    fd_set *readset;

} FdInfo;

void *acceptConnection(void *arg)
{
    printf("子线程的线程id为 : %ld\n", pthread_self());

    FdInfo *info = (FdInfo *)(arg);
    int lfd = info->fd;

    struct sockaddr_in addr;
    int addr_len = sizeof(addr);

    int cfd = accept(lfd, (struct sockaddr *)&addr, &addr_len);

    pthread_mutex_lock(&mutex);

    // 将用于通信文件描述符 cfd 也加入到 读集合中
    FD_SET(cfd, info->readset);

    // 更新 maxfd
    *info->maxfd = cfd > *info->maxfd ? cfd : *info->maxfd;

    pthread_mutex_unlock(&mutex);

    printf("获取连接成功 , 客户端 ip : %s  , port : %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));

    free(info);

    return NULL;
}

void *communicate(void *arg)
{
    printf("子线程的线程id为 : %ld\n", pthread_self());

    FdInfo *info = (FdInfo *)(arg);
    int cfd = info->fd;

    memset(buf, 0, sizeof buf);
    int len = read(cfd, buf, sizeof buf);
    printf("客户端 : %s\n", buf);

    if (len < 0)
    {
        perror("read");
        free(info);
        return NULL;
    }
    else if(len == 0){
        
        // 客户端已经关闭了连接
        printf("客户端已经关闭了连接...\n");

        pthread_mutex_lock(&mutex);

        FD_CLR(cfd, info->readset);

        pthread_mutex_unlock(&mutex);

        close(cfd);

        free(info);
        
        return NULL;
    }

    int str_len = strlen(buf);
    for(int i = 0;i < str_len;i++) buf[i] = toupper(buf[i]);

    write(cfd,buf,len);

    free(info);
    return NULL;
}

int main()
{

    // 初始化 mutex
    pthread_mutex_init(&mutex, NULL);

    // 1.创建用于监听的文件描述符
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket");
        return -1;
    }

    // 2.绑定 ip 和 端口号
    unsigned short port = 10000;

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(port);
    saddr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (ret == -1)
    {
        perror("bind");
        return -1;
    }

    // 3.设置监听
    ret = listen(lfd, 128);
    if (ret == -1)
    {
        perror("listen");
        return -1;
    }
    printf("设置监听成功...\n");

    // 4.获取连接
    fd_set readset, temp;
    FD_ZERO(&readset);

    // 把用于监听的文件描述符 lfd , 加入到 readset 读集合中
    FD_SET(lfd, &readset);
    int addr_len = sizeof(struct sockaddr_in);
    int maxfd = lfd;

    while (1)
    {
        pthread_mutex_lock(&mutex);

        temp = readset;

        pthread_mutex_unlock(&mutex);

        int ret = select(maxfd + 1, &temp, NULL, NULL, NULL);

        // 检测 用于监听的文件描述符 lfd 读缓冲区是否有数据 , 也就是是否有新的连接到来
        if (FD_ISSET(lfd, &temp))
        {
            FdInfo *info = (FdInfo *)malloc(sizeof(FdInfo));
            info->fd = lfd;
            info->maxfd = &maxfd;
            info->readset = &readset;

            pthread_t tid;
            pthread_create(&tid, NULL, acceptConnection, info);
            pthread_detach(tid);
        }

        for (int i = 0; i <= maxfd; i++)
        {
            // 用于通信的文件描述符 cfd 的读缓冲区有数据了 , 也就是有客户端发消息过来了
            if (i != lfd && FD_ISSET(i, &temp))
            {
                FdInfo *info = (FdInfo *)malloc(sizeof(FdInfo));
                info->fd = i;
                info->maxfd = &maxfd;
                info->readset = &readset;

                pthread_t tid;
                pthread_create(&tid, NULL, communicate, info);
                pthread_detach(tid);
            }
        }
    }

    close(lfd);
    pthread_mutex_destroy(&mutex);

    return 0;
}

客户端不需要使用IO多路复用进行处理,因为客户端和服务器的对应关系是 1 : N 1:N 1:N,也就是说客户端是比较专一的,只能和一个连接成功的服务器通信。

虽然使用select这种IO多路转接技术可以降低系统开销,提高程序效率,但是它也有局限性:

  • 待检测集合(第 2 、 3 、 4 2、3、4 234 个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低
  • 内核对于 select() 传递进来的待检测集合的检测方式是线性的
    • 如果集合内待检测的文件描述符很多,检测效率会比较低
    • 如果集合内待检测的文件描述符相对较少,检测效率会比较高
  • 使用 select 能够检测的最大文件描述符个数有上限,默认是 1024 1024 1024,这是在内核中被写死了的。

2.poll

poll 的机制跟 select 类似,使用方法也是类似的,以下是它们两个的对比:

  • 内核检测文件描述符的状态也是通过 线性遍历 的形式;
  • pollselect 检测的文件描述符的集合 会在被检测的过程频繁的进行 用户区 和 内核区的拷贝,它的开销随着文件描述符数量的增加而增大,所以效率也会变得越来越低;
  • select 可以跨平台使用,支持 LinuxWindowsMacOS;而 poll 只能在 Linux 平台下使用;
1.函数原型
#include <poll.h>
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
    int   fd;         /* 委托内核检测的文件描述符 */
    short events;     /* 委托内核检测文件描述符的什么事件 */
    short revents;    /* 文件描述符实际发生的事件 -> 传出 */
};

struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds :这是一个 struct pollfd 类型的数组,里面存储了待检测的文件描述符的信息,它一共有三个成员:

  • fd : 委托内核检测的文件描述符;
  • events:委托内核检测的文件描述符对应的事件(读,写,错误);
  • revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果;

nfds :这是第一个参数数组中最后一个元素的下标 + 1,用来表示循环结束的条件;

timeout :指定 poll() 函数的阻塞时长:

  • − 1 -1 1,一直阻塞,直到检测的集合中有就绪的文件描述符才会解除阻塞;
  • 0 0 0,不阻塞,不管待检测的集合中有没有文件描述符,函数执行完之后就返回;
  • > 0 > 0 >0,阻塞指定的毫秒数,就解除阻塞返回;

函数返回值:

  • − 1 -1 1,失败;
  • > 0 >0 >0,成功。返回的数就是集合中已经就绪的文件描述符的总个数;
2.实现

客户端代码:

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

int main(){

    //1.创建用于通信的文件描述符 cfd
    int cfd = socket(AF_INET,SOCK_STREAM,0);
    if(cfd == -1){
        perror("socket");
        return -1;
    }

    printf("1.成功创建了用于通信的文件描述符 : %d\n",cfd);

    //2.连接服务器
    unsigned short port = 10000;
    const char* ip = "10.0.8.14";

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET,ip,&addr.sin_addr.s_addr);

    int ret = connect(cfd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret == -1){
        perror("connet");
        return -1;
    }
    printf("2.成功连接了服务器 , ip : %s , port : %d\n",ip,port);

    //3.开始通信
    char send_buf[1024];
    char recv_buf[1024];

    int cnt = 0;

    while(1){
        memset(send_buf,0,sizeof send_buf);
        memset(recv_buf,0,sizeof recv_buf);

        sprintf(send_buf,"hello i love you : %d",cnt++);

        //发送数据
        send(cfd,send_buf,strlen(send_buf) + 1,0);
        //接收数据
        int len = recv(cfd,recv_buf,sizeof(recv_buf),0);

        if(len > 0){
            printf("服务端 : %s\n",recv_buf);
        }
        else if(len == 0){
            printf("服务端已经断开了连接...\n");
            break;
        }
        else{
            perror("recv");
            break;
        }

        sleep(1);
    }

    close(cfd);

    return 0;
}

服务端代码:

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>

int main()
{

    // 1.创建用于监听的文件描述符
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket");
        return -1;
    }

    // 2.绑定 ip 和 端口号
    unsigned short port = 10000;

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(port);
    saddr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (ret == -1)
    {
        perror("bind");
        return -1;
    }

    // 3.设置监听
    ret = listen(lfd, 128);
    if (ret == -1)
    {
        perror("listen");
        return -1;
    }
    printf("设置监听成功...\n");

    // 4.获取连接

    struct pollfd fds[1024];

    for (int i = 0; i < 1024; i++)
    {
        fds[i].fd = -1;
        fds[i].events |= POLLIN;
    }

    // 把用于监听的文件描述符 lfd 加入到 fds 数组中
    fds[0].fd = lfd;

    int addr_len = sizeof(struct sockaddr_in);
    int maxfd = 0;

    char buf[1024];
    char *str = "ok";

    while (1)
    {

        int ret = poll(fds, maxfd + 1, -1);

        if(ret == -1){
            perror("poll");
            exit(0);
        }

        // 检测 用于监听的文件描述符 lfd 读缓冲区是否有数据 , 也就是是否有新的连接到来

        if (fds[0].revents & POLLIN)
        {
            struct sockaddr_in addr;

            // 获取连接 , 返回用于通信的文件描述符
            int cfd = accept(lfd, (struct sockaddr *)&addr, &addr_len);

            printf("获取连接成功 , 客户端 ip : %s  , port : %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));

            // 把用于通信的文件描述符 cfd 加入到 fds 中
            int i = 1;
            for (; i < 1024; i++)
            {
                if (fds[i].fd == -1)
                {
                    fds[i].fd = cfd;
                    break;
                }
            }

            maxfd = i > maxfd ? i : maxfd;
        }

        for (int i = 1; i <= maxfd; i++)
        {
            // 用于通信的文件描述符 cfd 的读缓冲区有数据了 , 也就是有客户端发消息过来了
            if (fds[i].revents & POLLIN)
            {
                int cfd = fds[i].fd;

                memset(buf, 0, sizeof buf);
                int len = read(cfd, buf, sizeof buf);
                printf("客户端 : %s\n", buf);

                if (len > 0)
                {
                    write(cfd, str, strlen(str) + 1);
                }
                else if (len == 0)
                {
                    // 客户端已经关闭了连接
                    printf("客户端已经关闭了连接...\n");
                    close(cfd);
                    fds[i].fd = -1;
                }
                else
                {
                    perror("read");
                    exit(0);
                }
            }
        }
    }

    close(lfd);

    return 0;
    }

3.epoll

1.概念

epoll 全称是 eventpoll,是 Linux Io多路复用技术的一个实现之一。epoll 可以说是 selectpoll 的升级版,相比于这两个,epoll 的底层数据结构是红黑树,实现起来更为高效。

  • 对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的。
  • select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
  • select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题;
  • 程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测
  • 使用epoll没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制;

IO多路复用 的文件描述符数量庞大、IO流量频繁的时候,一般不太适合使用select()poll() ,这种情况下 select()poll() 表现较差,推荐使用 epoll()

2.函数原型

epoll 的三个 API 函数:

#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_create() 是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合:

int epoll_create(int size);
  • size :在 Linux 2.6.8 版本后这个参数是被忽略的,只需要指定一个大于 0 0 0 的数值即可;

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

苏州健雄职业技术学院人工智能学院学生在“火焰杯”软件测试开发选拔赛总决赛获奖

3月22日&#xff0c;第三届“火焰杯”软件测试开发选拔赛颁奖仪式在人工智能学院D2-102机房举行&#xff0c;软件工程20级学生和软件测试社团全体社团成员参加本次活动。本次活动由测吧&#xff08;北京&#xff09;科技有限公司项目总监王雪冬担任颁奖嘉宾&#xff0c;并为同学…

使用Windows平台的Hyper-V虚拟机安装CentOS7的详细过程

Hyper-V虚拟机安装CentOS7 前言常见Linux系统CentOSUbuntuDebianKaliFedoraArch LinuxMintManjaroopenSUSE Hyper-V开启Hyper-V打开Hyper-V Hyper-V的使用新建虚拟机开始安装分区配置开始安装 修改yum源为阿里源 前言 作为一名开发者&#xff0c;就服务器而言&#xff0c;接触最…

SpringMVC 报文信息转换器(HttpMessageConverter)

文章目录 描述1、RequestBody2、RequestEntity3、ResponseBody4、SpringMVC处理json5、SpringMVC处理ajax6、RestController注解7、ResponseEntity 描述 HttpMessageConverter&#xff0c;报文信息转换器&#xff0c;将请求报文转换为Java对象&#xff0c;或将Java对象转换为响…

Linux:firewalld防火墙-(实验2)-IP伪装与端口转发(4)

实验环境 本章实验环境要建立在上一章之上&#xff0c;ip等都是继承上一章&#xff0c;完全在上一章之下的操作 Linux&#xff1a;firewalld防火墙-小环境实验&#xff08;3&#xff09;-CSDN博客https://blog.csdn.net/w14768855/article/details/133996151?spm1001.2014.3…

动态链接函数(dlopen/dlsym/dlclose)使用总结

一、简介 动态链接函数操作&#xff08;显式运行时链接&#xff09;主要包含头文件dlfcn.h&#xff08;/usr/include/dlfcn.h&#xff09;&#xff0c;涉及的常用的函数主要有dlopen&#xff0c;dlysm&#xff0c;dlclose。主要作用是从动态库中加载函数到程序中使用&#xff…

shell脚本条件语句(极其粗糙版)

条件测试操作和条件测试语句&#xff1a; $?:条件判断&#xff0c;失败或者成功&#xff0c;真或者假&#xff0c;true false shell脚本中&#xff1a;0为真&#xff0c;true 执行成功&#xff1b;其他所有的非0 都是假&#xff0c; false&#xff0c;执行失败 条件测试的命…

如何禁止员工上班玩游戏

如何禁止员工上班玩游戏 在这个游戏盛行的年代里&#xff0c;不少游戏玩家会玩到忘我的状态&#xff0c;也有不少员工在上班的时候也要玩上两把&#xff0c;但是公司是雇佣员工的时间是来工作的&#xff0c;出现这种情况很显然是对公司不利的&#xff0c;会严重影响工作效率和…

Python print 函数用法总结

Python3 print 函数用法总结 一、print()函数概述 print() 方法用于打印输出&#xff0c;是python中最常见的一个函数。 print([*objects][,seq ][,end\n][,filesys.stdout]) 参数的具体含义如下&#xff1a; objects --表示输出的对象。输出多个对象时&#xff0c;需要用…

MySQL---表的增查改删(CRUD基础)

文章目录 什么是CRUD&#xff1f;新增&#xff08;Create&#xff09;单行数据 全列插入多行数据 指定列插入 查询&#xff08;Retrieve&#xff09;全列查询指定列查询查询字段为表达式起别名查询去重查询排序查询条件查询分页查询 修改&#xff08;Update&#xff09;删除&…

新手如何备考学习PMP?

一、PMP学习7步走攻略 1、熟悉考试大纲&#xff1a; PMP考试大纲是备考的基础&#xff0c;考生需要详细熟悉考试大纲&#xff0c;了解各个知识领域的重点和难点。 2、制定学习计划&#xff1a; 根据考试大纲和个人情况&#xff0c;制定学习计划&#xff0c;合理分配学习时间…

stm32移植u8g2库内存不足解决办法

1.现象 跟着视频教程移植完u8g2库到stm32f103c8t6后&#xff0c;进行编译&#xff0c;报了100多个空间不足的问题&#xff0c;如下图。 ..\Output\Output.axf: Error: L6406E: No space in execution regions with .ANY selector matching u8g2_fonts.o(.constdata). ..\Outp…

蓝天远控2023(VIP会员版)

蓝天远控2023&#xff08;VIP会员版&#xff09;下载地址&#xff1a;https://user.qzone.qq.com/512526231/main

【逆向】导入表注入

练手的exe链接 链接&#xff1a;https://pan.baidu.com/s/1_87QNHaZYlfY_5uwIRePUQ?pwd6gds 提取码&#xff1a;6gds 原理&#xff1a; 在动态链接库一章提到DllMain&#xff0c;这里再回顾一次 当dll被加载进4GB空间时&#xff0c;会调用一次DllMain&#xff08;入口方法&…

在家制作电子相册一定需要的一款工具

​随着科技的发展&#xff0c;越来越多的人开始喜欢在家制作电子相册&#xff0c;记录自己的生活点滴。那么&#xff0c;如何在家制作电子相册呢&#xff1f; 一款好的工具是必不可少的。可以使用这款工具&#xff0c;轻松上手----FLBOOK在线制作电子杂志平台 1.打开FLBOOK在线…

手撕Vue-实现事件相关指令

经过上一篇文章的学习&#xff0c;实现了界面驱动数据更新&#xff0c;接下来实现一下其它相关的指令&#xff0c;比如事件相关的指令&#xff0c;v-on 这个指令的使用频率还是很高的&#xff0c;所以我们先来实现这个指令。 v-on 的作用是什么&#xff0c;是不是可以给某一个元…

SpringCloud复习:(3)LoadBalancerInterceptor

使用Ribbon时&#xff0c;execute方法会由RibbonLoadBalancerClient类来实现 它会调用重载的execute方法 getLoadBalancer默认会返回ZoneAwareLoadBalancer&#xff08;基类是BaseLoadBalancer).此处调用的getServer方法就会根据负载均衡策略选择适当的服务器来为下一步的htt…

电脑缺失duilib.dll是什么情况,有什么办法可以解决duilib.dll缺失

在使用电脑时突然提示duilib.dll丢失&#xff0c;这是什么情况&#xff1f;有什么办法可以解决这个问题呢&#xff1f;今天就给大家分享几种解决duilib.dll丢失的办法&#xff0c;解决duilib.dll丢失的办法其实还是非常的简单的&#xff0c;来看看有什么办法可以解决duilib.dll…

使用Spring Boot限制在一分钟内某个IP只能访问10次

有些时候&#xff0c;为了防止我们上线的网站被攻击&#xff0c;或者被刷取流量&#xff0c;我们会对某一个ip进行限制处理&#xff0c;这篇文章&#xff0c;我们将通过Spring Boot编写一个小案例&#xff0c;来实现在一分钟内同一个IP只能访问10次&#xff0c;当然具体数值&am…

【Happy!1024】C++智能指针

&#x1f34e; 博客主页&#xff1a;&#x1f319;披星戴月的贾维斯 &#x1f34e; 欢迎关注&#xff1a;&#x1f44d;点赞&#x1f343;收藏&#x1f525;留言 &#x1f347;系列专栏&#xff1a;&#x1f319; C/C专栏 &#x1f319;请不要相信胜利就像山坡上的蒲公英一样唾…

【计算摄像学】博资考

TOC 本博客将覆盖的内容&#xff1a; 数字相机&#xff08;Digital Camera&#xff09; 小孔成像模型&#xff08;pinhole imaging model&#xff09; 如下图所示&#xff0c;物体反射的光线射向四面八方&#xff0c;直接使用传感器无法直接得到物体形貌。 小孔成像模型在传…