1. 使用select改写tcp服务器

在此之前,回顾一下多进程并发型服务器通信过程,并发型服务器的做法是针对每一个客户端请求,服务器的父进程就fork创建一个子进程来处理客户端的请求。如果有大量的客户端请求的话,那么服务器也需要创建大量的子进程来处理请求,这将会消耗服务器大量的系统资源。

 

因此我们可以使用select改写tcp服务器,通过select来处理多个客户端的请求,具体处理过程如下:

14-改写tcp服务器
图1

在客户端连接服务器之前,服务器只创建了单个文件描述符进行监听,在图1我们用一个方块表示。

服务器只维护一个读文件描述符集合rest,当服务器启动时描述符0,1,2分别表示标准输入,标准输出,标准出错,且这三个文件描述符被置为0,而服务器监听的描述符3被置为1,表示描述符3处于监听。而client数组中则记录每个客户端连接的描述符,开始时会将client数组初始化为-1,FD_SETSIZE代表服务器处理的最大客户端连接的数量,即client数组的大小。

14-改写tcp服务器
图2

 

在rest数组中,描述符3处于监听,因此select中的maxfd参数就是4。当第一个客户端与服务器建立tcp连接时,监听的描述符会变为可读,随后服务器调用accept,假设accept返回已连接的新描述符值是4,如图3所示:

14-改写tcp服务器
图3

 

那么在client数组中必须记录新的已连接描述符的值,同时把描述符4加入到rest集合中:

14-改写tcp服务器
图4

 

 

接着第二个客户端与服务器建立tcp连接:

14-改写tcp服务器
图5

 

同理,新的已连接描述符5也需要在client数组中记录,同时把描述符5加入到rest集合中去:

14-改写tcp服务器
图6

 

 

如果第一个客户端发送了FIN终止了tcp连接,那么服务器中的描述符4将会变的可读,当服务器读取描述符4将会返回0,于是可以关闭该套接字并更新rest集合将描述符4置为0,同时将client数组中client[0]的值置为-1,需要注意maxfd的值不变。

14-改写tcp服务器
图7

 

当有新的客户端建立tcp连接时,就可以在client数组中的第一项记录新的已连接描述符,并将新的已连接描述符添加到rest集合中。变量maxi是client数组中当前使用项的最大下标,变量maxfd(加1之后)则表示select函数的第一个参数的值。

 

2. 服务器程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 1024
#define SERV_PORT 10001

int main(void)
{
        int i, n ,maxi, maxfd, listenfd, connfd, sockfd;
        int nready;
        int client[FD_SETSIZE];

        fd_set rset, allset;

        char buf[MAXLINE];
        char str[INET_ADDRSTRLEN];

        socklen_t cliaddr_len;
        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, (struct sockaddr *)&servaddr, sizeof(servaddr));
        listen(listenfd, 20);

        //初始化maxfd
        maxfd = listenfd; 

        //client数组的下标
        maxi = -1;

        //初始化client数组
        for (i = 0; i < FD_SETSIZE; i++){
                client[i] = -1; 
        }

        FD_ZERO(&allset);
        FD_SET(listenfd, &allset); /* 构造select检测文件描述符集 */

        while(1){
                /* 每次循环时都重新设置select检测的描述符集合 */
                rset = allset;
                nready = select(maxfd+1, &rset, NULL, NULL, NULL);
                if (nready < 0)
                        perror("select error:");

                /* 判断是否有新的客户端连接建立完成 */
                if (FD_ISSET(listenfd, &rset)) {
                        cliaddr_len = sizeof(cliaddr);
                        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
                        printf("received from %s at PORT %d\n",
                                inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
                                ntohs(cliaddr.sin_port));

                        //在client数组中必须记录新的已连接描述符的值
                        for (i = 0; i < FD_SETSIZE; i++) {
                                if(client[i] < 0){
                                        client[i] = connfd;
                                        break;
                                }
                        }

                        /* 判断是否达到select能监控的文件个数上限 1024 */
                        if(i == FD_SETSIZE){
                                puts("too many clients");
                                exit(1);
                        }

                         /* 添加一个已连接的新描述符到监控信号集里监听 */
                         FD_SET(connfd, &allset); 

                        /* 更新maxfd为最大描述符的值 */
                        if(connfd > maxfd)
                                maxfd = connfd;

                        /* 一旦i大于maxi的话,同时更新client数组中当前使用项的最大下标 */
                        if(i > maxi)
                                maxi = i;

                         /* 如果nready为0,说明I/O事件处理完毕 */
                        /* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,负责处理未处理完的就绪文件描述符 */
                        if (--nready == 0)
                                continue;

                }


                /* 检测哪个clients有数据就绪 */
                for (i = 0; i <= maxi; i++) {
                        if ( (sockfd = client[i]) < 0)
                                continue;

                        if (FD_ISSET(sockfd, &rset)) {
                                //读取数据,有可能会读到0
                                if ( (n = read(sockfd, buf, MAXLINE)) == 0) {
                                        /* 当client关闭链接时,服务器端也关闭对应链接 */
                                        close(sockfd);
                                        /* 取消select监听该文件描述符 */
                                        FD_CLR(sockfd, &allset);
                                        /* client数组中置为-1 */
                                        client[i] = -1;
                                }else{
                                         //读到数据后就处理数据
                                         int j;
                                        for (j = 0; j < n; j++)
                                                buf[j] = toupper(buf[j]);
                                        write(sockfd, buf, n);
                                }

                                /* 判断select返回的I/O事件是否处理完毕 */
                                if (--nready == 0)
                                        break;
                        }

                }
                  
        }
        close(listenfd);
        return 0;
}

 

程序执行结果:

14-改写tcp服务器

 

相关文章: