15.4 多客户
到目前为止,本章一直介绍的是,如果用套接字来实现本地的和跨网络的客户/服务器系统.一旦连接建立,套接字连接的行为就类似于打开的底层文件描述符,而且在很多方面类似于双向管道.
现在考虑有多个客户同时连接一个服务器的情况.服务器程序在接受来自客户的一个新连接时,会创建出一个新的套接字,而原先的监听套接字将被保留以继续监听以后的连接.如果服务器不能立刻接受后来的连接,它们将被放到队列中以等待处理.
原先的套接字仍然可用并且套接字的行为就像文件描述符,这一事实提供了一种同时服务多个客户的方法.如果服务器调用fork为自己创建第二份副本,打开的套接字就将被新的子进程所继承.新的子进程可以和连接的客户进行通信,而主服务器进程可以继续接受以后的客户连接.这些改动对服务器程序来说是非常容易的.
因为创建子进程,但并不等待它们的完成,所有必须安排服务器忽略SIGCHLD信号以避免出现僵尸进程.
程序 可以同时服务多个客户的服务器
编写程序server4.c
/************************************************************************* > File Name: server4.c > Description: server4.c > Author: Liubingbing > Created Time: 2015年07月25日 星期六 17时22分52秒 > Other: server4.c ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <signal.h> int main() { int server_sockfd, client_sockfd; int server_len, client_len; struct sockaddr_in server_address; struct sockaddr_in client_address; /* socket函数创建套接字 */ server_sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 套接字地址由结构sockaddr_in来指定,一个AF_INET套接字由它的域,IP地址和端口号完全确定 */ 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函数给套接字命名,使AF_INET套接字关联到一个IP端口号 * 此外bind调用需要就爱那个一个特定的地址结构转换为指向通用地址类型(struct sockaddr *) */ bind(server_sockfd, (struct sockaddr *)&server_address, server_len); /* 创建一个连接队列,忽略子进程的退出细节,等待客户的到来 */ listen(server_sockfd, 5); signal(SIGCHLD, SIG_IGN); while (1) { char ch; printf("server waiting\n"); /* 接受连接 */ client_len = sizeof(client_address); client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len); /* 通过fork调用为这个客户创建一个子进程,然后测试在父进程还是子进程 */ if (fork() == 0) { read(client_sockfd, &ch, 1); sleep(5); ch++; write(client_sockfd, &ch, 1); close(client_sockfd); exit(0); } else { close(client_sockfd); } } }
在处理客户请求时插入的5秒延迟是为了模拟服务器的计算时间或数据库访问时间.如果在前面的服务器中这样做,client3的每次运行都将花费5秒钟的时间,而新服务器可以同时处理多个client3程序,所花费的总时间将只有5秒钟多一点.如下所示:
程序解析
服务器程序现在将创建一个新的子进程来处理每个客户,所以将看到好几个服务器在等待消息,而主进程将继续等待新的连接.server4进程正在等待新的客户,而3个client进程正在由3个服务器的子进程进行服务.在经过5秒的暂停后,所有的客户都得到了它们的结果并结束运行.服务器的子进程也都退出,只留下主服务器进程在运行.
服务器程序用fork函数处理多个客户,但在数据库应用程序中,这可能不是最佳的解决方案,因为服务器程序可能会相当大,而且在数据库访问方面还存在着需要协调多个服务器副本的问题.事实上,真正需要的是,如果让单个服务器进程在不阻塞,不等待客户请求到达的前提下处理多个客户.这个问题的解决方案设计如何同时处理多个打开的文件描述符,并且它不仅仅局限于套接字,请看下一节的select系统调用.
15.4.1 select系统调用
在编写linux应用程序时,经常会遇到需要检查好几个输入的状态才能确定下一步行动的情况.例如,像终端仿真器这样的通信程序,需要有效地同时读取键盘和串行口.如果是在一个单用户系统中,运行一个"忙等待"循环还是可以接受的,它不停地扫描输入设置看是否有数据,如果有数据到达就读取它,但这种做法很消耗CPU的时间.
select系统调用允许程序同时在多个底层文件描述符上等待输入的到达(或输出的完成).这意味着终端仿真程序可以一直阻塞到有事情可做为止.类似地,服务器也可以通过同时在多个打开的套接字上等待请求到来的方法来处理多个客户.
select函数对数据结构fd_set进行操作,它是由打开的文件描述符构成的集合,有一组定义好的宏可以用来控制这些集合:
#include <sys/types.h> #include <sys/time.h> void FD_ZERO(fd_set *fdset); void FD_CLR(int fd, fd_set *fdset); void FD_SET(int fd, fd_set *fdset); void FD_ISSET(int fd, fd_set *fdset);
FD_ZERO用于将fd_set初始化为空集合.
FD_SET和FD_CLR分别用于在集合中设置和清除由参数fd传递的文件描述符.
如果FD_ISSET宏中由参数fd指向的文件描述符是由参数fdset指向的fd_set集合中的一个元素,FD_ISSET将返回非零值.
fd_set结构中可以容纳的文件描述符的最大数目由常量FD_SETSIZE指定.
select函数还可以用一个超时值来防止无限期的阻塞,这个超时值是由一个timeval结构给出,这个结构定义在文件件sys/time.h中,它由一下几个成员组成:
struct timeval { time_t tv_sec; /* seconds */ long tv_usec; /* microseconds */ };
类型time_t在头文件sys/types.h中被定义为一个整数类型.
select系统调用的原型如下所示:
#include <sys/types.h> #include <sys/time.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
select调用用于测试文件描述符集合中,是否有一个文件描述符已处于可读状态或可写状态或错误状态,它将阻塞以等待某个文件描述符进入上述这些状态.
参数nfds指定需要测试的文件描述符的数目,测试的描述符访问从0到nfds-1.3个描述符集合都可以被设为空指针,着表示不执行相应的测试.
select函数会发生以下情况时返回:readfds集合中描述符可读,writefds集合中有描述符可写或errorfds集合中有描述符遇到错误条件,如果这3种情况都没有发生,select将在timeout指定的超时时间经过后返回.如果timeout参数是一个空指针并且套接字上也没有任何活动,这个调用将一直阻塞下一.
当select返回时,描述符集合将被修改以指示哪些描述符正处于可读,可写或者错误的状态.可以用FD_ISSET对描述符进行测试,来找到需要注意的描述符.可以修改timeout值来表明剩余的超时时间,但这并不是在X/Open规范中定义的行为.如果select是因为超时而返回的话,所有描述符集合都将被清空.
select调用返回状态发生变化的描述符总数.失败时它将返回-1并设置errno来描述错误.可能出现的错误有:EBADF(无效的描述符),EINTR(因中断而返回),EINVAL(nfs或timeout取值错误)
版权声明:本文为博主原创文章,未经博主允许不得转载。