【Linux】多路IO复用技术①——select详解如何使用select模型在本地主机实现简易的一对多服务器(附图解与代码实现)

news2024/9/27 5:44:47

这一篇的篇幅可能有点长,但真心希望大家能够静下心来看完,相信一定会有不小的收获。那么话不多说,我们这就开始啦!!!

目录

一对一服务器中的BUG

如何实现简易的一对多服务器

实现简易一对多服务器的大体步骤

每个步骤的具体流程

1.网络初始化

2.启动监听,等待socket相关事件

多路IO复用技术之select模型

3.监听到相关事件 , 辨别是服务器socket还是客户端socket并进行处理

本地主机实现简易一对多服务器的程序实现

程序构成

结果演示

select模型的优缺点


一对一服务器中的BUG

我们先来看一段我之前写的程序——功能:实现一对一的单进程服务器

大家可以直接看该程序的while循环,来看一下该部分有哪些BUG

/*************************************************************************
        > File Name: nan_server.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月20日 星期五 13时59分10秒
 ************************************************************************/
 
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<string.h>
 
//定义一个开关,用于决定服务器是否开启,默认为开启状态
#define SERVER_SWITCH 1
 
 
int main()
{
    //1.分别定义服务端与客户端的网络信息结构体
    struct sockaddr_in server_addr , client_addr;
    bzero(&server_addr , sizeof(server_addr));
    bzero(&client_addr , sizeof(client_addr));
    //定义一个读写缓冲区与一个存放客户端IP的缓冲区
    char rw_buffer[1500];
    char client_IP[16];
    bzero(rw_buffer , sizeof(rw_buffer));
    bzero(client_IP , sizeof(client_IP));
    //2.对服务端网络信息结构体进行初始化
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(6060);
    //server_addr.sin_addr.s_addr = inet_addr("192.0.0.1");
    server_addr.sin_addr.s_addr = inet_addr("本地主机IPV4地址");
    //3.创建套接字,该套接字起到监听与传输信息的作用
    int server_sockfd = socket(AF_INET , SOCK_STREAM , 0);
    if(server_sockfd == -1)
    {
        perror("server socket call failed!\n");
        exit(-1);
    }
    //4.将IP地址与端口号绑定到监听套接字上
    int bind_result = bind(server_sockfd , (struct sockaddr*)&server_addr , sizeof(server_addr));
    if(bind_result == -1)
    {
        perror("server bind call failed!\n");
        exit(-1);
    }
    printf("server wait connect!\n");//日志打印,可帮助理解程序执行逻辑
    //5.监听是否有TCP链接
    int backlog = 128;
    listen(server_sockfd , backlog);
    socklen_t addrlen;
    int client_sockfd;
    while(SERVER_SWITCH)
    {
        printf("已进入循环!\n");//日志打印,可帮助理解程序执行逻辑
        addrlen = sizeof(client_addr);
        //6.如果接收成功,返回对应的文件描述符,并执行以下程序
        if( (client_sockfd = accept(server_sockfd , (struct sockaddr*)&client_addr , &addrlen)) > 0)
        {   
            printf("accept call success!\n");
            //将网络信息结构体中的大端序IP转为字符串IP并放到读写缓冲区中
            inet_ntop(AF_INET , &(client_addr.sin_addr.s_addr) , client_IP , sizeof(client_IP));
            printf("client_IP = %s\n" , client_IP);//日志打印,帮助检测是否写入IP地址
            sprintf(rw_buffer , "Hello , %s , welcome connect nan_server\n" , client_IP);
            printf("读写缓冲区中内容为 %s\n" , rw_buffer);//日志打印,帮助检测是否写入要发送的数据
            //将读写缓冲区中的内容发送到服务端的套接字中,由套接字向客户端发送数据
            send(client_sockfd , rw_buffer , sizeof(rw_buffer) , MSG_NOSIGNAL);
            //清空读写缓冲区与存放IP的缓冲区,以供下一次使用
            bzero(rw_buffer , sizeof(rw_buffer));                                      
            bzero(client_IP , sizeof(client_IP));
            //读取客户端发来的数据
            recv(client_sockfd , rw_buffer , sizeof(rw_buffer) , 0);
            printf("client_message : %s\n" , rw_buffer);
            bzero(rw_buffer , sizeof(rw_buffer));
        }
        else if(client_sockfd == -1)
        {
            perror("accept call failed!\n");
            continue;
        }
    }
    close(server_sockfd);
}

