在上一文中 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 类型的描述符集,一个用做监听描述符集,一个用作连接描述符集。
例如下面的这张图:
我们仍然以https://github.com/michaelnju/UNPV-Relaxing-Code/blob/master/Chaper5_Echo_example/echo_tcp_server.c 这个回射程序为例,看一看select是怎么实现并发服务器的,下面是Richard老先生原程序的relax版本。
/*Relaxed by Lingtao in 2015/4*/ //#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); /* end fig01 */ /* include fig02 */ 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 */ } } } } /* end fig02 */
关于这段代码里的详细解释,后面会给出。
原文地址:http://blog.csdn.net/michael_kong_nju/article/details/44906461