首页 > 代码库 > Select单进程非阻塞TCP echo服务器

Select单进程非阻塞TCP echo服务器

Select单进程非阻塞TCP echo服务器

 

1. select 描述

 

  #include <sys/select.h>

  #include <sys/time.h>

  int select( int maxfdp1,   fd_set *readset,  fd_set *writeset,  fd_set *exceptset,  const struct timeval *timeout);

        返回:正确:就绪描述符数目;超时: 0;出错: -1

  参数说明:

  timeout: 内核等待指定描述符集合中的任意一个就绪的时间。

                     struct timeval {

                          long tv_sec;    //seconds

                          long tv_usec;    //microseconds
                      }; 

                      timeout = NULL:  永远等待下去:仅在有一个描述符准备好I/O才返回。

                      timeout != NULL: 等待timeval中指定的时间返回。

                      timeout != NULL, timeval == 0 : 不等待:检查描述符后立即返回。这称为轮循(polling);

                      前两种情况会被信号中断,并从信号处理程序返回。   

       timeout为const表示函数返回时不会修改timeval,但是有的linux版本会修改timval值。

                      考虑到可移植性,每次都应对timeout进行初始化。  

 

  readset, writeset, exceptset:指定我们让内核测试读写异常条件的描述符:

                     可读:

                     (1) 套接字缓冲区数据字数 >= 套接字接受缓冲区低水位标记的当前大小。 不会阻塞,返回>0的值(准备好读入的数据)。

                           可用SO_RCVLOWAT套接字选项设置标志,TCP和UDP默认1。所以read时可能返回1字节数据,需要反复读。

                     (2) 接受到FIN,读操作不阻塞返回0(EOF)。

                      (3) 监听套接字已完成的连接数不为0,新的连接到达。accept不阻塞。

                       (4) 有一个套接字错误要处理。读不阻塞返回-1。设置errno

 

                     可写:

                     (1) 发送缓冲区可用空间字节数 >= 最低水位。写不阻塞返回>0(例如:传输层接受的字节数)。 TCP和UDP最低水位2048。

                     (2) 连接的写半部关闭,对这样的套接字写产生SIGPIPE信号。

                     (3) 非阻塞的connect套接字已建立连接,或者connect已失败告终。

                     (4) 有一个套接字错误带处理。写不阻塞返回-1.设置errno。

 

                     异常:带外数据到达。

 

  maxfdp1: 指定待测试的描述符个数,为最大描述符 + 1, 从0开始。

 

2. select 宏和相关说明

 

  select使用描述符集fd_set,通常为一个整数数组,其中每个整数中的每一位对应一个描述符。

       4个宏设置fd_set:

  void FD_ZERO(fd_set *fdset);                      //清空fdset中所有位

       void FD_SET(int fd, fd_set *fdset);                    //fd_set中fd位置位

       void FD_CLR(int fd, fd_set *fdset);                    //fd_set中fd位复位

       void FD_ISSET(int fd, fd_set *fdset);                 //fd_set中fd是否置位

 

  select中的readset,writeset,exceptset = NULL,表示我们不关心这个。select会修改这三个描述符集和,时值——结果参数。

调用select时指定我们关心的描述符值返回时指示那些已经就位,未就位的置0。因此每次调用select时都需要把我们关心的描述符置1

 

3. select 缺陷

 

    #include <sys/types.h> 定义了FD_SETSIZE通常为1024。当描述符超出时,可用poll解决。

通过修改内核的FD_SETSIZE大小,需要重新编译内核,可能带来扩展性问题,不推荐使用。

 

4. 一个echo服务器程序示例

 

#include "unp.h"

int main(int argc, char **argv)
{
    if (argc != 2) {
        err_quit("Usage: a.out <#port>");
    }

    int listenfd = Tcp_listen(NULL, argv[1], NULL);
    int flags = Fcntl(listenfd, F_GETFL, 0);
    Fcntl(listenfd, F_SETFL, flags | O_NONBLOCK);

    //最大文件描述符
    int maxfd = listenfd;

    //连接的客户端描述符
    int client[FD_SETSIZE];
    memset(client, -1, FD_SETSIZE);

    //client已使用的描述符最大下标
    int maxi = -1;

 //要监听的描述符集合
    fd_set allset;
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    for (;;) {

        //select会修改描述符集合,每次重新初始化
        fd_set rset = allset;
        DPRINTF("Start to select()");

        //select阻塞到可读
        int nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);

  //监听描述符可读       

        if (FD_ISSET(listenfd, &rset)) {
            DPRINTF("Start to accept a connection");
            //int connfd = Accept(listenfd, NULL, NULL);
            //非阻塞方式忽略若干错误
            int connfd = accept(listenfd, NULL, NULL);
            DPRINTF("connfd %d", connfd);
            if (connfd < 0) {
#ifdef EPROTO
                DPRINTF("EPROTO");
                if (errno == EINTR || errno == EWOULDBLOCK ||
                    errno == ECONNABORTED || errno == EPROTO) {
#else
                if (errno == EINTR || errno == EWOULDBLOCK ||
                    errno == ECONNABORTED) {
#endif        
                    continue;
                }  else {
                    err_sys("accept() failed");
                }
            }
            DPRINTF("Accept a connection");

            //设置非阻塞
            int flags = Fcntl(connfd, F_GETFL, 0);
            Fcntl(connfd, F_SETFL, flags | O_NONBLOCK);

            int i = 0;
            while (i < FD_SETSIZE) {
                if (client[i] < 0) {
                    client[i] = connfd;
                    break;
                }
                ++i;
            }
            if (i == FD_SETSIZE) {
                err_quit("Too many client");
            }
            FD_SET(connfd, &allset);
            if (connfd > maxfd) {
                maxfd = connfd;
            }
            if (i > maxi) {
                maxi = i;
            }

           //检测是否还有准备好的连接
            if (--nready <= 0) {
                continue;
            }
        } //end if(FD_ISSET)


       //循环检测已连接的客户端
        DPRINTF("maxi = %d", maxi);
        for (int i = 0; i <= maxi; ++i) {
            int sockfd = client[i];
            if (sockfd < 0) {
                continue;
            }
            DPRINTF("client[]: i %d, sockfd %d", i, sockfd);
            if (FD_ISSET(sockfd, &rset)) {
                char line[MAXLINE];
                int n;
again:
                while ((n = read(sockfd, line, MAXLINE)) > 0) {
                    Write(sockfd, line, n);
                }
                if (n < 0 && errno == EINTR) {
                    goto again;
                } else if (n < 0 && errno == EWOULDBLOCK) {
                    ;
                } else if (n <= 0) {        //0: FIN, close by client; -1: RST, error
                    Close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;    
                    DPRINTF("read n = %d", n);
                    if (n < 0) {
                        perror("n < 0");
                    }
                }
            }

            if (--nready <= 0) {
                break;
            }
        } //end for(0..maxi)
    } //end for(;;)

    return 0;
}