如果大家看不出来的话,再给大家一点提示:这些BUG都和阻塞与socket缓冲区有关

现在来公布一下答案吧,这个服务器的BUG在于——

  1. 如果一直没有客户端向服务器发起TCP链接请求,socket缓冲区中没有表示链接请求的标志数据SYN可读取,服务器的程序就会一直阻塞在accept函数那里,无法执行其他程序,整个服务器一直处于阻塞等待状态
  2. recv函数的调用,由于服务器使用阻塞状态的recv函数,如果客户端迟迟不发送信息,socket缓冲区就会一直为空,整个服务器就会一直阻塞等待该客户端发送数据,无法处理其他客户端的链接请求

那么我们要怎么处理这些BUG呢?

其实看完上面的BUG,大家或多或少都能明白这两个BUG的本质——其实无非就是,由于socket缓冲区中没有数据,可accept函数和recv函数却一直在等待数据的读取,导致服务器一直阻塞等待数据的读取

要想解决这两个BUG,其实也很简单——我们需要一个类似信号的功能,当socket缓冲区中有数据,触发相关事件,需要服务器进行处理时,我们再去进行处理,socket缓冲区中没有数据时,服务器不要一直阻塞等待数据

如何实现简易的一对多服务器

实现简易一对多服务器的大体步骤

  1. 网络初始化
  2. 启动监听,等待socket相关事件(也就是查看socket缓冲区中是否有数据需要处理)
  3. 监听到相关事件辨别是服务器socket还是客户端socket并进行处理

PS:处理socket相关事件这里要分两类——

  1. 服务器套接字接收到客户端的TCP链接请求,调用accept函数
  2. 服务器中为该客户端创建的套接字接收到客户端发来的信息,调用recv函数

每个步骤的具体流程

1.网络初始化

网络初始化这个步骤就不多做介绍了,就是简单的初始化网络信息结构体、创建服务器套接字、绑定套接字等操作

不会的同学可以去看一下我之前写的这篇博客,相关函数与使用方法都在里面:【Linux】如何在本地主机实现简易的一对一服务器(附图解与代码实现)icon-default.png?t=N7T8http://t.csdnimg.cn/thQS8

2.启动监听,等待socket相关事件

既然是要实现一对多的服务器,就代表着我们要为每个链接的客户端分别创建对应的套接字,同时这也就意味着我们自然也要去监听这些套接字

多路IO复用技术这么名字听起来很高大上,其实本质上就是一个IO事件监听技术,也就是一次性可以监听多个socket,来判断这些socket中是否有数据需要处理并反馈给服务器。所以在这个过程中我们也就要用到该技术中的一种——select

接下来我们来讲解一下select的原理实现与相关函数

多路IO复用技术之select模型

原理实现

select中有一个集合,叫做监听集合,我们可以将需要监听的套接字放入套接字文件描述符表中,由该集合负责帮我们监听该文件描述符表中这些套接字文件描述符对应的这些套接字的缓冲区中是否有数据需要处理

这个监听集合的大小为1024(固定大小,不可改),但需要注意的是,虽然这个集合的大小为1024,但实际能帮我们监听的客户端套接字只有1020个,因为前1-3个分别用于监听标准输入、标准输出和标准出错,第四个用于存放服务器套接字

可能只用文字描述过于抽象了,大家可以看下面的这个图来帮助理解

通过这个监听集合,我们就可以实现对多个socket的同时监听

这时候可能就有同学好奇了,监听?他咋监听啊?是什么高级手段吗?

其实这个监听真的是一种很朴实无华的方法,就是遍历,一次次的遍历,当监听集合完成一遍遍历,发现有套接字处于就绪状态,也就是某些套接字的缓冲区中有数据需要处理时,他就会传出一个就绪码(处于就绪状态的套接字数量)和一个就绪集合(就绪的套接字的位码置1,未就绪的套接字的位码置0)

还是画个图来帮助大家理解

以上就是select模型的相关原理了,接下来我们来讲一讲相关函数

以下函数的头文件都是#include<sys/select.h>

介绍一下一会会用到的变量:

  • fd_set set ; //创建监听集合
  • int sockfd ; //套接字文件描述符
  • int max_fd ; //套接字文件描述符表中的描述符个数
  • struct timeval *timeout ; //时间结构体,在这里表示工作模式——1.阻塞、2.非阻塞、3.定时阻塞(非阻塞与定时阻塞需要设置该结构体)

PS : timeout中有两个成员,一个表示秒(timeout.tv_sec),一个表示微秒(timeout.tv_usec)

