为了使得程序能够去监听多个文件描述符,即可以同时关注多个监听套接字和连接套接字,这里有三种基本方式可以解决服务器端同时与多个客户端进行通信的问题:
1. io复用
2. poll
3. epoll
这里我们先来介绍IO复用,IO复用不仅仅可以适用于网络编程,也可以用于监听管道数据等其他有读写数据的方面。
1. int select(int fd, fd_set *readset, fd_set * writeset,fd_set * exceptions, struct timeval * timeout)
这个函数的返回值等于0,表示超时;小于0,表示错误,大于0,表示已准备就绪的描述符数。
select函数的机制是采用轮询的方式,时刻关注套接字是否有数据到达,该函数是在一定时间段内,监听某些描述符上的可读可写以及异常事件。
IO复用的原理是内核使用一个1024个位来存储文件描述符,一个位存一个文件描述符,存放方式采用偏移量的方式,如文件描述符为9,则第9位置1,表示该文件描述符加入了被关注的集合中。当调用了select()函数时,它会返回所有已加入文件描述符上数据准备就绪的文件描述符个数,在调用的过程·中,该函数会将已加入集合但数据还没有准备好的文件描述符所在的集合中的相应存储位重新置为0,所以每调用一次该函数,当函数返回时,1024个位上只有数据已经准备就绪的文件描述符所对应的位才是1,而不是我们刚开始的关注的所有描述符。所以我们只需要FD_ISSET(...)循环遍历查看哪个位为1,就找到了那些数据已经准备就绪的文件描述符,因此就可以进行我们的相应业务处理了。这里牵扯到了四个函数:
void FD_ZERO(fd_set * set) : 初始化集合,将集合中的所有位均设置为0
void FD_SET(int fd,fd_set * set) : 将对应的文件描述符加入集合
int FD_ISSET(int fd, fd_set * set) : 查看某个文件描述符上的数据是否准备好
void FD_CLR(int fd,fd_set * set) : 某一位清0
2. 在利用IO复用编写程序的地方如下:
step 1: 准备好描述符,设置到集合中,通常有多少个描述符,就要循环多少次
step 2: 每次调用select函数,都要从用户空间向内核空间拷贝数据
step 3: select返回之后,要循环遍历所有文件描述符,找到就绪的文件描述符
3. 当每次只接收读取一个字符,而客户端每次输入一个字符串时,那么服务器就会调用select一共字符串的长度加1次(因为包含回车),最终服务器的终端上会逐次将输入的字符串以一次一个字符的形式打印显示出来,再输出一个回车字符。
4. IO复用代码如下,在此程序中,就实现了服务器端同时与多个客户端进行通信的过程:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
#define MAX 10
void fds_init(int fds[])
{
int i=0;
for(;i<MAX;++i)
{
fds[i]=-1;
}
}
void fds_add(int fds[],int fd)
{
int i=0;
for(;i<MAX;++i)
{
if(fds[i]==-1)
{
fds[i]=fd;
break;
}
}
}
void fds_clr(int fds[],int fd)
{
int i=0;
for(;i<MAX;++i)
{
if(fds[i]==fd)
{
fds[i]=-1;
break;
}
}
}
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("192.168.31.120");
int res = bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
assert(res!=-1);
listen(sockfd,5);
int fds[MAX];
fds_init(fds);
fds_add(fds,sockfd);
while(1)
{
fd_set fdset;
FD_SERO(&fdset);
int maxfd=-1;
int i=0;
for(;i<MAX;++i)
{
if(fds[i]==-1)
{
continue;
}
FD_SET(fds[i],&fdset);
if(maxfd<fds[i])
{
maxfd=fds[i];
}
}
struct timeval tv={5,0};
int n=select(maxfd+1,&fdset,NULL,NULL,&tv);
if(n==-1)
{
perror("select error\n");
continue;
}
else if(n==0)
{
printf("time out\n");
continue;
}
else
{
int i=0;
for(;i<MAX;++i)
{
if(fds[i]==-1)
{
continue;
}
if(FD_ISSET(fds[i],&fdset))
{
if(fds[i]==sockfd)
{
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept ::c=%d\n",c);
fds_add(fds,c);
}
else
{
char buff[128]={0};
if(recv(fds[i],buff,127,0)<=0)//why we can not read many times?because once no data,the recv will block,can not over
{
printf("one client over\n");
close(fds[i]);
fds_crl(fds,fds[i]);
continue;
}
printf("read(%d)=%s\n",fds[i],buff);
send(fds[i],ok,2,0);
}
}
}
}
}
}