LinuxI/O多路复用转接服务器——select模型实现
select函数
select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数和返回值
参数:
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds:监控有读数据到达文件描述符集合,传入传出参数
writefds:监控写数据到达文件描述符集合,传入传出参数
exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout:定时阻塞监控时间,3种情况
1. NULL,阻塞监听
2. 设置timeval,等待固定时间
3. 设置timeval里时间均为0,检查描述字后立即返回,轮询
返回值:
成功:返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。
失败:返回-1并设置errno。如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。
fd_set结构体
上面select函数中需要用到两个fd_set形参,这个结构体到底做什么用的呢?
fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。
位操作函数
系统提供了FD_SET, FD_CLR, FD_ISSET, FD_ZERO进行操作,声明如下:
#include <sys/select.h>
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
select实现实现I/O多路复用服务器
实现流程
程序实现
服务端程序
#include<iostream>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <signal.h>
#include "wrap.h"
using namespace std;
//定义服务端端口号
#define SERVER_PORT 9527
int main (int argc ,char*argv[])
{
int i,n;
int client[FD_SETSIZE];//自定义数组,大小为1024
int nready=0;//保存select函数返回值
int maxfd=0;//记录最大的文件描述符
int lfd=0;//用于监听的套接字
int cfd=0;//用于通信的套接字
int sockfd=0;
int maxi;//用于检索客户端文件描述符的下标
char buf[BUFSIZ];
//创建套接字
lfd=Socket(AF_INET,SOCK_STREAM,0);
//创建地址结构
struct sockaddr_in server_addr,client_addr;
socklen_t client_addr_len;
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//初始化
//memset(&server_addr,0,sizeof(server_addr));//将地址结构清零
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(SERVER_PORT);
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定地址结构
Bind(lfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
//设置监听
Listen(lfd,128);
//定义文件描述符集合 读事件文件描述符集合 allset用来存储
fd_set rset;
//存放所有可以被监控的文件描述符
fd_set allset;
//刚开始只有一个用于连接的文件描述符,故最大的即为lfd
maxfd=lfd;
//用于检索客户端文件描述符的下标
maxi=-1;
for(i=0;i<FD_SETSIZE;i++)
{
//初始化client数组
client[i]=-1;
}
//把文件描述符集合里所有位清0
FD_ZERO(&allset);
//把文件描述符集合里lfd位置1
FD_SET(lfd,&allset);
//需要循环设置监听
while(1)
{
rset=allset;
//调用select函数监听文件描述符对应事件
nready=select(maxfd+1,&rset,NULL,NULL,NULL);
//检查是否成功返回
if(nready<0)
{
sys_err("select error");
}
//判断监听套接字是否在文件描述符集合里
//如返回为真则说明有新的客户端进行连接请求
if(FD_ISSET(lfd,&rset))
{
client_addr_len=sizeof(client_addr);
//调用accept函数接收客户端请求
cfd=Accept(lfd,(struct sockaddr*)&client_addr,&client_addr_len);
//将返回的文件描述符存入client数组
for(i=0;i<FD_SETSIZE;i++)
{
if(client[i])
{
client[i]=cfd;
break;
}
}
//达到监听上限报错 1024
if(i==FD_SETSIZE)
{
cout<<"too many clients"<<endl;
exit(1);
}
//将返回的文件描述符添加到文件描述符集合中
FD_SET(cfd,&allset);
//获取最大文件描述符
if(maxfd<cfd)
maxfd=cfd;
//保证maxi存的为数组的最后一个元素下标
if(i>maxi)
{
maxi=i;
}
//判断是否只有lfd事件,若为真则只有lfd事件,后续代码不需要执行
if(0==--nready)
continue;
}
//循环检测哪个客户端数据就绪
for(i=0;i<maxi;i++)
{
if((sockfd=client[i])<0)
{
continue;
}
//判断文件描述符是否在集合里
if(FD_ISSET(sockfd,&rset))
{
//调用read函数读取数据
n =Read(sockfd,buf,sizeof(buf));
//判断是否读到结尾,若是就将该文件描述符关闭,并从文件描述符集合中清除
if(n==0)
{
Close(sockfd);
FD_CLR(sockfd,&allset);
client[i]=-1;
}
//没有读到结尾,则对读取到的数据完成大小写转换
else if(n>0)
{
for(int j=0;j<n;j++)
{
//大小写转换
buf[j]=toupper(buf[j]);
}
//写回buf
Write(i,buf,n);
//写到屏幕输出
Write(STDOUT_FILENO, buf, n);
}
}
}
}
Close(lfd);
return 0;
}
客户端程序
运行结果
服务端:
客户端:
select使用优缺点
优点:
- 跨平台使用
缺点:
- select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个。
- 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。
- 检测满足条件的文件描述符,编码难度增加。