struct timeval
{
__time_t  tv_sec;        /* Seconds. */
__suseconds_t  tv_usec;  /* Microseconds. */
};
  • timeout = NULL 就表示阻塞监听
  • timeout.tv_sec = 0 、timeout.tv_sec = 0 就表示非阻塞监听
  • timeout.tv_sec = 4、timeout.tv_usec = 30 就表示阻塞4秒30微秒,之后不阻塞
函数功能返回值
FD_ZERO(&set);初始化监听集合,将所有位的位码都初始化为0因为它们只是对文件描述符集合进行操作,而不是返回任何值,所以他们的返回值都是void
FD_SET(sockfd , &set);将set集合中与sockfd对应位的位码设置为1因为它们只是对文件描述符集合进行操作,而不是返回任何值,所以他们的返回值都是void
FD_CLR(sockfd , &set);将set集合中与sockfd对应位的位码设置为0因为它们只是对文件描述符集合进行操作,而不是返回任何值,所以他们的返回值都是void
FD_ISSET(sockfd , &set);获取set集合中与sockfd对应位的位码0或1
int select(max_fd, 是否监听读事件 , 是否监听写事件 , 是否监听错误事件 , timeout);监听我们要求的文件描述符的状态变化情况,并通过返回值告知(PS:想监听对应时间就传入&set,不想就传NULL)返回处于就绪状态的套接字数量

3.监听到相关事件 , 辨别是服务器socket还是客户端socket并进行处理

上面的就绪集合还需要我们自己去遍历,从而找到哪些套接字需要进行数据处理,并辨别是服务器套接字还是客户端套接字

如果是服务器套接字,就说明是有客户端向服务器发送了TCP链接请求,有以下步骤需要执行:

  1. 调用accept函数进行链接并获取与该客户端对应的套接字文件描述符
  2. 将其放入套接字文件描述符存放数组(由于该服务器为单进程,所以我们需要建立一个数组来存放这些客户端套接字文件描述符)(这个地方不太懂的话别着急,结合代码来看一定会让你豁然开朗)
  3. 将该套接字文件描述符放入套接字文件描述符表中,来让监听集合对该套接字进行监听

如果是客户端套接字,就说明是客户端向服务器发送了数据,有以下步骤需要执行:

  1. 调用recv函数读取套接字缓冲区中的数据
  2. 根据客户端发来的数据进行相应处理

具体过程如下图所示:

在了解了实现一对多服务器的具体流程后,我们来看一下具体如何使用select模型用程序实现一对多服务器

本地主机实现简易一对多服务器的程序实现

程序构成

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

  • func_2th_parcel.h:定义二次包裹的函数名
  • func_2th_parcel.c:对网络初始化相关的函数进行二次包裹
  • select_server.c:使用select模型的服务器程序
  • 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: select_server.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月25日 星期三 18时53分30秒
 ************************************************************************/

#include <func_2th_parcel.h>

