一.I/O复用
(一)基于I/O复用的服务器端
1.多进程服务器
每次服务都需要创建一个进程,需要大量的运算和内存空间
2.复用
只需创建一个进程。
3.复用技术在服务器端的应用
(二)select函数实现服务器端
- (Linux和Windows平台下均有select函数,所以具有良好移植性)
1.select函数调用过程:
2.select函数示例:(5秒内控制才没有输入,就输出 Timeout ;否则打印输入。)
#include<iostream>
#include<unistd.h>
#include<sys/time.h>
#include<sys/select.h>
using namespace std;
#define BUF_SIZE 30
int main(int argc,char *argv[]){
fd_set reads,temps;
int result,str_len;//fd_set类型是一个文件描述符集合,
char buf[BUF_SIZE];//在这里声明了reads和temps两个集合用于I/O复用。
struct timeval timeout;
FD_ZERO(&reads);//用于清空文件描述符集合。
FD_SET(0,&reads);//将标准输入文件描述符添加到集合中
while(1){
temps=reads;//在每次循环开始时,将temps设置为上一次存有文件描述符的集合reads的副本。
timeout.tv_sec=5;//设置超时时间为5秒
timeout.tv_usec=0;
result=select(1,&temps,0,0,&timeout);调用select函数来等待可读事件就绪或超时发生。
if(result==-1){//第一个参数表示要监视的最大文件描述符值加1,
cout<<"select() error"<<endl;//第二个参数是指向待检查的文件描述符集合的指针,
break; //后面的三个参数是输出参数。
}
else if(result==0)
cout<<"Time out"<<endl;
else{
if(FD_ISSET(0,&temps))//检查输入文件描述符是否就绪
{
str_len=read(0,buf,BUF_SIZE);
buf[str_len]=0;
cout<<"message from console: "<<buf<<endl;
}
}
//如果select函数返回-1,表示出现了错误,输出错误信息并跳出循环。
//如果select函数返回0,表示超时,输出"Time out"。
//如果select函数返回大于0的值,表示文件描述符就绪。
//这里通过FD_ISSET宏检查标准输入文件描述符是否在集合中就绪。
//如果标准输入文件描述符就绪,调用read函数读取输入内容,并输出到控制台。
}
return 0;
}
3.实现I/O复用服务器端
#include<iostream>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<sys/select.h>
using namespace std;
#define BUF_SIZE 100//宏定义了一个缓冲区大小为100的常量。
void error_handling(const char *buf);
int main(int argc,char *argv[]){
int serv_sock,clnt_sock;
struct sockaddr_in serv_adr,clnt_adr;
struct timeval timeout;
fd_set reads,cpy_reads;
socklen_t adr_sz;
int fd_max,str_len,fd_num,i;
char buf[BUF_SIZE];
if(argc!=2){
cout<<"Usage:"<<argv[0]<<endl;
exit(1);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0);//创建套接字
memset(&serv_adr,0,sizeof(serv_adr));//将结构体清零
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);//设置服务器地址信息、IP地址和端口号
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("bind() error"); //将套接字和指定地址绑定
if(listen(serv_sock,5)==-1)//开始监听连接请求
error_handling("listen() error");
FD_ZERO(&reads);//清空文件描述符集合
FD_SET(serv_sock,&reads);//将服务器套接字添加到reads集合中
fd_max=serv_sock;//fd_max初始化为服务器套接字的值
while(1){//进入主循环
cpy_reads=reads;//将reads集合复制过来
timeout.tv_sec=5;//超时时间
timeout.tv_usec=5000;
if((fd_num=select(fd_max+1,&cpy_reads,0,0,&timeout))==-1)
break; //监控文件描述符的状态变化
if(fd_num==0)
continue;
for(i=0;i<fd_max+1;i++){
if(FD_ISSET(i,&cpy_reads)){//检查文件描述符是否就绪
if(i==serv_sock){//如果是服务器套接字,表示有新的客户端连接请求
adr_sz=sizeof(clnt_adr);
clnt_sock=
accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
FD_SET(clnt_sock,&reads);
if(fd_max<clnt_sock)
fd_max=clnt_sock;
cout<<"connect client: "<<clnt_sock<<endl;
}
else{//如果不是服务器套接字,表示已连接的客户端有数据发送过来
str_len=read(i,buf,BUF_SIZE);
if(str_len==0){
FD_CLR(i,&reads);
close(i);
cout<<"closed client:"<<i<<endl;
}
else
write(i,buf,str_len);
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(const char *buf){
cout<<buf<<endl;
exit(1);
}
(三)总结
1.请解释复用技术的通用含义,并说明何为I/O复用。
复用技术指为了提高物理设备的效率,用最少的物理要素传递最多数据时使用的技术。同样,I/O复用是指将需要I/O的套接字捆绑在一起,利用最少限度的资源来收发数据的技术。
2.多进程并发服务器的缺点有哪些?如何在I/O复用服务器端中弥补?
多进程并发服务器的服务方式是,每当客户端提出连接要求时,就会追加生成进程。但构建进程是一项非常有负担的工作,因此,向众多客户端提供服务存在一定的局限性。而复用服务器则是将套接字的文件描述符捆绑在一起管理的方式,因此可以一个进程管理所有的I/O操作。
3.select函数的观察对象中应包含服务器端套接字(监听套接字),那么应将其包含到哪一类监听对象集合?请说明原因
服务器套接字的作用是监听有无连接请求,即判断接收的连接请求是否存在?应该将其包含到“读”类监听对象的集合中。
4.select函数使用的fd_set结构体在Windows和Linux中具有不同的声明。请说明却别,同时解释存在区别的必然性
Linux的文件描述符从0开始递增,因此可以找出当前文件描述符数量和最后生成的文件描述符之间的关系。但Windows的套接字句柄并非从0开始,并且句柄的整数值之间并无规律可循,因此需要直接保存句柄的数组和记录句柄数的变量。