什么是多路复用IO呢?

多路io:​​允许同时对多个I/O进行控制​

可能这样说还是有点抽象,stm32下面有GPIO口,如果我需要一个LED屏幕连接在32开发板上面,是不是又很多的接线,每个接线接着板子上面的对应引脚上面,既然我们接线这个引脚了,是不是首先需要让这个引脚处于使能状态,这就是开时钟,多个引脚我们是不是要开多个引脚的时钟。配置好后我们的开发板的处理器是不是会按照需要去对这些IO口进行操作。类比Linux,socket编程里面,一个服务器端可以连接多个客户端,但是仅仅socket编程只能实现1v1,我们如何实现1 v n呢,一个客户端连接多个客户端?

select函数--IO多路复用详解_多路复用IO

我们可以这样理解:

select函数--IO多路复用详解_多路复用IO_02

有这样一个表,存储所有客户的标识符,根据判断那个标识符出现动作,与之进行通信。

这就需要我们要学习的第一个函数select函数。通过select函数来检测表是否准备完成,简单的说就是检测表里面有没有事件发生,比如描述符的增加,请求通信等。如果没有动作就阻塞,有动作就返回。

所以我们先来看一下​​表​

还是以socket为例子,如果使用多路IO的话:我在服务器端,我创建了一个socket套接字,然后将服务器端socket套接字加入到表中。

当我们创建完socket后socket套接字文件描述符是3,为什么是3?因为Linux里面有三个标准文件,标准输入、标准输出、错误。他们的描述符已经占用了0、1、2所以在创建文件,文件描述符是从3开始的。

select函数--IO多路复用详解_socket_03

然后下面调用select函数,现在表里面没有异动(无动作),那么select就阻塞了,假如现在有一个客户来连接,​​那么被连接的服务器端的socket文件描述符出现了动作,也就是3发生了异动,因为有人向他发出连接请求,它是不是肯定要做些什么哇,就像你向你女神表白了,你女神肯定会有所动作吧,要么拒绝你,要么答应你​​那么select就检测到表里面发生了动作,就会解除阻塞,然后我们就可以接受连接请求,将这个客户端的套接字文件描述符加入表中:

select函数--IO多路复用详解_linux网络编程_04

如果我设计这样一个结构:

创建表;
将服务器端套接字描述符加入表;
while(1)
{
select;
接收accept连接请求
将新客户文件描述符加入到表中。
}

这样是不是表里面服务器端socket描述符出现异动select才会解除阻塞,然后接受新的客户端连接请求,再将新的客户端socket文件描述符加入表中,客户A,客户B,客户C。。。。。

是不是实现了这个结构:

select函数--IO多路复用详解_linux_05

注意:

假如现在表里面有:
0 1 2 3 4 5 6
如果现在有新的客户来连接,是不是就是7,表在判断3出现动作时候
(FD_ISSET函数),会清除别的文件描述符,也就是表里面只剩下3,所以
我们要注意保存问题。实例中会仔细说解决方法,下面只是大概思路

我们可以创建这种结构:
*********************************************************
创建表1,表2;
将服务器端套接字描述符加入表2;
while(1)
{
表1=表2 //每次将表2内容复制给表1
select;//阻塞等待表异动
FD_ISSET检测表1那个文件发生异动
if(服务器端套接字描述符发生异动)
{
接收accept连接请求
将新客户文件描述符加入到表2中。
}
}
这样我们操作在表1里面,内容保存在表2里面。

接下面我们看一下需要的函数:

表操作函数

头文件:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

1、创建表:
fd_set yyy;
yyy是表名称。
例如:fd_set lisk;
这就创建了一个名为lisk的表。

2、判断表里面那个文件描述符fd发生动作:
int FD_ISSET(int fd, fd_set *set);
参数1:要判断的文件描述符fd
参数2:表指针
返回值:当描述符fd在描述符集fdset中时返回非零值,否则,返回零。

3、将文件描述符fd添加到表中:
void FD_SET(int fd, fd_set *set);
参数1:要添加的文件描述符fd
参数2:表指针
返回指:无

4、清除表中的描述符
void FD_CLR(int fd, fd_set *set);
参数1:要清除的文件描述符fd
参数2:表指针
返回指:无