int main(void)
{
    //一、进行网络初始化
    int server_sockfd;//服务器套接字文件描述符
    int max_fd;//套接字文件描述符表中的描述符个数
    int client_sockfd_array[1020];//存放客户端套接字文件描述符的数组  
    int client_sockfd;//客户端套接字文件描述符
    int ready_num = 0;//获取处于就绪状态的套接字数目
    char rw_buffer[1500];//读写缓冲区
    int flag;
    int recv_len = 0;//客户端发来的数据长度
    memset(client_sockfd_array , -1 , sizeof(client_sockfd_array));//将套接字数组每一位都置为-1,方便后面查找就绪套接字
    bzero(rw_buffer , sizeof(rw_buffer));
    fd_set listen_set , ready_set;//监听集合,就绪集合
    server_sockfd = SOCKET_NET_CREATE("192.168.79.128" , 6060);//初始化服务器套接字
    max_fd = server_sockfd;//初始化最大套接字数目
    //初始化监听集合,将server_sockfd设置为监听套接字
    FD_ZERO(&listen_set);
    FD_SET(server_sockfd , &listen_set);
    printf("select_server wait TCP connect\n");
    //二、启动监听,等待socket相关事件
    while(1)
    {
        ready_set = listen_set;
        //阻塞等待socket相关事件
        if((ready_num = select(max_fd + 1 , &ready_set , NULL , NULL , NULL)) == -1)
        {
            perror("select call failed\n");
            exit(0);
        }

        //三、监听到相关事件 , 辨别是服务器socket还是客户端socket并进行处理
        while(ready_num)
        {
            //辨别就绪,如果是服务端套接字就绪
            if(FD_ISSET(server_sockfd , &ready_set))
            {
                client_sockfd = SERVER_ACCEPTING(server_sockfd);//与客户端建立TCP链接
                FD_SET(client_sockfd , &listen_set);//将该套接字放入监听集合中
                //如果max_fd小于客户端套接字返回的描述符,说明这个新的客户端套接字放到了最后一位,max_fd需要加1
                if(max_fd < client_sockfd)
                {
                    max_fd = max_fd + 1;
                }
                for(int i = 0 ; i < 1020 ; i++)
                {
                    //将该客户端套接字,放到数组中有空缺的地方
                    if(client_sockfd_array[i] == -1)
                    {
                        client_sockfd_array[i] = client_sockfd;
                        break;
                    }
                }
                //将就绪集合中服务器套接字这一位的位码置为0,因为如果ready_num > 1,不做该处理服务器会一直认为是客户端发送了TCP链接请求,从而导致错误处理
                FD_CLR(server_sockfd , &ready_set);
            }
            //如果是客户端套接字就绪
            else
            {
                for(int i = 0 ; i < 1020 ; i++)
                {
                    //检测存放的客户端套接字是否处于就绪状态
                    if(client_sockfd_array[i] != -1)
                    {
                        //如果该套接字处于就绪状态
                        if(FD_ISSET(client_sockfd_array[i] , &ready_set))
                        {
                            recv_len = RECV(client_sockfd_array[i] , rw_buffer , sizeof(rw_buffer) , 0);//获取数据长度
                            printf("客户端%d 发来数据 : %s , 现在进行处理\n" , client_sockfd_array[i] , rw_buffer);
                            flag = 0;
                        }
                        //如果recv_len = 0,就说明与客户端套接字对应的客户端退出了,将对应客户端套接字移出套接字存储数组与监听集合
                        if(recv_len == 0)
                        {
                            printf("客户端%d 已下线\n" , client_sockfd_array[i]); 
                            FD_CLR(client_sockfd_array[i] , &ready_set);
                            client_sockfd_array[i] = -1;
                            break;
                        }
                        //进行业务处理:小写字母转大写字母
                        while(recv_len > flag)
                        {
                            rw_buffer[flag] = toupper(rw_buffer[flag]);
                            flag++;
                        }
                        SEND(client_sockfd_array[i] , rw_buffer , recv_len , MSG_NOSIGNAL);
                        printf("已向客户端%d 发送处理后的数据 : %s\n" , client_sockfd_array[i] , rw_buffer);
                        bzero(rw_buffer , sizeof(rw_buffer));
                        recv_len = 0;
                        FD_CLR(client_sockfd_array[i] , &ready_set);
                        break;
                    }
                }
            }
            ready_num--;
        }
    }
    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;
}

结果演示

在了解了如何用程序实现使用select模型的一对多服务器后,我们来了解一下select模型的优缺点

select模型的优缺点

优点:

  1. 由于select模型出现的非常早,所以他的兼容性很强,便于跨平台,各个平台语言都支持
  2. select可以实现微妙级别的定时阻塞,可满足某些对时间精度要求较高的场景
  3. 适宜于局域网开发

缺点:

  1. 监听的数量太少,最多只能同时监听1024个套接字,不适宜于广域网开发
  2. select的监听是通过一次次的遍历实现的,非常消耗CPU,会导致服务器吞吐能力会非常差
  3. 随着select的持续使用,会产生大量的拷贝开销和挂载开销(注释①)
  4. select没有对监听集合进行传入传出分离,用户需要自己定义传入集合(监听集合)和传出集合(就绪集合)
  5. select只传出了处于就绪状态的套接字数量,而没有告诉用户是哪些套接字处于就绪状态,需要用户自己一个一个的去遍历查找
  6. select模型只能监听读事件、写事件、异常事件,但其实socket的相关事件是有很多的,选择性比较少
  7. select模型只能批量监听。以读事件举例,这个函数就导致select模型要么监听所有套接字的读事件,要么完全不监听所有套接字的读事件,无法灵活地为每个套接字监听不同的事件(不明白这个地方的可以看下前面讲解的select函数的构成)

注释①:

第3个缺点需要为大家讲解一下原因

