首先我们来了解下什么是多路io复用
什么是io多路复用?
- io 多路复用是一种同步io的模型,实现一个线程可以同时监听多个文件描述符;
- 一旦有某个监听的文件句柄就绪,就能够通知应用程序进行相应的读写操作
- 没有文件句柄就绪就会阻塞应用程序,交出cpu
- 多路是指网络连接,复用指的是同一个线程
为什么要有io多路复用机制?
我们先来看一段代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERPORT 8000
#define SERIP "127.0.0.1"
int main(int argc,char* argv[])
{
int lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in seraddr,cliaddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SERPORT);
inet_pton(AF_INET,SERIP,(void *)&seraddr.sin_addr.s_addr);
int ret = bind(lfd,(struct sockaddr*)&seraddr,sizeof(seraddr));
if(ret<0)
{
printf("bind erro");
return 0;
}
listen(lfd, 64);
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd,(struct sockaddr * )&cliaddr, &len);
char dst[64];
short cliport;
cliport = ntohs(cliaddr.sin_port);
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr,dst,sizeof(dst));
if(cfd < 0)
{
perror("accept error");
return 0;
}
if(cfd > 0)
{
printf("客户端已连接cfd = %d,ip = %s,port = %d\n",cfd,dst,cliport);
char buf[1024];
while(1)
{
int ret = read(cfd,buf,sizeof(buf));
write(1,buf,ret);
write(cfd,buf,ret);
}
}
while(1);
return 0;
}
这是一个linux下的一个回射服务器的代码,最开始我们的程序会在accept函数阻塞,当用一个客户端与我们的服务器建立连接时,我们的程序会进入while(1)将我们在客户端接受到的数据再次发回给客户端,但是如果我们已经与一个客户端建立了连接,此时如果还有一个客户端想与我们建立连接就会发现虽然我们已经完成了三次握手,但是并没有调用accept函数将我们这个新的客户端进入使用,我们原来的程序进入了while(1),已经没有机会调用accept
使用多线程/进程虽然也能解决这个问题,但是我们每建立一个连接就新建一个进程/线程,这会大大的增加我们服务器的开销,多路io复用的出现就是为了解决这样的问题。
select()的系统调用
首先我们来看下select()的函数原型
int select(
int nfds,
fd_set *readset,
fd_set *writeset,
fd_set *exceptset,
struct timeval *timeout
)
接下来我们来一一的解释select()函数的每个参数代表的含义。
我们先来看 readset writeset 和exceptset。
- readfds是用来检测输入是否就绪的文件描述符集合。
- writefds是用来检测输出是否就绪的文件描述符集合。
- exceptfds是用来检测异常情况是否发生的文件描述符集合。
这三个参数分别代表着文件描述符作为select的传入传出参数,我们需要将我们监听的文件描述符在调用select()之前放到这三个文件描述符结合之中。
而 nfds 我们需要传入这三个文件描述符结合之中最大的文件描述符大1的值。
timeout控制着select的阻塞行为。该参数可以指定为NULL,此时select()会一直阻塞,或者指向一个timeval结构体
struct timeval{
time_t tv_sec;
suseconds_t tv_usec;
}
如果结构体timeval的两个域都为0的话,此时selelct()不会阻塞,他只是简单的轮询指定的文件描述符集合,看看其中是否有就绪的文件描述符并且立即返回。否则timeout将为selec()指定一个等待时间的上限值。
当timeout设为NULL,select()会阻塞,直到readset writeset 和exceptset中指定的文件描述符成为就绪态并且返回
而如何向readset writeset 和exceptset。中放入我们的文件描述符呢?
我们需要涉及到四个函数
FD_ZERO(fd_set *set) //清空集合
FD_SET(int fd,fd_set* fds) //将给定的文件描述符加入集合
FD_ISSET(int fd,fd_set* fds) //判断指定的描述符是否在集合中
FD_CLR(int fd,fd_set* fds) //将给定的文件描述符从文件中删除
通过这四个函数我们可以向我们需要的文件描述符集合中放入相应的文件描述符,从而告诉系统,我们需要让系统监听哪个文件描述符的对应的哪个事件
select()的返回
当select返回时,对应的readset writeset 和exceptset中没用触发事件的文件描述符会被情况,其他的文件描述符会被保存,我们可以调用FD_ISSET查看对应的文件描述符是否在readset writeset 和exceptset就能知道是哪个文件描述符触发了哪个事件,从而进行相对应的操作
下面是Linux下的select()的一个回射服务器的代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <sys/select.h>
#define SERPORT 8848
void myerr(char * str){
perror(str);
exit(1);
}
int main(int argc,char* argv[])
{
int lfd = socket(AF_INET,SOCK_STREAM,0);
//socket 打开一个网络通信端口AF_INET ip4 SOCK_STREAM 用tcp传输
struct sockaddr_in seraddr,cliaddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SERPORT);
seraddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&seraddr,sizeof(seraddr));
if(ret < 0){
myerr("bind error");
}
listen(lfd, 64);
char buf[1024];
socklen_t addrlen = sizeof(cliaddr);
fd_set rest,aset;
FD_ZERO(&aset);
FD_SET(lfd, &aset);
int maxfd = lfd;
//select
int i;
while(1)
{
rest = aset;
int selectret = select(maxfd+1, &rest,NULL,NULL,NULL);
if(selectret == 0)
{
myerr("select erro");
}
else
{
if(FD_ISSET(lfd, &rest))
{
int cfd = accept(lfd,(struct sockaddr*)&cliaddr, &addrlen);
if(cfd<0)
{
myerr("accept error");
}
char dst[64];
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,dst,sizeof(dst));
int port = ntohs(cliaddr.sin_port);
printf("客户端已连接: serip = %s ,serPORT = %d, serCFD = %d\n",dst,port,cfd);
FD_SET(cfd,&aset);
if(maxfd<cfd)
{
maxfd = cfd;
}
if(--selectret == 0)
{
continue;
}
}
for(i = lfd+1; i < maxfd+1;i++)
{
if(FD_ISSET(i, &rest))
{
int rr = read(i,buf,sizeof(buf));
if(rr == 0)
{
close(i);
FD_CLR(i,&aset);
}
write(1,buf,rr);
write(i,buf,rr);
if(--selectret == 0)
{
break;
}
}
}
}
}
return 0;
}