linux编程:多路转接复用IO之select模型
多路转接IO概念
描述的是转接IO,就是对大量的描述符的IO状态进行监控,能够让程序知道哪个描述符IO是就绪的,可以进行IO操作,直接让程序针对就绪描述符进行IO操作,提高效率(更多用于网络通信服务器)。
IO就绪状态
- 可读状态:套接字接收缓冲区中,数据大小大于低水位标记(默认-1字节)
- 可写状态:套接字发送缓冲区中,剩余空间大小大于低水位标记(默认-1字节)
- 异常状态
select模型
- 实现思路就是在内核中轮询遍历所有监控的描述符,判断IO就绪状态
- 接口:int select(int nfds,fd_set *readfds,fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 操作流程
- 程序员定义描述监控集合(可读,可写,异常),在fd_set结构体中有个数组当做位图使用,默认的比特位个数1024
- 初始化集合后,将对应监控状态的描述符添加到对应集合中
- 将集合中的数据拷贝到内核中进行遍历判断监控,等到监控超时或者有描述符就绪了,监控返回,在返回前将集合中没有就绪的描述符移除
- 监控返回后,select返回0表示没有就绪的描述符,监控超时了,大于0表示有描述符就绪;这时候只需要判断哪个描述符还在集合中,哪个描述符就是就绪的描述符,对应就绪了哪个状态;然后对描述符进行对应操作。
多路转接IO应用场景
- 当用户处理多个描述符时(一般用于网络套接字),一般要使用I/O复用
- 当一个tcp服务器既要处理监听套接字,又要处理通信套接字时,一般要使用I/O复用
- 如果一个服务器既要处理tcp协议的请求,又要处理udp协议的请求,一般要使用I/O复用
- 如果一个服务器要处理多个服务或多个协议的请求,一般要使用I/O复用
select的应用场景
select不一定非要大量描述符状态监控,单个描述符更适合,因为select有超时控制。所以针对单个描述符如果需要进行超时等待控制也可以使用select
select优缺点分析
优点: select遵循POSIX标准,跨平台移植性良好
缺点:
- select监控描述符数量有最大上限(取决于位图的比特位个数默认1024个–由_FD_SETSIZE宏控制)
- select监控每次都需要向集合中重新添加描述符,并且每次都需要将集合拷贝到内核(因为select每次返回时都会移除未就绪的描述符)
- select监控原理是在内核中进行轮训遍历判断监控,性能会随着描述符的增多而降低
- select返回后,依然需要完成一次集合遍历才能获取到就绪的描述符进行操作(select都是通过集合进行操作)
select编程
- 这里将其应用在tcp服务器中,让tcp服务器可以同时处理多个客户端的请求,并实现多次通信。
- 具体过程如下
- 先创建一个描述符的监控集合,清空它,并将监听描述符添加进集合,此时监听描述符最大
- 然后对集合中就绪状态的描述符进行遍历,如果集合中的监听描述符就绪了就创建新的通信套接字,并将新的通信套接字也放入集合中进行监控
- 此时集合中最大的描述符是通信描述符,然后继续遍历,等轮询到集合中的通信描述符时,再进行tcp服务器和客户端的通信
- 此后每当有新的客户端要与服务器通信时,都会将新创建的通信套接字放入集合中,进行监控,并修改集合中描述符的最大值,使得集合中所有描述符全部能够被遍历到,然后每次再对集合中处于就绪状态的描述符进行操作
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>//地址结构
#include<arpa/inet.h>//字节序转换接口
#include<sys/socket.h>//套接字接口
#include<sys/select.h>
#define MAX_LISTEN_NUM 5
int main()
{
//1.创建套接字
int sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd<0){
perror("socket error");
return -1;
}
//2.绑定地址信息
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(9000);
addr.sin_addr.s_addr=inet_addr("0.0.0.0");
socklen_t len=sizeof(addr);
int ret = bind(sockfd,(struct sockaddr*)&addr,len);
if(ret<0){
perror("bind error");
return -1;
}
//3.开始监听
ret =listen(sockfd,MAX_LISTEN_NUM);
if(ret<0){
perror("listen error");
return -1;
}
fd_set set;//监控集合
FD_ZERO(&set);//清空集合
FD_SET(sockfd,&set);//将监听描述符添加到监控集合中,监控是否有新连接到来
int max_fd=sockfd;
//4.获取新连接
while(1){
struct timeval tv;
tv.tv_sec=3;
tv.tv_usec=0;
fd_set rfds;
memcpy(&rfds,&set,sizeof(fd_set));
//select每次返回会移除集合中未就绪的描述符,因此需要用set备份,用于下一次监控描述符时用
int ret=select(max_fd+1,&rfds,NULL,NULL,&tv);
if(ret<0){
perror("select error");
continue;
}else if(ret==0){
printf("No descriptor ready,wait timeout\n");
continue;
}
for(int i=0;i<=max_fd;i++){
if(FD_ISSET(i,&rfds)){
//第i个描述符就绪
if(i==sockfd){//监听描述符就绪
struct sockaddr_in cliaddr;
int newfd=accept(sockfd,(struct sockaddr*)&cliaddr,&len);
FD_SET(newfd,&set);//将新连接添加监控
max_fd=newfd>max_fd?newfd:max_fd;
printf("new connect \n");
}else{//通信描述符就绪
char buf[1024]={0};
//接收数据
ret=recv(i,buf,1023,0);
if(ret<=0){
perror("recv error");
close(i);
FD_CLR(i,&set);//解除监控
continue;
}
printf("client say:%s\n",buf);
//发送数据
ret=send(i,buf,strlen(buf),0);
if(ret<0){
perror("send error");
close(i);
FD_CLR(i,&set);//解除监控
continue;
}
}
}
}
//5.使用新连接与客户端通信(收发数据)
}
//6.关闭套接字
close(sockfd);
return 0;
}
通信演示
TCP服务端
TCP客户端