select函数可以让单进程单线程去处理多个客户端。
其原理是将与每个客户端通信的文件描述符和接收客户端的文件描述符制成一张文件描述符表,文件描述符一般是非负整数,文件描述符的值就是在这个文件描述符表的位置,比如fd=6,就是在这张表的第六位就代表了fd,循环使用select函数去检测文件描述符表中的文件描述符对应的缓冲区,是否存在缓冲数据,如果有,就把该文件描述符所在位置1。然后进程在根据更改过的文件描述符表去处理对应的客户端操作。select函数是由内核来完成的操作。
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *timeout);
参数解释:
第一个参数int nfds:有效文件描述符的最大值+1或者填1024(这个参数为了节省内核操作select函数的时间,比nfds更大的数值,就不会去判断了,文件描述符表中最大位置就是1024,这是内核规定了,使用1024就是把文件描述符中所有的位都检查一遍)
数据结构fd_set:
这是文件描述符表的数据结构,类似于信号集,可以用以下四种函数来操作
void FD_ZERO(fd_set *)将某一个文件描述符表集合清空
void FD_SET(int, fd_set *)将一个给定的文件描述符加入到集合之中,也就是将文件描述符表对应的位置置1
void FD_CLR(int, fd_set *)从集合中删除指定的文件描述符,也就是将文件描述符表对应的位置置0。
int FD_ISSET(int, fd_set *)检查集合中指定的文件描述符是否准备好(可读或可写),也就是判断文件描述符表对应的位置是否为1,为1返回大于0的数,为0返回0。
第二个参数fd_set *readfds:readfds是一个文件描述符表集合,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了。我的理解是会去文件描述符对应的读缓冲区查看是否存在可读数据,如果有,就返回1
第三个参数fd_set *writefds:和上面类似,只不过是对应的写操作,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了
第四个参数fd_set *exceptfds:是用来监视文件错误异常的文件描述符。(以上三个参数都是传入传出参数,传入原来的文件描述符表,传出select检测后有反应的文件描述符表)
第五个参数struct timeval* timeout:是select的超时时间,这个参数至关重要,它可以使select处于三种状态。
第一:若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二:若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三:timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
返回值:返回需要处理的文件描述符的个数。或者0
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>using namespace std;int main(int argc, const char *argv[])
{if (argc<2){cout<<"input :./a.out port";exit(1); }int sfd, cfd;int port=atoi(argv[1]);char buf[256];int n;struct sockaddr_in addr_server;addr_server.sin_port=htons(port);addr_server.sin_addr.s_addr=htonl(INADDR_ANY);addr_server.sin_family=AF_INET;struct sockaddr_in addr_client;socklen_t addrlen=sizeof(addr_client);sfd=socket(AF_INET, SOCK_STREAM, 0);bind(sfd, (struct sockaddr*) &addr_server, sizeof(addr_server));listen(sfd, 20);fd_set reads,temp;//定义两个文件描述符表,由于客户端主要是被动传输,所以主要接受读端的文件描述符表int maxfd;//存放最大的文件描述符值FD_ZERO(&reads);//将读端文件描述符表初始化FD_SET(sfd, &reads);//将负责接受客户端的文件描述符放入表中maxfd=sfd;while (1){temp=reads;//需要temp作为临时表,因为select2到4的参数都是传入传出参数,会改变原表中的值,不能直接传入原表。int n = select(maxfd+1, &temp, NULL, NULL, NULL);//只负责监听读端,并且是阻塞的if (FD_ISSET(sfd, &temp))//先是判断接受端有没有动作,如有就接收客户端{cfd=accept(sfd, (struct sockaddr*) &addr_client, &addrlen);if (cfd==-1){cout<<"accept is error!";exit(1);}char ip[64];cout<<"the client ip is"<<inet_ntop(AF_INET, &addr_client.sin_addr.s_addr, ip, sizeof(ip))<<endl;cout<<"the client port is"<<ntohs(addr_client.sin_port)<<endl;FD_SET(cfd, &reads);//将文件描述符填入表中对应的位置maxfd=maxfd>cfd?maxfd:cfd;}for (int i=sfd+1; i<=maxfd; i++)//,遍历select函数返回的临时表,如果有需要处理的客户端,对应文件描述符位就会置1,不需要处理的就会置0{if (FD_ISSET(i, &temp)){int n=read(i, buf, sizeof(buf));if (n==0)//客户端结束,就清除标志位{close(i);FD_CLR(i, &reads);}else if (n<0){cout<<"read is error!";exit(1);}else{for (int i=0; i<n; i++)buf[i]=toupper(buf[i]);write(cfd, buf, n);}}}}close(sfd);return 0;
}