我们知道,select模型中的监听集合可以实现对套接字的监听,我们也讲过所谓的监听其实就是通过遍历实现的,但遍历这件事其实不是监听集合去做的,接下来为大家讲解一下具体流程,如下所示:

  1. 我们在用户层定义了一个变量 : fd_set set ; 
  2. 系统会将这个用户层的监听集合拷贝到内核层(这就是拷贝开销的第一部分)
  3. 系统会将内核层的该监听集合中监听的套接字放入IO设备等待队列(这就是挂载开销的第一部分),由IO设备等待队列来进行一次又一次的遍历来判断那些套接字中有数据需要处理
  4. 当IO设备等待队列发现有套接字处于就绪状态时,会传出就绪集合到内核层(挂载开销的第二部分)
  5. 系统通过select模型将该就绪集合由内核层拷贝到用户层(拷贝开销的第二部分),供用户使用

如果看文字看不明白的话,大家可以看一下下面的图

有人一看了就会说,这不也没啥吗,不就拷贝一下挂载一下嘛,有啥大的开销啊?

这样看上去,可能开销确实没什么,但要注意的是,每当有新的套接字放入监听集合中时,系统是不会将新的套接字拷贝到内核层并放入IO设备等待队列,而是将整个新的监听集合全部拷贝到内核层,并将监听的套接字一个一个重新挂载到IO设备等待队列,举个例子

原先这个监听集合里监听100个套接字,后面又加了6个新的套接字来让监听集合监听,系统会直接把这整个新的监听集合拷贝到内核层,然后把这106个套接字重新挂载到IO设备等待队列,如果有相同的直接覆盖掉。一旦用的轮数越多,监听的套接字个数越多,这个开销的增长就不好估计了

所以,这就是为什么select模型会有上面所述的第3个缺点

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

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

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

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

相关文章

web前端常见开发工具汇总 你用过几个?

搬运旗下公众号的内容~ 目录 1.记事本 2.Visual studio code 3.Hbuilder 4.Eclipse 5.Webstorm 6.Notepad 随着信息时代的不断进步&#xff0c;互联网在人类社会中所占的地位愈发举足轻重。大大小小的网站&#xff0c;构成了如今光怪陆离的网络社会。我们知道&#xff0c…

上海中优城市万豪酒店推出全新国际IP童趣主题房,独特住宿体验中国首秀

2023年10月30日&#xff0c;中国上海 – 近日&#xff0c;上海中优城市万豪酒店正式推出由全球品牌娱乐公司孩之宝官方授权打造的小马宝莉和变形金刚主题客房&#xff0c;以创意客房、新奇体验和丰富礼遇&#xff0c;为童游家庭或年轻的动漫迷们开启沉浸式入住之旅&#xff0c;…

直击电商商城内核!一站式解决方案

作为一家深耕电商运营多年的软件开发公司&#xff0c;我们拥有先进的轻量级电商中台系统&#xff0c;且100%开源&#xff0c;包含B2C、B2B2C、S2B2C、O2O和社区团购等多种商业模式&#xff0c;无论在技术、业务架构、功能、设计还是售后支持上&#xff0c;我们都秉承着追求极致…

函数栈帧的创建和销毁(以C语言代码为例,汇编代码的角度分析)

函数栈帧的创建和销毁[以C语言代码为例,汇编代码的角度分析] 一.前言1.几个问题2.几个说明 二.相关寄存器和汇编命令的简要说明三.从汇编代码调试的角度逐步分析函数栈帧的创建于销毁1.函数栈区的知识:2.逐步调试分析1.保存__tmainCRTStartup这个函数栈帧的栈底地址2.正式进入m…

【Linux】centOS7安装配置及Linux的常用命令---超详细

一&#xff0c;centOS 1.1 centOS的概念 CentOS&#xff08;Community Enterprise Operating System&#xff09;是一个由社区支持的企业级操作系统&#xff0c;它是以Red Hat Enterprise Linux&#xff08;RHEL&#xff09;源代码为基础构建的。CentOS提供了一个稳定、可靠且…

解决计算机msvcp120.dll文件丢失的5种方法,亲测有效

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“msvcp120.dll丢失”。这个错误提示可能会给我们带来很大的困扰&#xff0c;影响我们的正常使用。本文将详细介绍msvcp120.dll丢失的原因、解决方法以及预防措施&#xff0c;帮助大家更好地…

python读取shadow文件脚本

python读取shadow文件脚本 该脚本源代码为kali中执行的源代码 from dataclasses import fieldswith open(/etc/shadow,r)as file:for line in file:listline.split(:)if list[1]!"*" and list[1]!"!" and list[1]!"!*":paslist[1].split($)sal…