5、表清0(初始化)
void FD_ZERO(fd_set *set);
参数:要初始化的表指针;
返回值:无

select函数

头文件:
#include <sys/select.h>
函数:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout)
参数:
nfds : 就是你表中的文件描述符的最大值+1
readfds: 读事件需要结构体
writefds:写事件需要结构体
exceptfds:异常事件需要结构体
timeval:(结构体)
struct timeval
{
long tv_sec; /* second */ //秒
long tv_usec; /* microsecond */ //微秒
};
一般写NULL 无限等待,就是阻塞

timeval参数解释:
第一:若将NULL以形参传入,即不传入时间结构,就是将 select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;

第二:若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;

第三:timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值:>0返回已经准备好并包含在fd_set结构中的描述符的总数
=0时间限制过期则返回0
<0出错

接下来就是示例演示,实现一个1 v n的服务器对客户端

我们进行分段演示,完整代码在最下面:

服务器端:

首先是socket创建等步骤:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#define PORT 55555//1024~49050
#define IP "192.168.0.103"
//一下是表部分变量
int MAX,i,flag;
fd_set lisk,lock;//创建表
int comfd;//客户端描述符
int w_flag=0;

int main()
{
char r_buf[100]={0};
char w_buf[100]={0};
struct sockaddr_in addr,cli_addr;
int len=sizeof(addr);
int sockfd=socket(AF_INET,SOCK_STREAM,0);//IPV4,tcp
if(sockfd==-1)
{
perror("error socket");
return -1;

}
printf("连接套接字创建成功\n");
addr.sin_family=AF_INET;
addr.sin_port=htons(PORT);//转换
addr.sin_addr.s_addr=inet_addr(IP);//转换
int ret=bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
if(ret==-1)
{
perror("bind error");
return -1;
}
printf("绑定成功\n");
ret=listen(sockfd,3);
if(ret==0)
{
printf("监听成功\n");
}
else
{
return -1;
}

接下来是不是accept了,如果常规的直接只使用accept函数。那么只能实现1v1。我们搭配多路IO。

//一些定义在socket创建等步骤部分,这里我拿下来
/* 全局变量
int MAX,i,flag;
fd_set lisk,lock;//创建表
int comfd;//客户端描述符
int w_flag=0;
*/
FD_ZERO(&lock);//清空表
FD_SET(sockfd,&lock);//像表里面添加内容
MAX=sockfd+1;
while(1)
{
lisk=lock;//备份表
flag=select(MAX,&lisk,NULL,NULL,NULL);//调用select阻塞,产生异动后解除阻塞
if(flag<0)
{
perror("select");
}
else
{
for(i=0;i<MAX;i++)
{
if(FD_ISSET(i,&lisk))//判断那个描述符出现异动
{
if(i==sockfd)//如果是server端套接字异动说明有人请求连接
{
comfd = accept(sockfd,(struct sockaddr *)&cli_addr,&len);//接受连接
FD_SET(comfd,&lock);//新客户端描述符加入表
if(comfd>MAX-1)
{
MAX=comfd+1;
}
printf("%d 已连接\n",comfd);
}
else//如果是服务器端描述符以外的客户端描述符异动,说明客户端请求通信或者退出
{
memset(r_buf,0,sizeof(r_buf));
int col=read(i,r_buf,sizeof(r_buf));

memset(w_buf,0,sizeof(w_buf));
strcpy(w_buf,r_buf);

if(col==0)//读到的字节数为0说明不是请求通信,是退出
{
printf("%d 已经退出\n",i);
close(i);//关闭对应套接字文件
FD_CLR(i,&lock);//清除表中退出的描述符
if(i==MAX-1)//如果是最大的描述符
{
MAX=MAX-1;
}
}
else
{ //显示接收到的内容
printf("%d 说:%s\n",i, r_buf);
w_flag=1;
}

}

}//显示给所有用户看消息内容
if(w_flag==1)
{
for(int n=4;n<MAX;n++)
{
write(n,w_buf,sizeof(r_buf));
}

}
w_flag=0;
}
}


}
return 0;
}

