IO多路转接select
//为什么要写多进程/多线程的并发服务器?
在进行套接字通信的时候有一些阻塞函数:accept,read、recv,write、send
需要不停的检测客户端链接,需要不停的调用accept,需要占用一个线程或进程进行检测
发送数据:write 、send 如果写缓冲区被写满,阻塞→需要一个单独的线程去处理接收数据:read/recv,对方不给当前终端发送数据,当前终端阻塞->需要单独线程处理数据接收
总结套接字通信过程中有大量的阻塞操作,需要多个线程处理阻塞任务;
细节分析:
accept为啥会阻塞;
使用accept读了监听文件描述符的读缓冲区,检测过程是阻塞的
read,recv为什么会阻塞:
使用这两个检测了通信文件描述符的读缓冲区,阻塞检测
write send 为啥会
检测了通信的写缓冲,如果满了,就阻塞
iO多路转接:委托内核帮助我们检测文件描述符的状态,内核检测完毕之后会给用户一个回馈,用户通过内核反馈,就指定的哪些文件描述符有状态变化,有针对的对这些文件描述符的状态处理
在处理有状态变化的文件描述符的时候:
1.检测到有新链接,建立新链接,调用accept函数(不会阻塞)
2.内核检测到通信的文件描述符读缓冲区有数据==>对端给当前终端发送数据(需要接收read,recv,不会阻塞)
3.内核检测到通信的文件描述符写缓冲区可以写可以使用 write()/send()发送数据 ==> 不阻塞
io多路转接函数掌握两个
select重要
poll
epoll重要
select:
select是如何实现IO转接的?
select是一个跨平台的函数,linux和window平台都可以使用
我们调用这个函数,该函数会调用相对应的平台的系统api,委托操作系统执行某些操作
3.在调用select的时候需要我们通过参数的形式将要检测到文件描述符的集合传递内核中去,内核根据这个集合进行的文件描述符的状态检测
读集合:要检测的这一系列的文件描述符的读缓冲区(监听的加通信的)写集合:委托内核检测集合中的文件描述符对应的写缓冲区是否可写异常集合:检测集合中文件描述符进行读写操作的时候是否有异常 4.内核根据传递的集合中的数据,对文件描述符表进行线性检测,如果有满足条件的文件描述符,内核会通知调用者满足条件:内核如何通知调用者:内核会将用户传递给内核的读写异常集合进行修改,得到新的数据;
5.最终用户得到的信息:
1.知道委托内核检测的数据集合一共有多少文件描述符的变化2.通过检测内核传出的读写异常集合可以判断出是哪一个文件描述符发生了变化
检测是线性的
select函数:
nfds:
readfds:
writefds:
exceptfds:
timeout:
返回值:
0:
检测完成之后,满足条件的文件描述符总个数(三个集合的和
=0:
没有检测到满足条件的文件描述符,超时间到了,强制函数返回
-1:
函数调用失败了
fd_set类型操作函数
内核会把符合条件的读缓冲区的文件描述符返回,因为这是
传入传出出来,有数据的返回读集合,内核会修改集合
程序流程:
1.创监听的套接字
int lfd =socket();
2.绑定
bind();
3.设置监听
listen()
4.初始化要检测的文件描述
fd_set reads;。创建FD_ZERO(lfd,&reads);//清零FD_SET(lfd,&reads);//函数检测集合当中的文件描述符的状态
5.使用select函数检测集合中的文件描述符的状态
![image.png](https://flowus.cn/preview/2fd74c7d-f964-4f97-b19e-34c9ad344d63)![image.png](https://flowus.cn/preview/899fe710-6326-4d5b-808b-53b81e3e5357)![image.png](https://flowus.cn/preview/87165043-1aac-4c16-8a6f-b7a57d1d56a4)
结果集合并不一定和监视集合具有相同的长度。在调用
select
函数时,监视集合是用户空间传递给内核的,它包含了需要监视的所有套接字的文件描述符。而当
select
函数返回时,返回的结果集合可能会比监视集合小,因为只会返回发生了事件的套接字的文件描述符。也就是说,结果集合中只包含了发生了事件的套接字的文件描述符,而不包含未发生事件的套接字。因此,结果集合的长度可能比监视集合小,甚至为空。这取决于在监视集合中哪些套接字发生了事件,以及它们的数量。
#include<sys/select.h>
18 using namespace std;
19 int main(){
20
21 // 1.创建监听的套接字
22 int lfd=socket(AF_INET,SOCK_STREAM,0);
23 if(lfd==-1){
24 perror("socket");
25 exit(0);
26 }
27 //2.绑定
28 struct sockaddr_in addr;
29 addr.sin_family =AF_INET; //IPV4
30 addr.sin_port=htons(8989);//端口,转换大端
31 addr.sin_addr.s_addr=INADDR_ANY;//ip地址为0
32 int ret =bind(lfd,(struct sockaddr*)&addr,sizeof(addr));//绑定
33 if(ret==-1){//如果绑定失败
34 perror("bind");
35 exit(0);
36 }// 3. 设置监听
38 ret =listen(lfd,128);
39 if(ret==-1){
40
41 perror("listen");
42 exit(0);
43 }
44 //4.初始化检测集合、
45 fd_set reads,temp;
46 FD_ZERO(&reads);
47 FD_SET(lfd,&reads);
48 int nfds=lfd;//5.委托内核不停检测集合中的文件描述符
50 while(1){
51 temp=reads;
52 int num=select(nfds+1,&temp,NULL,NULL,NULL);//最后一个是> 什么时候解除阻塞,NULL那就是不解除阻塞,知道检测出有状态变化;,内核传出的都是变化过的集合
53 cout<<num<<endl;
54 for(int i=lfd;i<=nfds;i++){
55 if(i==lfd&&FD_ISSET(lfd,&temp))、{`temp` 中只会保留发生了状态变化的文件描述符,而没有发生变化的文件描述符状态会被清除,不再包含在 `temp` 中。
56 //建立新连接,这调用绝对不阻塞
57 int cfd=accept(lfd,NULL,NULL);
58 //cfd 添加到检测的原始集合
59 FD_SET(cfd,&reads);
60 nfds=nfds<cfd?cfd:nfds;//取最大的哪一个
61 }
62 else{
63 if(FD_ISSET(i,&temp)){
64 //接受数据
65 char buf[1024];
66 memset(buf,0,sizeof(buf));
67 int len=recv(i,buf,sizeof(buf),0);
68 if(len==0){
69 cout<<"客户端断开连接"<<endl;
70 //将i从原始集合中删除,下次不检查了
71 FD_CLR(i,&reads);close(i);
73 }
74 else if(len>0){
75 cout<<"recv data :"<<buf<<endl;
76 send(i,buf,len,0);
77 }
78 else{
79 perror("recv");
80 break;
81 }
82 }
83 }
84 }
86 }
88 close(lfd);89 return 0;90 }
你提出了一个很好的观察。你是对的,将监听套接字 lfd
添加到 reads
集合中并不意味着一定会有连接请求到来。但是在这个特定的代码中,我们可以进行一些假设:
-
这段代码运行在一个循环中,并且
select
函数被用于阻塞等待可读事件。 -
在循环中,每次调用
select
函数都会检查reads
集合中的所有文件描述符,包括监听套接字lfd
。 -
当有连接请求到来时,监听套接字
lfd
会变为可读状态,select
函数会返回,并且FD_ISSET(lfd, &temp)
条件将会为真,触发建立新连接的代码。
总的来说,在这个代码的设计中,监听套接字 lfd
被添加到了待检测的文件描述符集合中,因此每次调用 select
函数时都会检查其状态。虽然 lfd
被添加到了集合中并不保证一定有连接请求到来,但是在有连接请求到来时,通过检查 lfd
的状态,可以及时响应并处理连接请求。
poll(从select到epoll的过度),
不能跨平台,select有1024最大的并发上线,poll是没有的,select和poll都是线性的,而poll是动态数组,内部是链表,效率不高
epoll(重要)
如果内存是1g,epoll就可以支持10万连接
不能跨平台,只能在linux
支持大并发
IO多路转接函数:
select:
支持跨平台,第一个参数文件描述符在windows无意义为0,linux下的有意义集合最大文件描述符+1检测方式:线性数组,越多越低多次数据拷贝,用户态内核态,内核区拷贝回来传出信息的量:多少文件描述符发生变化了→返回值到底是谁发生变化,需要使用者检测
poll:
不支持跨平台,只支持linux,检测的链接数和内存正相关检测方式:线性链表,动态扩容多次数据拷贝,用户区到内核区,内核区拷贝回来
epoll:
也不支持跨平台,只支持linux检测方式:红黑树;不影响委托epoll检测的文件描述符集合用户区和内核区使用的是同一块内存;(共享内存)花销少传出的信息的量:有多少文件描述符的发送变化了→返回值可以精确到知道哪个文件描述符发生了变化
epoll使用步骤:
epoll:是一个模型,树状模型,需要调用3个函数
1.创建树状模型,没有节点
2。将要检测的节点添加到epoll树上
文件描述符的类型监听的通信的从检查的事件上说:读。写。异常
3.开始委托内核对树上的节点进行检测
4.处理的过程
监听的:建立新连接通信的:接受和发送数据
函数:
头文件#include
//创建一个epoll红黑模型树
int epoll_create(int size);
//size值多少无所谓,>0即可,没有实际意义
返回值:
成功:有效的文件描述符,红黑树的根节点;通过这个文件描述符就可以访问创建的实例失败:-1;
对节点的操作函数
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()函数返回值,找到epoll的实例
events :传出参数,记录了发生状态变化的所有文件描述符信息
是结构体数组的地址
maxevents:events数组的容量
timeout :超时的时长,和poll一样,
如果为-1,委托内核检测epoll 树上的文件描述符的状态,如果没有状态变化,该函数一直阻塞有满足的就返回如果为0 epoll_wait 调用之后立马返回>0 :规定时间内没有检测到,函数强制解除阻塞
返回值:
成功:有多少文件描述发生的状态变化
基于epoll的tcp服务器的伪代码
int main(){
//创建监听的套接字int lfd =socket();//2. 绑定bind();//3.设置监听listen();//4.创建epoll模型int epfd=epoll_create(size);//5.将需要检测的文件描述符添加到epoll模型中struct epoll_event ev;ev.events=EPOLLIN;ev.data.fd=lfd;epoll_ctl(epfd,epoll_ctl_add,lfd,&ev);//将lfd挂在树上,事件为add添加//6.开始检测struct epoll_event events[1024];while(1){//不停的检测,不停的调用epoll_waitint num=epoll_wait(epfd,events,1024,-1);//处理num个有状态变化的文件描述符,会放到events结构体数组的前num个for(int i=0;i<num;i++){int curfd=events[i].data.fd;if(curfd==lfd){int cfd =accept(lfd,NULL,NULL);//cfd 添加到epoll模型上ev.data.fd=cfd;ev.events=EPOLLIN;epoll_ctl(epfd,epoll_ctl_add,cfd, &ev);}else{//处理通信char buf[1024];int len =recv (curfd,buf,sizeof(buf),-1);if(len==0){cout<<"客户端断开链接"<<endl;//从epoll树上删除检测节点epoll_ctl(epfd,epoll_ctl_del,curfd,NULL);close(curfd);}else if(len>0){send();}else{perror("recv");}}}}}
执行代码:
18 #include<sys/epoll.h>19 using namespace std; 20 int main(){21 22 // 1.创建监听的套接字23 int lfd=socket(AF_INET,SOCK_STREAM,0);24 if(lfd==-1){25 perror("socket");26 exit(0);27 }28 //2.绑定29 struct sockaddr_in addr;30 addr.sin_family =AF_INET; //IPV431 addr.sin_port=htons(8989);//端口,转换大端32 addr.sin_addr.s_addr=INADDR_ANY;//ip地址为033 int ret =bind(lfd,(struct sockaddr*)&addr,sizeof(addr));//绑定34 if(ret==-1){//如果绑定失败35 perror("bind");36 exit(0);37 }38 // 3. 设置监听 39 ret =listen(lfd,128);40 if(ret==-1){
42 perror("listen");43 exit(0);44 }45 46 //4.创建epoll模型47 int epfd=epoll_create(100);48 if(epfd==-1){49 perror("epoll_create");50 exit(0);51 }52 //5.将要检测的节点添加到epoll模型中53 struct epoll_event ev;54 ev.data.fd=lfd;//将lfd添加到结构体数组中初始化55 ev.events=EPOLLIN;//检测lfd的读缓冲区56 ret=epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);57 if(ret==-1){58 perror("epoll_ctl");59 exit(0);60 }
61 //6.不停的委托内核检测epoll模型中的文件描述符的状态 62 struct epoll_event evs[1024];//evs 是传出方63 int size=sizeof(evs)/sizeof(evs[0]);64 while(1){65 int num=epoll_wait(epfd,evs,size,-1);//-1 就是一直阻塞检测66 cout<<num<<endl;67 //遍历evs68 for(int i=0;i<num;i++){69 //取出数组元素的文件描述符70 int curfd=evs[i].data.fd;71 if(lfd==curfd){72 int cfd=accept(curfd,NULL,NULL);73 //添加结构体数组到检测的原始集合74 ev.events=EPOLLIN;75 ev.data.fd=cfd;76 epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);77 }78 else{79 //通信
80 char buf[1024];81 memset(buf,0,sizeof(buf));82 int len=recv(curfd,buf,sizeof(buf),0);83 if(len==0){84 cout<<"客户端断开链接"<<endl;85 epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL);//从ep oll树删除此节点86 close(curfd);87 }88 else if(len>0){89 cout<<"recv data :"<<buf<<endl;90 send(curfd,buf,len,0);91 }92 else{93 perror("recv ");94 exit(0);95 }96 }97 98 }99
100 }
101 return 0;
102 }
epoll的工作模式(水平模式,边缘模式)
LT :水平工作模式,默认的工作模式,上面的就是,缺省就是默认的意思
![image.png](https://flowus.cn/preview/7d020cf0-3afe-417d-8cac-df1eafea2b86)阻塞非阻塞套接字都支持阻塞指定的接受和发送数据的状态:(缓冲区有关)read、recvwrite、send
场景:
客户端给服务器端发送数据,每次发送1k的数据,服务器使用epoll检测,检测到读缓冲区中有数据,每次接收500字节
发送的快,接收慢
水平模式特点:
读事件:接收端接收的数据少,接收一次数据包接收不完,还有500字节在读缓冲区这种场景下,只要是epoll_wait 检测函数到读缓冲有数据,就会通知用户一次不官有没有读完,只要有数据就通知通知就是epoll_wait ()函数返回,我们就可以处理传出参数中的文件描述符的状态写事件:检测写缓冲是否可用,只有是可写epoll_wait()就会返回
演示:
第一个num是监听的文件描述符发生变化的时候取的1
客户端发送helloworld,nihaoshijie的,服务器端用了好几次,是因为接收的读缓冲区很小,但是epoll检测就返回
客户端收到的是服务器多次返回的结果
ET :边沿触发模式,效率高,通知的次数少(只支持非阻塞模式)
边沿模式,需要我们手动模式,如何设置边沿模式
场景:
客户端给服务器发送数据,每次发送1k的数据,服务器使用epo11检测(边沿模式),检测到读缓冲区中有数据,每次接收500字节发送的快接收的慢
特点:检测的次数变少了,效率高,满足条件的新状态才会通知
读事件:接收端每次收到一条新的数据,epoll_wait()会通知一次通知的这一次内,数据没有接收完,没有将缓冲区的数据全部读出,epoll_wait()也不会通知只通知一次,不管你有没有读完一条数据来了,没读完,就通知一次,第二条新的才会通知写事件:检测写缓冲区是否可用(是否有容量)检测到写缓冲区的可用时候通知一次,再次检测到就不通知了写缓冲区原来是不可用(满了),后来缓冲区可用(不满),epo11_wait()检测到之后通知一次(唯一)
如何设置边沿模式?
在ev.events=EPOLLIN | EPOLLET4![image.png](https://flowus.cn/preview/980218a8-ffca-4fb6-bb88-ffd66a77fcbb)导致缓冲区的数据越积越多,直到新数据来处理**循环队列,先进先出,直到对列被写满才会发送处理,数据不能及时全部读出,无法处理客户端请求,如何解决这个问题?**既然只发生了一次,那么就得在epoll_wait的时候一次性读完方案一:接收端(服务器端)准备一个特别大的内存块,用来存储待接受的数据弊端:内存块多大是特别大,客户端的发送的数据多大不知道,内存块的上限不确定,系统不分配大内存给用户方案二:循环进行数据回收![image.png](https://flowus.cn/preview/6b2f60c9-1bb6-4c90-98fe-4ed36285953a)
如何设置文件描述符的非阻塞?
int flag =fcntl(cfd,F_GETFL)//获取
flag=flag | O_NONBLOCK; 按位或,给flag追加非阻塞
fcntl(cfd,F_SETFL,flag);//将新的属性设置文件描述符中
只需要将cfd通信文件描述符设置为非阻塞,开始通信的时候,直接while把数据通信完
在非阻塞模式下读数据遇到错误
由于是非阻塞,缓存区一直再读,如果没有数据了,非阻塞还是会继续读,然后会报错误read ,recv失败了,返回-1;
错误号 errno=EAGAIN or EWOULDBOLCK 一般情况使用EAGAIN判断就可以了