具有代表性的并发服务器端实现模型和方法:
多进程服务器:通过创建多个进程提供服务。
多路复用服务器:通过捆绑并统一管理I/O对象提供服务。✔
多线程服务器:通过生成与客户端等量的线程提供服务。
目录
1. I/O复用
2. select函数
2.1 select函数的作用
2.2 设置文件描述符
2.3 指定监视范围
2.4 设置超时
2.5 查看调用select函数后的结果
2.7 与Windows系统的区别
3. 实现I/O复用的回声服务器端
1. I/O复用
“在一个通信频道中传递多个数据(信号)的技术。”
“为了提高物理设备的效率,用最少的物理要素传递最多数据时使用的技术。”
举个例子,某个教师里有10名学生,1位老师,这10名学生都非等闲之辈,他们会不停的提问,所以学校没有办法,只能给他们每个人都配一个老师,这样这个教师就有10个老师,10个学生,但这样的话,以后每当有一个新学生进来,就要增加一个新老师,这样下去也不是办法。这时,学校来了个贼牛的老师,他一个人就可以应对所有学生的提问,而且速度很快,所以学校就把其他老师给转移到了其他班。并且,现在学生提问必须举手,老师确认学生的提问再回答问题。现在,这间教师就是以I/O复用方式运行的。
如图是I/O复用在服务端的模型。
2. select函数
#include<sys/select.h>
#include<sys/time.h>
int select(
int maxfd, //监视对象文件描述符数量
fd_set* readset, //将所有关注"是否存在待读取数据"的文件描述符注册到fd_set型
//变量,并传递其地址值
fd_set* writeset, //将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set型
//变量,并传递其地址值
fd_set* exceptset, //将所有关注"是否发生异常"的文件描述符注册到fd_set型变量,
//并传递其地址值
const struct timeval* timeout //调用select函数后,为防止进入无限阻塞状态,传递超时信息
);
成功返回>0的值,表示发生上述事件的文件描述符
超时返回0
失败返回-1
struct timaval
{
long tv_sec; //seconds
long tv_usec; //microseonds
}
2.1 select函数的作用
作用:将多个文件描述符集中到一起统一监视。获取发生监视事件的文件描述符,从而与这个文件描述符指定的套接字进行通信。
监视事件:
1.是否存在套接字接收数据(read)
2.无需阻塞传输数据的套接字有哪些(write)
3.哪些套接字有异常(except)
2.2 设置文件描述符
select函数是怎么将多个文件描述符集中到一起监视的?
答:使用fd_set数组。
fd_set数组的结构:
fd_set数组是一个位数组,即只存储0与1。0:表示当前文件描述符未被监视,1:表示当前文件描述符正在被监视。
那么fd_set数组是怎么将文件描述符集中到数组里的?又是怎么设置它的位数的?
答:通过提供的宏来完成。
fd_set数组提供如下宏:
宏 | 含义 |
FD_ZERO(fd_set* fdset); | 将fdset数组里的所有位初始化为0 |
FD_SET(int fd,fd_set* fdset); | 将文件描述符fd的信息注册到fdset数组里,即指定一个位置为此文件描述符,并将其位设置为1。 |
FD_CLR(int fd,fd_set* fdset); | 将fdset数组里的文件描述符fd的信息清除掉,即设置为0。 |
FD_ISSET(int fd,fd_set* fdset); | 判断fdset数组里有没有注册文件描述符fd的信息,即指定位置处,其位的值是否为1。有则返回true,无则返回false。 |
所以使用select函数前的第一步,就是先把要监视的文件描述符注册到fd_set数组里。
2.3 指定监视范围
select函数通过第一个参数来传递监视对象的文件描述符的数量。因为Linux里文件描述符的值是从3开始递增的,所以你只需将最大的文件描述符的值再加1传递给select的第一个参数即可。
2.4 设置超时
struct timaval
{
long tv_sec; //seconds
long tv_usec; //microseonds
}
通过select函数的最后一个参数来设置超时,因为select函数只有在有文件描述符发生变化时,才会返回,否则会一直阻塞住。
所以如果你不想阻塞住程序,那么就可以设置超时时间,传递给select的最后一个参数。
如果你想阻塞住程序,直到有文件描述符发生变化,你可以给select的最后一个参数传nullptr。
当达到超时时间而没有文件描述符改变时,select函数返回0。
2.5 查看调用select函数后的结果
select函数在调用时,除发生变化的文件描述符对应位外,会把传递的fd_set数组里的其余位全部置为0。
如图,你传入select函数的fd_set数组里,要求监视的是文件描述符fd1、fd2、fd3等,然后,select函数就会将fd0、fd1、fd2、fd3等,没有发生变化的文件描述符置为0,发生变化的文件描述符就不改变。
所以,你调用select函数后,获取到的fd_set数组里的值,位为1的都是发生变化的文件描述符,然后你根据fd_set数组是第几个参数,从而可以对它进行指定的操作,例如:你传递的fd_set数组的参数是select函数的第二个参数,那么说明,这个fd_set数组里位为1的文件描述符,此时有输入流,你就可以通过read函数来将里面的数据取出了。
2.7 与Windows系统的区别
上述都是在Linux系统下的,select函数在Linux和Windows系统上,完全相同。只是:
1.在Windows系统上,select函数的第一个参数是无意义的,只是为了保持与Linux系统的兼容性。
2.Windows系统的fd_set与Linux的有区别。如下所示是Windows的fd_set结构体,其中数组也是位数组,并且使用的宏和Linux也是一样的。
typedef struct fd_set
{
u_int fd_count; //套接字句柄数
SOCKET fd_array[FD_SETSIZE]; //保存套接字句柄
}fd_set
为什么Windows要这样?
因为:在Linux中,文件描述符是递增的,所以你注册的时候,系统可以很好的找出当前文件描述符和最后生成的文件描述符之间的关系。但是在Windows中,套接字句柄(SOCKET)的生成并非是从0开始的,值之间也没有规律,所以需要直接保存句柄的数组和记录句柄数的变量。
3. 实现I/O复用的回声服务器端
实现思路:
创建一个fdset数组,里面要存放所有要监视的文件描述符。首先把server文件描述符注册到里面,接着select监听这个server文件描述符,因为TCP的连接,也是以数据接收的形式进行的,所以当有连接请求时,select函数里的readfdset数组里注册的server文件描述符就会置为1。紧接着就处理这个连接请求,把后面每连接的一个客户端的文件描述符就注册到fdset数组里,用循环,来遍历,只要是server文件描述符为1,就处理连接请求,不是,就是客户端文件描述符,就处理读写。
代码:
#include<iostream>
#include<sys/socket.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<cstring>
#include<sys/time.h>
#include<sys/select.h>
int main()
{
int socketfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if(socketfd==-1)
{
std::cout<<"socket fail!"<<std::endl;
return 0;
}
int bReuse=true;
socklen_t reuseLen=sizeof(bReuse);
setsockopt(socketfd,SOL_SOCKET,SO_REUSEADDR,(void*)&bReuse,reuseLen);
sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
serverAddr.sin_port=htons(9130);
if(-1==bind(socketfd,(sockaddr*)&serverAddr,sizeof(serverAddr)))
{
std::cout<<"bind fail!"<<std::endl;
return 0;
}
if(-1==listen(socketfd,1))
{
std::cout<<"listen fail!"<<std::endl;
return 0;
}
fd_set fdset;
FD_ZERO(&fdset);
FD_SET(socketfd,&fdset);
timeval timeout;
timeout.tv_sec=5;
timeout.tv_usec=0;
fd_set tempset;
int fdmax=socketfd;
while(1)
{
tempset=fdset;
int result=select(fdmax+1,&tempset,0,0,&timeout);
if(result==0)
{
continue;
}
else if(result==-1)
{
std::cout<<"select fail"<<std::endl;
break;
}
else
{
for(int i=0;i<fdmax+1;++i)
{
if(FD_ISSET(i,&tempset))
{
if(i==socketfd) //服务器文件描述符发生了变化,意味着有连接请求
{
sockaddr_in clientAddr;
memset(&clientAddr,0,sizeof(clientAddr));
socklen_t clientLen=sizeof(clientAddr);
int clientfd=accept(i,(sockaddr*)&clientAddr,&clientLen);
std::cout<<"客户端IP地址:"<<inet_ntoa(clientAddr.sin_addr)<<std::endl;
if(-1==clientfd)
{
std::cout<<"accept fail!"<<std::endl;
}
else
{
FD_SET(clientfd,&fdset); //成功接收请求把客户端文件描述符信息注册到fdset里
if(fdmax<clientfd)
fdmax=clientfd;
}
}
else //客户端文件描述符发生了变化,意味着有读取
{
char buff[1024];
int readlen=read(i,buff,sizeof(buff));
if(readlen==0) //客户端EOF,断开连接不再发送数据
{
FD_CLR(i,&fdset); //把fdset里的客户端文件描述符给注销掉
close(i); //关闭客户端套接字
}
else
{
std::cout<<"客户端发来的消息:"<<buff<<std::endl;
write(i,buff,readlen);
}
}
}
}
}
}
close(socketfd);
return 0;
}
Windows系统:
要注意,其fdset不是一个数组,而是一个结构体,要用“.”或“->”运算符来取里面的套接字文件描述符数组,来进行处理。