服务器端完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#define PORT 55555//1024~49050
#define IP "192.168.0.103"
int MAX,i,flag;
fd_set lisk,lock;//创建表
int comfd;//客户端描述符
int w_flag=0;
int main()
{
char r_buf[100]={0};
char w_buf[100]={0};
struct sockaddr_in addr,cli_addr;
int len=sizeof(addr);
int sockfd=socket(AF_INET,SOCK_STREAM,0);//IPV4,tcp
if(sockfd==-1)
{
perror("error socket");
return -1;

}
printf("连接套接字创建成功\n");
addr.sin_family=AF_INET;
addr.sin_port=htons(PORT);//转换
addr.sin_addr.s_addr=inet_addr(IP);//转换
int ret=bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
if(ret==-1)
{
perror("bind error");
return -1;
}
printf("绑定成功\n");
ret=listen(sockfd,3);
if(ret==0)
{
printf("监听成功\n");
}
else
{
return -1;
}
FD_ZERO(&lock);//清空表
FD_SET(sockfd,&lock);//像表里面添加内容
MAX=sockfd+1;
while(1)
{
lisk=lock;
flag=select(MAX,&lisk,NULL,NULL,NULL);
if(flag<0)
{
perror("select");
}
else
{
for(i=0;i<MAX;i++)
{
if(FD_ISSET(i,&lisk))//成功返回非0值
{
if(i==sockfd)
{
comfd = accept(sockfd,(struct sockaddr *)&cli_addr,&len);
FD_SET(comfd,&lock);
if(comfd>MAX-1)
{
MAX=comfd+1;
}
printf("%d 已连接\n",comfd);
}
else
{
memset(r_buf,0,sizeof(r_buf));
int col=read(i,r_buf,sizeof(r_buf));

memset(w_buf,0,sizeof(w_buf));
strcpy(w_buf,r_buf);

if(col==0)
{
printf("%d 已经退出\n",i);
close(i);
FD_CLR(i,&lock);
if(i==MAX-1)
{
MAX=MAX-1;
}
}
else
{
printf("%d 说:%s\n",i, r_buf);
w_flag=1;
}

}

}
if(w_flag==1)
{
for(int n=4;n<MAX;n++)
{
write(n,w_buf,sizeof(r_buf));
}

}
w_flag=0;
}
}


}
return 0;
}


客户端:

客户端就写的比较简单了

read函数可以说一下:

读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define PORT 55555
#define IP "192.168.0.103"

void * client_fun1(void * arg);
void * client_fun2(void * arg);

int sockfd=0;
char r_buf[100]={0};
char w_buf[100]={0};
int main()
{

pthread_t pthrea_id[2]={0};
struct sockaddr_in addr;
int len=sizeof(addr);
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
perror("error socket");
return -1;

}
printf("连接套接字创建成功\n");
addr.sin_family=AF_INET;
addr.sin_port=htons(PORT);
addr.sin_addr.s_addr=inet_addr(IP);


connect(sockfd,(struct sockaddr *)&addr,len);

pthread_create(&pthrea_id[0],NULL,client_fun1,NULL);
pthread_create(&pthrea_id[1],NULL,client_fun2,NULL);

pthread_join(pthrea_id[0],NULL);
pthread_join(pthrea_id[1],NULL);

close(sockfd);
return 0;
}
void * client_fun1(void * arg)
{
while(1)
{
read(sockfd,r_buf,sizeof(r_buf));
printf("server :%s\n",r_buf);
memset(r_buf,0,sizeof(r_buf));
}
}
void * client_fun2(void * arg)
{
while(1)
{
scanf("%s", w_buf);
getchar();
write(sockfd,w_buf,sizeof(w_buf));
memset(w_buf,0,sizeof(w_buf));
}
}

运行截图

服务器端:

select函数--IO多路复用详解_linux网络编程_06

select函数--IO多路复用详解_select_07

客户端:

select函数--IO多路复用详解_多路复用IO_08

可能看起来有点懵,写的比较粗糙没有优化,主要实现就是:服务器端可以看到所有人的消息,每个用户也可以看到其他用户发送的消息,类似群聊。后续会优化一下代码。



感谢评阅,感谢指正。欢迎在评论区交流或者私信我。

_

禁止转载。