目录
前言
主要介绍了几种server-client模型,包括多进程多线程处理客户端命令请求的高并发服务器,以及通过内核替应用程序监视满足监听条件文件的多路I/O并发服务器。
一、高并发服务器
1.多进程并发服务器
父进程负责监听服务器套接字,等待客户端的连接请求。一旦客户端向服务器请求连接,fork子进程用于处理客户端的命令请求。
使用多进程并发服务器时要考虑以下几点:
1.父进程最大文件描述个数(父进程中需要关闭accept返回的新已连接文件描述符,子进程要关闭监听描述符);
2.系统内创建进程个数(与内存大小相关);
3.进程创建过多是否降低整体服务性能(进程调度);
4.当「子进程」退出时,实际上内核里还会保留该进程的一些信息,也是会占用内存的,如果不做好“回收”工作,就会变成僵尸进程,随着僵尸进程越多,会慢慢耗尽我们的系统资源。
/* server.c */
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<ctype.h>
#define SERV_PORT 1666//定义服务器端口号,端口号<65535,小于1000的端口号通常给系统使用
int main()
{
int lfd;//连接描述符,用于绑定监听套接字
int confd;//已连接描述符
char client_IP[20];
pid_t pid;//多进程并行服务器,子进程号
struct sockaddr_in sever_addr,client_addr;//服务器套接字结构,客户端套接字结构
lfd=socket(AF_INET,SOCK_STREAM,0);//新建一个套接字 IPV4 TCP协议
sever_addr.sin_family=AF_INET;//使用IPV4
sever_addr.sin_port=htons(SERV_PORT);//设置端口号
sever_addr.sin_addr.s_addr=htonl(INADDR_ANY);//网络地址为INADDR_ANY
//INADDR_ANY这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址
//这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址
int ret1=bind(lfd,(struct sockaddr*)&sever_addr,sizeof(sever_addr));//将连接描述符与套接字(IP地址:端口号)绑在一起
if(ret1<0)
{
perror("bind error:");
exit(1);
}
int ret2=listen(lfd,128);//设定该服务器最多允许处于待连接状态的客户端数
if(ret2<0)
{
perror("listen error:");
exit(1);
}
while(1)
{
int client_addr_len=sizeof(client_addr);
printf("Waiting for client....\n");
confd=accept(lfd,(struct sockaddr*) &client_addr,&client_addr_len);//阻塞等待客户端的连接请求
pid=fork();//fork子进程用于处理客户端的命令请求
if(pid<0)
{
perror("fork error:");
exit(1);
}
else if(pid==0)
{
close(lfd);//子进程关闭监听描述符
while(1)
{
char buf[100];
int n=read(confd,buf,sizeof(buf));//读取客户端发来数据
for(int i=0;i<n;i++)
{
buf[i]=toupper(buf[i]);//小写字母转换成大写
}
write(confd,buf,n);//将转换后的数据写回客户端
printf("The client IP is %s in port %d\n",
inet_ntop(AF_INET,&client_addr.sin_addr,client_IP,sizeof(client_IP)),
ntohs(client_addr.sin_port));
}
close(confd);//子进程结束记得关闭已连接描述符
}
1.多线程并发服务器
父进程负责监听服务器套接字,等待客户端的连接请求。一旦客户端向服务器请求连接,pthread_create子线程用于处理客户端的命令请求。
在使用线程模型开发服务器时需考虑以下问题:
1.调整进程内最大文件描述符上限。
2.线程如有共享数据,考虑线程同步,采用互斥锁。
3.服务于客户端线程退出时,退出处理。(退出值,分离态)。
4.系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU。
/* server.c */
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<pthread.h>
#define SERV_PORT 6666//定义服务器端口号,端口号<65535,小于1000的端口号通常给系统使用
int lfd;//连接描述符
int confd;//已连接描述符
/*线程回调函数*/
void* handler(void* client)
{
pthread_detach(pthread_self());//分离线程
//接收客户端发来的字符串,将字符大写转小写后写回客户端
while(1)
{
char buf[20];
char client_ip[20];
struct sockaddr_in* Client=(struct sockaddr_in*)client;
printf("The client IP %s from port %d\n",inet_ntop(AF_INET,&Client->sin_addr.s_addr,client_ip,sizeof(client_ip)),ntohs(Client->sin_port));
int n=read(confd,buf,sizeof(buf));
for(int i=0;i<n;i++)
{
buf[i]=toupper(buf[i]);
}
write(confd,buf,n);
}
close(confd);//关闭已连接描述符
}
int main()
{
pthread_t thread;
struct sockaddr_in client_addr;//客户端套接字结构
struct sockaddr_in sever_addr;//初始化服务器套接字结构
sever_addr.sin_family=AF_INET;//使用IPV4
sever_addr.sin_port=htons(SERV_PORT);//设置端口号
sever_addr.sin_addr.s_addr=htonl(INADDR_ANY);//网络地址为INADDR_ANY
//这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址
//这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址
lfd=socket(AF_INET,SOCK_STREAM,0);//新建一个监听文件描述符 IPV4 TCP协议
/*将监听描述符与服务器套接字(IP地址:端口号)绑在一起*/
int ret3=bind(lfd,(struct sockaddr*)&sever_addr,sizeof(sever_addr)); if(ret3<0)
{
perror("bind error:");
exit(1);
}
listen(lfd,128);//设定该服务器最多允许处于待连接状态的客户端数
while(1)
{
int client_addr_len=sizeof(client_addr);
confd=accept(lfd,(struct sockaddr*) &client_addr,&client_addr_len);//阻塞等待客户端的连接请求
printf("Connect sucessful!!!\n");
if(confd<0)
{
perror("accept error:");
exit(1);
}
//与客户端建立连接后,创建同步线程处理客户端请求命令
int ret1=pthread_create(&thread,NULL,handler,(void*)&client_addr);//创建一个线程
if(ret1<0)
{
perror("pthread_create:");
exit(1);
}
}
close(lfd);//主线线程结束,关闭监听描述符
return 0;
}
二、多路I/O转接服务器
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种:select 、poll(此处不介绍,意义不大)、epoll
1.select(支持跨平台使用)
为方便理解先给定三个变量:
fd_set preset;//监听集合
fd_set aftset;//满足监听条件集合
int client[FD_SETSIZE];//用于存放监听描述符集合,用于遍历获得满足条件描述符
1.1 实现方法
1.服务器的监听描述符加入到监听集合preset中,并将监听描述符加入数组client中。每当客户端与服务器建立连接时,均将已连接文件描述符加入到监听集合preset中,并将已连接文件描述符加入数组client中。
2.通过select函数监听集合preset中是否存在满足监听条件的文件描述符,如果存在,则获得满足监听条件集合aftset。
3.通过遍历文件描述符数组client,来判断其中的个文件描述符是否存在于满足监听条件集合aftset中。如果存在,则说明此时该描述符产生了事件请求。(读事件,写时间,错误事件)则服务器开始处理相应事件。
1.2 注意事项
1.select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。
2.解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。
3.监听集合和满足监听条件的集合是共用一个集合,因此每次都要将原有集合保存。
缺:在监听描述符集合中存在大量的监听描述符,但是在获得满足监听条件描述符集合中,我们只能获得满足条件的监听描述符个数,不知道具体是谁,因此要维护一个数组,来遍历获得满足监听条件的具体描述符。
1.3基础API
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数(监听文件描述符也是读数据类型)
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况:
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* s */
long tv_usec; /* ms */
};
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
/* server.c */
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<ctype.h>
#define SERVER_PORT 6666
int main()
{
struct sockaddr_in server_addr;//创建服务器sockaddr_in结构
int listenfd,connfd;//创建监听描述符,已连接描述符
struct sockaddr_in client_addr;//创建客户端sockaddr_in结构
listenfd=socket(AF_INET,SOCK_STREAM,0);//创建监听文件描述符
//配置服务器套接字结构
bzero(&server_addr,sizeof(server_addr));//清空服务器套接字结构
server_addr.sin_family=AF_INET; //IPv4
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);//配置服务器IP地址
server_addr.sin_port=htons(SERVER_PORT);//配置服务器端口号
//将服务器的套接字与监听描述符绑定
int ret1=bind(listenfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(ret1<0)
{
perror("bind error:");
exit(1);
}
listen(listenfd,100);//设定服务器最多监听的待连接客户端请求
int nready;//记录满足监听条件的描述符个数
int client[FD_SETSIZE];//用于存放监听描述符集合,用于遍历获得满足条件描述符
int maxfd=listenfd;//获得当前监听描述符集合最大值
int maxi=-1;//cient数组当前使用最大下标
int i;
for(i=0;i<FD_SETSIZE;i++)//先将监听描述符存放数组全部先初始化为-1
client[i]=-1;
//由于select函数的描述符集合是一个传入传出参数,因此在select前后会发生改变
//监听集合和满足监听条件的集合是一个集合,因此每次都要将原有集合保存
fd_set preset;//监听集合
fd_set aftset;//满足监听条件集合
FD_ZERO(&preset);//清零描述符集合
FD_SET(listenfd,&preset);//将监听描述符加入描述符集合
for(; ;)//相当于无限循环
{
aftset=preset;//初始化满足监听条件集合
//获取监听集合中满足监听条件的描述符个数,更新监听条件满足集合
//【监听读描述符集合,阻塞监控】
nready=select(maxfd+1,&aftset,NULL,NULL,NULL);
if(nready<0)
{
perror("slect error:");
exit(1);
}
//判断监听描述符是否在满足监听条件集合中(即判断此时是否有客户端连接请求)
else if(FD_ISSET(listenfd,&aftset))
{
//由于已连接客户端的结构长度是传出参数,所以要定义个变量存起来
int client_addr_len=sizeof(client_addr);
//获得与客户端的已连接描述符
connfd=accept(listenfd,(struct sockaddr*)&client_addr,&client_addr_len);
if(connfd<0)
{
perror("accept error:");
exit(1);
}
char buf[20];
printf("Receive from %s at port %d\n",
inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,buf,sizeof(buf)),
ntohs(client_addr.sin_port));//输出已连接的客户端IP地址和端口号
printf("nready is %d\n",nready);//获取满足监听条件描述符个数
//用client数组来记录已连接描述符,用于遍历获得满足条件描述符
for(i=0;i<FD_SETSIZE;i++)
{
if(client[i]<0)
{
printf("connfd is client[%d]==%d\n",i,connfd);
client[i]=connfd;
break;
}
}
if(i==FD_SETSIZE)//当连接客户端的个数超过设置上限时,报错
{
fputs("Too many clients\n",stderr);
exit(1);
}
//将刚刚与服务器建立连接的客户端文件描述符到监听集合中
FD_SET(connfd,&preset);
if(connfd>maxfd)
maxfd=connfd;//更新select参数maxfd
if(i>maxi)
maxi=i;//更新client数组最大下标
//如果只有监听描述符满足条件,则将监听描述符加入监听集合后,
//需要返回select函数,继续监控是否有满足监听条件的描述符。
//否则向下执行已经满足监听条件的一些文件描述符。
if((--nready)==-1)
continue;
}
int getfd;//遍历描述符集合使用
char BUF[10];//用于从文件描述符中读数据
//对于监听描述符集合进行扫描,看其中哪些描述符存在于满足条件的描述符集合中
for(i=0;i<=maxi;i++)
{
sleep(1);
if((getfd=client[i])<0)
continue;
if(FD_ISSET(getfd,&aftset))//判断该描述符是否在满足条件描述符集合中
{
int n=read(getfd,BUF,sizeof(BUF));//读取客户端发来的数据
if(n!=0)
{
printf("Have read %dwords\n",n);
int j;
for(j=0;j<n;j++)
{
BUF[j]=toupper(BUF[j]);
}
write(getfd,BUF,n);//将服务器受到的小写字母转换为大写传回客户端
//【不用关闭该已连接描述符,防止后续该客户端对服务器进行写】
}
}
}
}
close(listenfd);//最后关闭监听描述符
return 0;
}
2.epoll
设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包),也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效的处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?实际上,在select或者poll事件驱动方式是这样做的。
这里有个非常明显的问题,即在某一时刻,进程收集有事件的连接时,其实这100万连接中的大部分都是没有事件发生的。因此如果每次收集事件时,都把100万连接的套接字传给操作系统(这首先是用户态内存到内核态内存的大量复制),而由操作系统内核寻找这些连接上有没有未处理的事件,将会是巨大的资源浪费,然后select和poll就是这样做的,因此它们最多只能处理几千个并发连接。参考原文链接
针对此种情况可以考虑采用下面的epoll模型:
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。目前epoll是linux大规模并发网络程序中的热门首选模型。
2.1 与select相比的优点:
1.因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合。
2.获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入满足监听条件的描述符集合。
2.2 基础API
1.创建一个epoll句柄。
当调用epoll_create时,它将在这个虚拟epoll文件系统中创建一个文件节点。它只为epoll服务。当epoll被内核初始化时(操作系统启动),它也会打开自己的内核高速缓存区,用来存放我们要监控的每个事件结构(文件描述符+事件类型)。这些套接字将以红黑树的形式存储在内核缓存中,以支持快速搜索、插入和删除。
int epoll_create(int size) //size:监听数目(建议数),返回一个句柄
2.控制epoll监控句柄上某个文件描述符上的事件:注册、修改、删除。
epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd: 为epoll_create的句柄
op: 表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd);
event: event是结构体指针,告诉内核需要监听的事件以及文件描述符
struct epoll_event {
__uint32_t events; //事件类型:EPOLLIN/EPOLLOUT/EPOLLERR
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3.等待句柄epoll所监控的文件描述符上有事件的产生。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events: 用来存内核得到事件的集合(满足监听条件描述符数组)
maxevents: 告知内核这个events元素个数的最大值,不能大于创建epoll_create()时的size,
timeout: 是超时时间
-1: 阻塞
0: 立即返回,非阻塞
>0: 指定毫秒
返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024
int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
int nready, efd, res;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
//创建一个服务器监听描述符
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
//初始化服务器套接字结构
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
//监听描述符绑定服务器套接字结构
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
//设置带连接客户端个数最大值
listen(listenfd, 20);
//将文件描述符数组元素都初始化为-1
for (i = 0; i < OPEN_MAX; i++)
client[i] = -1;
//初始化数组被使用下标最大值
maxi = -1;
//创建一个epoll句柄
efd = epoll_create(OPEN_MAX);
if (efd == -1)
perr_exit("epoll_create");
//定义事件结构,满足监听条件的事件数组
struct epoll_event tep, ep[OPEN_MAX];
//针对监听文件描述符创建事件结构,事件类型为读事件
tep.events = EPOLLIN; tep.data.fd = listenfd;
//将监听文件描述符加入到epoll句柄中
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
if (res == -1)
perr_exit("epoll_ctl");
while (1) {
//阻塞监听efd句柄中的文件描述符,将满足监听条件的文件描述符存入ep数组
nready = epoll_wait(efd, ep, OPEN_MAX, -1);
if (nready == -1)
perr_exit("epoll_wait");
//遍历满足监听条件事件数组
for (i = 0; i < nready; i++) {
//处理读事件,不是读事件则跳过
if (!(ep[i].events & EPOLLIN))
continue;
//监听描述符满足条件,于客户端建立连接
//并将已连接描述符设置为监听读事件,并加入epoll监听句柄
if (ep[i].data.fd == listenfd) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
//将于客户端已连接文件描述符存入数组client
for (j = 0; j < OPEN_MAX; j++) {
if (client[j] < 0) {
client[j] = connfd; /* save descriptor */
break;
}
}
if (j == OPEN_MAX)//当客户端连接数过多时报错
perr_exit("too many clients");
//更新客户端数组下标最大值
if (j > maxi)
maxi = j;
//并将已连接描述符设置为监听读事件,并加入epoll监听句柄
tep.events = EPOLLIN;
tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if (res == -1)
perr_exit("epoll_ctl");
//处理处监听描述符外,满足监听条件的已连接描述符
} else {
sockfd = ep[i].data.fd;
n = read(sockfd, buf, MAXLINE);
//当文件描述符中当读取的内容长度为0时
if (n == 0) {
//(1)将其从数组client中除去
for (j = 0; j <= maxi; j++) {
if (client[j] == sockfd) {
client[j] = -1;
break;
}
}
//(2)将其从epoll监听句柄中除去
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if (res == -1)
perr_exit("epoll_ctl");
//(3)关闭该文件描述符
close(sockfd);
printf("client[%d] closed connection\n", j);
} else {
//当读取的内容长度不为0时,小写转大写,写回客户端
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
writen(sockfd, buf, n);
}
}
}
}
close(listenfd);//关闭监听描述符
close(efd);//关闭epoll监听句柄
return 0;
}
3.epoll进阶
3.1 事件模型
EPOLL事件有两种模型:
Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
Level Triggered (LT) 水平触发只要有数据都会触发。
思考如下步骤:
- 假定我们已经把一个用来从管道中读取数据的文件描述符(RFD)添加到epoll描述符。
- 管道的另一端写入了2KB的数据
- 调用epoll_wait,并且它会返回RFD,说明它已经准备好读取操作
- 读取1KB的数据
- 调用epoll_wait……
3.2 ET模式
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。
epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
(1)基于非阻塞文件句柄
(2)只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
3.3 LT模式
与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度较快的poll,无论后面的数据是否被使用。这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context