基于FMCW雷达的人体复杂动作识别

基于FMCW雷达的人体复杂动作识别

【Python算法】算法练习(一)

❤️博客主页&#xff1a; iknow181 &#x1f525;系列专栏&#xff1a; Python、JavaSE、JavaWeb、CCNP &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐评论✍ 目录 1、输出n以内的质数 2、求n以内最大的m个质数的和&#xff0c;并打印这些质数以及它们的和 方法一 方法二…

合肥中科深谷嵌入式项目实战——人工智能与机械臂(三)

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000 python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 作者&#xff1a;爱吃饼干的小白鼠。Python领域优质创作者&#xff0c;2022年度博客新星top100入围&#xff0c;荣获多家平台专家称号。…

postgresql14-sql基础(一)

初始化 在“开始”中运行pgadmin4&#xff0c;输入密码&#xff0c;连接实例&#xff0c;创建测试数据库&#xff1a;hrdb 开启查询工具面板。 https://download.csdn.net/download/hy19930118/88419281 SELECT version() AS "pg版本"简单查询 SELECT first_na…

性能测试支持结果抽样分析,执行机新增运行状态和CPU监控,MeterSphere开源持续测试平台v2.10.8 LTS版本发布

2023年10月30日&#xff0c;MeterSphere一站式开源持续测试平台正式发布v2.10.8 LTS版本。自2023年5月发布v2.10 LTS版本后&#xff0c;MeterSphere开源项目组坚持发布小版本&#xff0c;持续进行问题的修复更新&#xff0c;并针对部分功能进行优化。 本次发布的MeterSphere v…

C/C++ 作业题笔记

请计算下列代码运行结果 解析:

获取服务器或域控登录日志工具

SharpUserIP 功能简介 服务器登陆日志 (需管理员权限) 在域控或远程提取登录日志&#xff0c;快速获取域用户对应的 IP 地址 项目地址&#xff1a;https://github.com/lele8/SharpUserIP 使用说明 ___ _ _ _ ___ ___/ __| |_ __ _ _ _ _ _…

docker解决oracle中ORA-12514和ORA-03113问题

ORA-12514&#xff1a;TNS&#xff1a;监听程序当前无法识别连接描述符中请求的服务; 1、进入docke容器 docker exec -it 容器id bash 2、找到并修改listener.ora文件 查看oracle的位置 命令&#xff1a;cat /etc/profile 这是listener.ora文件的位置 /home/oracle/app/oracl…

输入输出缓冲区的作用,c++io流介绍,转换运算符(operator+类型)

目录 引入 输入输出缓冲区的作用 流 cio流 介绍 为什么要把流进行面向对象的设计呢? 原理 使用的注意点 istream类型对象转换为逻辑条件判断值 引入 转换运算符 文件io 介绍 示例 注意点 说明 利用字节流特性 字符串io 介绍 istringstream ostringstrea…

七、【图像添加水印】

文章目录 一、制作水印1、先新建图层2、新建文字图层并调好水印文字的大小与角度3、添加图层样式4、添加定义图案 二、添加水印 一、制作水印 1、先新建图层 2、新建文字图层并调好水印文字的大小与角度 3、添加图层样式 1、打开“描边” 2、选择“颜色” 4、添加定义图案 二…

【AI视野·今日NLP 自然语言处理论文速览 第六十期】Mon, 23 Oct 2023

AI视野今日CS.NLP 自然语言处理论文速览 Mon, 23 Oct 2023 (showing first 100 of 108 entries) Totally 100 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers Exploring Linguistic Probes for Morphological Generalization Autho…

电商数据采集抓取封装数据、淘宝、天猫、京东等平台商品详情API接口参数详解

电商数据采集抓取数据、淘宝、天猫、京东等平台的电商数据抓取&#xff0c;网页爬虫、采集网站数据、网页数据采集软件、python爬虫、HTM网页提取、APP数据抓包、APP数据采集、一站式网站采集技术、BI数据的数据分析、数据标注等成为大数据发展中的热门技术关键词。那么电商数据…

Android开发笔记(三)—Activity篇

活动组件Activity 启动和结束生命周期启动模式信息传递Intent显式Intent隐式Intent 向下一个Activity发送数据向上一个Activity返回数据 附加信息利用资源文件配置字符串利用元数据传递配置信息给应用页面注册快捷方式 启动和结束 &#xff08;1&#xff09;从当前页面跳到新页…