IO 设备的复用模型
多线程和多进程都有很大的缺点。
多进程资源消耗大,在很多用户同时连接时,需要消耗很大资源来创建足够多的进程。
而多进程虽然共用一个进程的资源,但是带来了同步问题
IO设备复用是常用的高并发模型
IO设备分时复用:
一个系统平台中,IO 设备通常情况下只有一个,而如果存在多个外部主机模块与之进行输入输出通信时,则此 IO 设备会出并发竞态,但如果 IO 设备执行得够快,则可以同时与多个外部主机通信,但需要分时复用 IO 设备有多个已经连接下的客户端与正要建立连接的客户端保持与服务器的网络通信,而网卡只有一个,它们的网络包均要经过服务器网卡发送与接收。
只要网卡工作得够快,在发现有数据包过来的情况下及时转移缓冲区数据,则是可以实现并发通信的
通过 IO 设备复用实现并发通信,则只需要:
1)及时发现交互数据包
2)查出要与之通信的客户端
3)快速实现收发动作
调用 Select 函数,可以调用 sys_select 系统调用,启动一个内核服务进程,用于监视我们需要监视的文件描述符的变化情况--读写或是异常。
select函数
select 函数,主要用来实现同步 IO 处理,即等待数据准备好
注意:select不止可以用来检查套接字的情况,它对于所有文件描述符都适用
头文件
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set exceptfds, struct timevaltimeout);
参数
nfds:需要检查的号码最高的文件描述符加 1
fd_set 结构: 在底层实现中,实质就是:
使用 Bit 来描述一个套接字,bit(0)表示套接字所代表的客户端还没有准备就绪;bit(1)表示套接字所代表的客户端已经准备就绪
例如:使用 select 来监听 1024 个套接字符,可以使用 32 个 int(32bit)来表示(32*32 = 1024)
readfds 表示要监听的“读套接字符组”,如果取 NULL,表示不关心任何文件的读变化
writefds 表示要监听的“写套接字符组”,如果取 NULL,表示不关心任何文件的写变化
exceptfds 表示要监听的“异常套接字符组”,如果取 NULL,表示不关心任何文件的异常变化
可以通过如下接口操作 fd_set 结构:
void FD_CLR(int fd, fd_set *set); // 从 set 中删除 fd 套接字符
void FD_SET(int fd, fd_set *set); // 将 fd 套接字符添加到 set 组中
void FD_ZERO(fd_set *set); // 清除 set 组中所有套接字符
int FD_ISSET(int fd, fd_set *set); // 检查描述符 fd 是否存在与队列 set 中(对 set 组中的 fd 套接字符进行检测,判断是否准备就绪)
timeval结构(定时器):
struct timeval{
long tv_sec;//秒
long tv_usec;//微秒
}
定时器方式有三种:
第 1 种:不等待或者等待 0,此时 timeout = 0
第 2 种:等待指定时间 t,此时 timeout = t
第 3 种:一直等待,直到满足,此时 timeout = NUL
返回值
失败:小于0
超时:0,未在限定时间内找到符合条件的描述符
成功:返回符合条件的描述符个数(大于0)
select的简单使用(系统调用)
当select返回结果大于0时,相关描述符会被标记,再次使用select时,select会直接使用上次的结果(只有对描述符进行读写操作后才会重新标志)
select的实现原理:
调用一次 select,就会对三种性质的描述符组进行判断:
0. 将描述符加入三个组中,让 select 监视
- 如果有可读或者可写或者异常,则 select 退出并返回一个大于 0 的值
- 如果没有可读或者可写或者异常,则判断是否超时
如果超时,会马上退出 select 并返回 0
如果还没超时,则继续阻塞,等待… - 如果出错,则直接退出 select 并返回一个负数。
- 一次 select 执行,会将组中不满足条件的描述符去掉,留下满足条件的
- 再通过 FD_ISSET 判断到底是哪个组中的哪个描述符满足了条件(要每个组中的描述符都FD_ISSET,select无法判断哪个描述符符合条件)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
int main()
{
fd_set rset,wset;
FD_ZERO(&rset);
FD_ZERO(&wset);
//设置等待时间
struct timeval tv;
while(1)
{
//select执行后会清空定时器
tv.tv_sec=4;
tv.tv_usec=1000;
//select执行后会清除不能读写执行的描述符
FD_SET(0,&rset);
FD_SET(0,&wset);
//FD_SET(1,&wset);
printf("begin select ...\n");
int ret=select(1+1,&rset,&wset,NULL,&tv);
printf("select return...\n");
if(ret<0)
{
perror("select fail");
exit(1);
}
else if(ret==0)
printf("timer out\n");//select最后一个参数不为NULL时才有可能触发
else
{
printf("select success,ret:%d\n",ret);
sleep(2);
//找出rset中可读的描述符
if(FD_ISSET(0,&rset))
printf("stdin can be read!\n");
if(FD_ISSET(0,&wset))
printf("stdin can be writed\n");
}
}
return 0;
}
select实现简单的并发服务器
服务器端:
实现一个单线程单进程的IO复用服务器,在收到客户端连接请求后,将客户端的描述符加入到读套接字组中,进行状态检测,如果可读即将客户端发送过来的消息读取出来
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/select.h>
#include<sys/time.h>
#define MAXSIZE 128
int main()
{
int sockfd;
//获取套接字
sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd<0)
{
perror("socket fail:");
exit(1);
}
printf("socket success\n");
//设置服务器地址信息
struct sockaddr_in saddr,caddr;
saddr.sin_family=AF_INET;
saddr.sin_port=htons(8990);
saddr.sin_addr.s_addr=htons(INADDR_ANY);
socklen_t slen,clen;
slen=sizeof(saddr);
clen=sizeof(caddr);
//绑定套接字
bind(sockfd,(struct sockaddr*)&saddr,slen);
listen(sockfd,128);
printf("listen end,begin accept---\n");
char rdbuffer[MAXSIZE]={0};
fd_set rset,tempset;
FD_ZERO(&rset);
FD_ZERO(&tempset);
struct timeval tv;//设置select的定时器
int mfd=sockfd;
FD_SET(mfd,&rset);//将mfd加入读套接字中
tempset=rset;
//开始IO复用
while(1)
{
tv.tv_sec=3;
tv.tv_usec=0;
//将监听套接字写入读套接字中,若有连接,则可读
printf("begin select---\n");
int ret=select(1+mfd,&rset,NULL,NULL,&tv);
//select失败
if(ret<0)
{
perror("select fail:");
exit(1);
}
//超时
else if(ret==0)
{
printf("time out\n");
rset=tempset;
}
//有可以读的套接字
else
{
if(FD_ISSET(sockfd,&rset))//新的连接
{
int client_fd=accept(sockfd,(struct sockaddr*)&caddr,&clen);
printf("client_fd:%d\n",client_fd);
printf("ip:%s\n",inet_ntoa(caddr.sin_addr));
//将新的套接字加入读套接字
FD_SET(client_fd,&tempset);
rset=tempset;//更新读套接字
char wbuf[32]={0};
sprintf(wbuf,"%d",client_fd);
write(client_fd,wbuf,32);
sleep(1);//防止粘包
write(client_fd,"hello,connect success!",32);
//更新最大的描述符
mfd=mfd<client_fd?client_fd:mfd;
}
else//连接的客户端中有客户端发送消息过来
{
for(int fd=4;fd<=mfd;++fd)
{
if(FD_ISSET(fd,&rset))//读取可读套接字的消息
{
memset(rdbuffer,0,MAXSIZE);
int cnt=read(fd,rdbuffer,MAXSIZE);
if(cnt<0)
perror("read fail:");
else if(cnt==0)//连接关闭时,客户端会发送0字节的数据过来(四次挥手)
{
printf("fd:%d---disconnect!\n",fd);
close(fd);
//在tempset中移除fd
FD_CLR(fd,&tempset);
}
else//正常读取
{
printf("rdbuffer:%s\n",rdbuffer);
}
}
rset=tempset;//更新读套接字
}
}
}
}
}
客户端
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define MAXSIZE 128
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd<0)
{
perror("socket fail:");
exit(1);
}
printf("socket success:%d\n",sockfd);
struct sockaddr_in saddr;
socklen_t slen;
saddr.sin_family=AF_INET;
saddr.sin_port=htons(8990);
saddr.sin_addr.s_addr=inet_addr("192.168.0.64");
slen=sizeof(saddr);
int err;
do{
err=connect(sockfd,(struct sockaddr*)&saddr,slen);
}while(err!=0);
printf("connect success\n");
char rdbuffer[MAXSIZE]={0};
char r[32]={0};
//等待服务器端发送套接字编号和连接成功的信息
read(sockfd,r,32);
read(sockfd,rdbuffer,MAXSIZE);
printf("rdbuffer:%s\n",rdbuffer);
int cnt=10;
memset(rdbuffer,0,MAXSIZE);
//编辑要发送给服务器的消息
sprintf(rdbuffer,"%s--send message!",r);
while(cnt)
{
write(sockfd,rdbuffer,MAXSIZE);
sleep(2);
--cnt;
}
close(sockfd);
printf("sockfd---send over\n");
}