服务端用fork来处理多个客户的思路是为每个客户创建一个新建的进程进行单独处理,也就是采取了多个服务器进程的方式,这在涉及到数据库的应用中不是最佳的解决方案。一般的解决方案是让单个服务器进程在不阻塞、不等待客户到达的情况下处理多个客户。采用select调用可以实现这一方案:select处理的核心是如何处理多个打开的文件描述符,由于是文件描述符,select的应用也不仅仅局限于套接字应用程序。
select函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
select调用检查被监控的文件描述符中,是否有一个文件描述符处于可读、可写、或错误状态。返回值为状态发生变化的描述符总数。失败时返回-1并设置errno来描述错误。
nfds指定需要监控的文件描述符的总数。
timeout指定超时时间,如果为空,select调用将一直阻塞直到某个描述符状态发生改变。
select函数对数据结构fd_set进行操作,它是由打开的文件描述符构成的集合,有一组定义好的宏可以用来控制这些集合:
void FD_ZERO(fd_set *fdset);//FD_ZERO用于将fd_set初始化为空集合
void FD_SET(int fd,fd_set *fdset);//在集合中设置fd指定的文件描述符
void FD_CLR(int fd,fd_set *fdset);//在集合中清除fd指定的文件描述符
int FD_ISSET(int fd,fd_set *fdset);//调用select()函数后,用FD_ISSET来检测fd在fdset集合中的状态是否变化,变化时返回真(非0)
下面是一个由select来监听标准输入的例子(文件描述符为0):
#include <sys/types.h> #include <sys/time.h> #include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> int main() { char buffer[128]; int result, nread; fd_set inputs, testfds; struct timeval timeout; FD_ZERO(&inputs); FD_SET(0,&inputs); while(1) { testfds = inputs; timeout.tv_sec = 2; timeout.tv_usec = 500000; result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, &timeout); switch(result) { case 0: printf("timeout\n"); break; case -1: perror("select"); exit(1); default: if(FD_ISSET(0,&testfds)) {//多客户时需要判断select的返回值,并用FD_ISSET轮询检查每个描述符。 ioctl(0,FIONREAD,&nread); if(nread == 0) { printf("keyboard done\n"); exit(0); } nread = read(0,buffer,nread); buffer[nread] = 0; printf("read %d from keyboard: %s", nread, buffer); } break; } } }
下面是一个由select来实现多客户服务端的例子:
由于select调用不能再处理第一个连接的客户时让其它客户等太长的时间,因而select调用适用于短链接。对于监听套接字链接,有以下二种情况:
-
监听套接字可读,说明有客户尝试建立连接,此时可以调用accept而不用担心发生阻塞。由于建立了连接,需要将客户描述符也放入select的监控中。 -
如果是某个客户描述符有事件发生,说明该描述符上有客户请求需要我们读取和处理。即调用ioctl(fd,FIONREAD,&nread)后,检查nread是否大于0.(ioctl函数获取接受缓冲区中支字符数的个数),大于0,读取该客户描述符上的值;等于0,关闭该客户描述符并从select监听集合中移除描述符。
//客户端采用上篇博文中的样例 #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <sys/time.h> #include <sys/ioctl.h> #include <unistd.h> int main() { int server_sockfd, client_sockfd; int server_len, client_len; struct sockaddr_in server_address; struct sockaddr_in client_address; int result; fd_set readfds, testfds; server_sockfd = socket(AF_INET, SOCK_STREAM, 0); server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = htonl(INADDR_ANY); server_address.sin_port = htons(9734); server_len = sizeof(server_address); bind(server_sockfd, (struct sockaddr *)&server_address, server_len); listen(server_sockfd, 5); FD_ZERO(&readfds); FD_SET(server_sockfd, &readfds); while(1) { char ch; int fd; int nread; testfds = readfds; printf("server waiting\n"); result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, (struct timeval *) 0); if(result < 1) { perror("server5"); exit(1); } for(fd = 0; fd < FD_SETSIZE; fd++) { if(FD_ISSET(fd,&testfds)) { if(fd == server_sockfd) { client_len = sizeof(client_address); client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len); FD_SET(client_sockfd, &readfds); printf("adding client on fd %d\n", client_sockfd); } else { ioctl(fd, FIONREAD, &nread); if(nread == 0) { close(fd); FD_CLR(fd, &readfds); printf("removing client on fd %d\n", fd); } else { read(fd, &ch, 1); sleep(5); printf("serving client on fd %d\n", fd); ch++; write(fd, &ch, 1); } } } } } }
改进点:可用一个变量来专门保存已经连接套接字的最大文件描述符,这样select轮询的范围就会小很多。