在上一文中 http://blog.csdn.net/michael_kong_nju/article/details/44887411 我们讨论了I/O复用技术,即如何在一个进程里监测多个I/O, 刚开始接触还有点混论,但是现在想想,其实原理很简单,或者说内核设计者的想法很直接,就是以前我一个进程一次只能处理一个I/O,现在我通过一个fd_set结构体来实现将多个I/O的描述符放在一个类似于数组中,这样我通过轮训这个数组当发现有就绪的描述符时就进行处理,而在轮训的间隙进程可以做别的事情,所以可以看到实现了非阻塞。(这句话不知道自己说的对不对,可能不准确)
在上篇文章中,我已经看到了使用select来实现客户端I/O的简单复用,即同时监听socket和控制台输出流上两个描述符,使得客户端的进程不阻塞在某一个之上。
在本节我们来分析如何使用select来实现服务器的并发。
其实在知道I/O复用技术之前我一直以为服务器进程通过fork一个子进程,或者至少pthread_create一个线程来处理客户端的请求而自己继续监听来自客户端的请求这种架构
是一种很好的并发服务器设计的思路,但是知道有一次面试我被问到“如果这台服务器上亿的用户访问时你的服务器会有什么问题的时候”我才重新思考了这个问题。
是的,这么多的进程或者线程得消耗多少OS的资源啊。
而当时我只知道select或者epoll好像是可以解决这个问题的,但是具体怎么解决的我真不知道,后来重新看Richard老先生的书才真正明白。
虽然我现在也知道了使用select有实现效率和最大描述符个数的限制,但是我还是想把select的实现方式记录下来,有助于以后我们自己设计服务器架构。
下面我先介绍一下整个设计的思路:
这里面服务器端维护几个数据结构:
1. 整形数组: int client[FD_SETSIZE];用于保存每个客户的已连接的套接字描述符,注意,每次装入的时候都从数组0位置向后找第一个可用的。初始时为全-1.
2. 两个fd_set 类型的描述符集,一个用做监听描述符集,一个用作连接描述符集。
例如下面的这张图:
图6-17是第一个客户建立连接后 client 数据结构和rset数据结构的变换情况,
1). 在初始时,client数组中的所有元素都是-1,rset只在fd3处有一个监听套接字的描述符。fd0,fd1,fd2分别用做stdin,stdout,strerror,所以监听套接字在fd3,
2). 当监听套接字满足之后建立和客户端的连接,假设此时accept返回的新已连接描述符是4,那么可以看到rset中fd4被置1,
3). 并且client[0]写入4,注意client中并不保存监听套接字的描述符,所以第一个可用位是0号。
4). 之后服务器进程便通过搜索client中不为0的数,并将他们取出作为套接字进行处理。
5). 如果后面连接中断或者释放,那么client[0]重新置为-1.
6). 在图6-16中有服务器监听套接字和描述符的集合。我认为原图有点问题,所以做了下修改。
我们仍然以https://github.com/michaelnju/UNPV-Relaxing-Code/blob/master/Chaper5_Echo_example/echo_tcp_server.c 这个回射程序为例,看一看select是怎么实现并发服务器的,下面是Richard老先生原程序的relax版本。
/*Relaxed by Lingtao in 2015/04 */ //#include "unp.h" #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/select.h> #include <sys/time.h> #define LISTENQ 5 #define MAXLINE 2048 #define SERV_PORT 9877 typedef struct sockaddr SA; int main(int argc, char **argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; ssize_t n; fd_set rset, allset; char buf[MAXLINE]; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); listen(listenfd, LISTENQ); maxfd = listenfd; /* initialize */ maxi = -1; /* index into client[] array */ for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; /* -1 indicates available entry */ FD_ZERO(&allset); FD_SET(listenfd, &allset); for ( ; ; ) { rset = allset; /* structure assignment */ nready = select(maxfd+1, &rset, NULL, NULL, NULL); if (FD_ISSET(listenfd, &rset)) { /* new client connection */ clilen = sizeof(cliaddr); connfd = accept(listenfd, (SA *) &cliaddr, &clilen); #ifdef NOTDEF printf("new client: %s, port %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL), ntohs(cliaddr.sin_port)); #endif for (i = 0; i < FD_SETSIZE; i++) if (client[i] < 0) { client[i] = connfd; /* save descriptor */ break; } if (i == FD_SETSIZE) { perror("too many clients"); exit(1); } FD_SET(connfd, &allset); /* add new descriptor to set */ if (connfd > maxfd) maxfd = connfd; /* for select */ if (i > maxi) maxi = i; /* max index in client[] array */ if (--nready <= 0) continue; /* no more readable descriptors */ } for (i = 0; i <= maxi; i++) { /* check all clients for data */ if ( (sockfd = client[i]) < 0) continue; if (FD_ISSET(sockfd, &rset)) { if ( (n = read(sockfd, buf, MAXLINE)) == 0) { /*4connection closed by client */ close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1; } else write(sockfd, buf, n); if (--nready <= 0) break; /* no more readable descriptors */ } } } }
Line 21定义了nready, client[FD_SETSIZE];其中client用来存储socket id , nready用来作为select的返回值即继续描述符的个数。
Line23 定义了fd_set 类型的 cliaddr, servaddr,用来存储监听套接字和连接套接字。
Line 28 -44 初始化client数组,并建立监听套接字放入allset描述符。
Line47,48 select阻塞等待就绪条件。
Line50-76行的if语句用来处理有新的socket建立时,进行相应存储以及对应字段的更新。
Line78-93的for循环用来处理每一个socket连接。
下面是我们编译运行程序,然后使用ps 查看,发现尽管已经建立连接,但是父进程仍然 只有一个。