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

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

目录

一对一服务器中的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 1int 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需要加1if(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.hqwc.cn/news/157565.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端css介绍

CSS介绍 CSS&#xff08;Cascading Style Sheet&#xff0c;层叠样式表)定义如何显示HTML元素。 当浏览器读到一个样式表&#xff0c;它就会按照这个样式表来对文档进行格式化&#xff08;渲染&#xff09;。 CSS语法 CSS实例 每个CSS样式由两个组成部分&#xff1a;选择器和…

使用Qt Installer Framework将自己的程序打包成安装包程序

使用Qt Installer Framework将自己的程序打包成安装包程序 制作安装包程序就是将自己的程序打包成一个可执行的exe&#xff0c;双击之后进行安装。 1. 在制作安装包程序之前需要安装qt官方提供的安装包制作工具Qt Installer Framework 去qt官方网址&#xff0c;下载对应的 Q…

HNU-计算机网络-实验1-应用协议与数据包分析实验(Wireshark)

计算机网络 课程基础实验一 应用协议与数据包分析实验(Wireshark) 计科210X 甘晴void 202108010XXX 一、实验目的&#xff1a; 通过本实验&#xff0c;熟练掌握Wireshark的操作和使用&#xff0c;学习对HTTP协议进行分析。 二、实验内容 2.1 HTTP 协议简介 HTTP 是超文本…

89 柱状图中最大的矩形

柱状图中最大的矩形 类似接雨水&#xff08;反过来&#xff0c;相当于找接雨水最少的一段&#xff09;题解1 暴力搜索&#xff08;超时&#xff09; O ( N 2 ) O(N^2) O(N2)另一种 题解2 单调栈【重点学习】常数优化 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的…

python调用飞书机器人发送文件

当前飞书webhook机器人还不支持发送文件类型的群消息&#xff0c;可以申请创建一个机器人应用来实现群发送文件消息。 创建机器人后&#xff0c;需要开通一系列权限&#xff0c;然后发布。由管理员审核通过后&#xff0c;才可使用。 包括如下的权限&#xff0c;可以获取群的c…

1. PPT高效初始化设置

1. PPT高效初始化设置 软件安装&#xff1a;Office 2019 主题和颜色 颜色可以在白天与黑夜切换&#xff0c;护眼 切换成了黑色 撤回次数 撤回次数太少&#xff0c;只有20次怎么办 自动保存 有时忘记保存就突然关闭&#xff0c;很需要一个自动保存功能 图片压缩 图…

OpenCV 笔记(4):图像的算术运算、逻辑运算

Part11. 图像的算术运算 图像的本质是一个矩阵&#xff0c;所以可以对它进行一些常见的算术运算&#xff0c;例如加、减、乘、除、平方根、对数、绝对值等等。除此之外&#xff0c;还可以对图像进行逻辑运算和几何变换。 我们先从简单的图像加、减、逻辑运算开始介绍。后续会有…

使用Objective-C和ASIHTTPRequest库进行Douban电影分析

概述 Douban是一个提供图书、音乐、电影等文化内容的社交网站&#xff0c;它的电影频道包含了大量的电影信息和用户评价。本文将介绍如何使用Objective-C语言和ASIHTTPRequest库进行Douban电影分析&#xff0c;包括如何获取电影数据、如何解析JSON格式的数据、如何使用代理IP技…

轻松搭建Nextcloud私有云盘并实现远程访问【内网穿透】

文章目录 摘要1. 环境搭建2. 测试局域网访问3. 内网穿透3.1 ubuntu本地安装cpolar3.2 创建隧道3.3 测试公网访问 4 配置固定http公网地址4.1 保留一个二级子域名4.1 配置固定二级子域名4.3 测试访问公网固定二级子域名 摘要 Nextcloud,它是ownCloud的一个分支,是一个文件共享服…

搜维尔科技:scalefit生物力学人体工学软件分析!

人体工程学分析 21加载参数和头像显示 识别(隐藏的)健康风险 根据DGUV交通灯进行生物反馈(DIN/ISO) 实时应力分析 三维空间可视化 静态/动态肩载 用左/右赋值加载输入 腰椎间盘压缩计算 距离和定时器显示 带有运动跟踪的化身/视频叠加 外骨骼与CAD工作站仿真 CSV原始…

不只保护隐私的防窥膜,还是屏幕的小铠甲

电脑防窥膜这种东西确实很实用&#xff0c;尤其是那些经常在公共场所用笔记本的朋友&#xff0c;更是需要这张贴膜的保护&#xff0c;不过虽然现在市面上这种防窥膜种类繁多&#xff0c;但是产品质量良莠不齐。有些防窥膜虽然有防窥效果&#xff0c;但透光率下降太多了&#xf…

在Qt中List View和List Widget的区别是什么,以及如何使用它们

2023年10月29日&#xff0c;周日晚上 目录 List View和List Widget的区别 如何使用QListView 如何使用QListWidget List View和List Widget的区别 在Qt中&#xff0c;QListView 和 QListWidget 是用于显示列表数据的两个常用控件&#xff0c;它们有一些区别和特点。 1. 数…