1、在之前学的TCP连接的时候我们知道,服务器端会阻塞在两个地方,第一阻塞在连接套接口的地方(recv那行代码)当和客户端连接好之后客户端没有发送数据,那我们就会一直阻塞在那里,第二就是监听套接口的地方(accept那行代码),因为刚才的recv还没有收到客户端的数据,recv一直处于阻塞的状态,进而第一次的c也不能释放,所以造成了accept没空去处理后面来的连接,但是后来我们采用了线程和进程去处理多客户端的问题,我们让子进程或子线程去和客户端交流,让主线程或主进程和新的客户端建立连接,虽然这解决了多客户端的问题但是还有一个问题不能解决,如果有一万个客户呢,我们总不能创建一万个线程或进程把,这是不可以的,因为创建它们又耗时又占资源,所以呢为了解决这个问题我们采用I/O复用。
2、 I/O复用:这里会用到一个数组和一个集合,select是用来关注这个集合的,这个集合里存的是是否有数据的描述字,大小为0到1023,这个数组是用来存放描述字的,如果有客户端连接我们,那我们就把连接我们的这个sockfd描述符存在这个数组里,当有客户端给服务器端发送数据的时候我们也将c这个描述符存在数组里,然后把存在数组里的有效数据全部塞给这个集合,然后,让select关注这个集合,它会扫描描述符最大值加1个描述符(比如说描述符最大值位6,那select只会观测前7个描述符是否有数据,因为在集合中存储的时候我们用的是下标表述描述字的,并且select如果关注的越多那么消耗就越大),集合中的所有值原本都是0,但是当数组中的数据传过来的时候,会把下标对应有描述符的位置全部改为1(比如描述符6,那我们就将下标为6的位置改为1)然后select会全部扫描一遍将有数的的描述字全部保留,将没有数据的描述符全部改为0,将为1的描述符调用accept或者recv来处理,这里也可以解决为什么还要用到数组这个问题,有人说只用一个集合就行了,何必浪费资源呢,就是因为这里的select每次关注完都会改变描述符的0 1值,如果还不能理解,那就把数组当成是存放多个客户端连接的返回值和客户端发送数据返回值的地方,但是客户端连接了不一定发送数据,所以为了防止阻塞我们就让select关注这个集合,我们只处理有数据的套接字。
3、select它返回的是集合中描述符有数据的描述符的个数,在整个代码过程中只有select会阻塞,所以我们会设置一个时间(一般设置5s),当select观测5秒之后还是没有发现描述符上有数据它就会返回超时,如果不设置一个超时时间那么它就会一直阻塞,当然了如果某个描述符上有数据,select返回,但是它返回的是有数据的描述符的个数,我们并不知道是哪个描述符,所以我们还要再遍历它,看它是哪个,然后调用相应的方法去处理。
4、如果对方已经关闭连接了,但是描述符还在集合里会发生什么?select在关注到这个已经关闭连接的描述符的时候,它会持续返回这个描述符上有数据,那么recv就会去读取数据,但是我们去读取数据的时候却是空的。
5、下面的代码中,数组的大小才是10,那如果有超过10个客户端来连接了怎么办?如果描述符的个数超过了数组的大小,那么后面的描述符是不会被放进集合中的,此时就说那个客户端强求失败。
下面我们对select函数进行详细介绍:
这个函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程。
头文件:<sys/select.h>
<sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set * writeset, fd_set *exceptset, const struct timeval *timeout)
返回值为准备好描述字的个数,返回值为0是超时,返回值为 -1出错
第一个参数:集合中描述字最大值加1; 第二个参数:集合的地址;第三、四个参数:指定我们要让内核测试读写和异常条件所需的描述字; 最后一个参数:指定的秒数和微秒数。
void FD_ZERO(fd_set *fdset); //情况这个集合
void FD_SET(int fd, fd_set *fdset) //将数组中的数据存在集合中
void FD_CLR(int fd, fd_set* fdset) //从集合中移除
void FD_ISSET(int fd, fd_set *fdset) //检查是否为有效的描述字
代码实现:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/select.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/time.h>
#define MAXFD 10
void fds_init(int fds[]) //初始化数组
{
int i = 0;
for(; i < MAXFD; i++)
{
fds[i] == -1;
}
}
void fds_add(int fds[], int fd) //往数组里添加数据
{
int i = 0;
for(; i < MAXFD; i++)
{
if(fds[i] == -1)
{
fds[i] = fd;
return ;
}
}
}
void fds_del(int fds[], int fd) //移除数据
{
int i = 0;
for(; i< MAXFD; i++)
{
if(fds[i] == fd)
{
fds[i] = -1;
return ;
}
}
}
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("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
listen(sockfd,5);
int fds[MAXFD]; //定义数组
fds_init(fds);
fds_add(fds,sockfd); //将套接字的返回值(描述字)放进数组中
while(1)
{
fd_set fdset; //定义一个集合
FD_ZERO(&fdset);//清空集合
int maxfd = -1;
int i = 0;
for(; i < MAXFD; i++)
{
if(fds[i] == -1)
{
continue;
}
FD_SET(fds[i], &fdset);//将有效的描述字放进集合中
if( fds[i] > maxfd) //找描述字中的最大值
{
maxfd = fds[i];
}
}
struct timeval tv = {5,0}; //设定超时时间
int n = select(maxfd+1, &fdset,NULL,NULL,&tv); //调用select关注
if(n == -1)//失败
{
printf("select error\n");
continue;
}
else if(n == 0) //没有哪个描述字上是有数据的
{
printf("time out\n");
continue;
}
else
{
int i = 0;
for(; i < MAXFD; i++)
{
if(fds[i] == -1)
{
continue;
}
if(FD_ISSET(fds[i], &fdset)) //判断集合中的描述字是否有数据(是否可读)
{
if(fds[i] == sockfd) //分为socket套接字
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr, &len);//调用accept
if(c < 0)
{
continue;
}
printf("accept c = %d\n",c);
fds_add(fds,c); //再将accept返回的c普通套接字存在数组中
}
else //和普通套接字
{
char buff[128] = {0};
int num = recv(fds[i],buff,127,0);//普通套接字调用recv
if(num <= 0)
{
close(fds[i]); //没有读到数据就对方可能是已经关闭了连接,所以我们也要关闭连接
fds_del(fds,fds[i]);//将这个描述符从数组中移除
printf("one client close\n");
}
else
{
printf("recv(%d) = %s\n",fds[i],buff);
send(fds[i],"ok",2,0);
}
}
}
}
}
}
}