• 4.I/O复用以及基于I/O复用的回射客户端/服务器


      I/O复用:当一个或多个I/O条件满足时,我们就被通知到,这种能力被称为I/O复用。

    1.I/O复用的相关系统调用

      posix的实现提供了select、poll、epoll两类系统调用以及相关的函数来实现I/O复用。select以及相关联的函数如下所示:

    #include <sys/select.h>
    /* 功能:监听多个fd,等待指定的fd指定的事件发生或者超时。
     * nfds: 最大描述符加1。
     * readfds:监听读fd集合。
     * writefds:监听写fd集合。
     * exceptfds:监听出错fd集合。
     * timeout:超时时间,null-select一直阻塞直到指定时间发生,0-select不阻塞立刻返回。
     * 返回值: 正常-返回可用fd数量,0-超时返回, -1-select调用出错,errno被设置。
     */
    
    int select(int nfds, fd_set *readfds, fd_set *writefds,
                      fd_set *exceptfds, struct timeval *timeout);
    
    /* 功能: 清除set中fd对应的位。
     * 返回值:无。
     */              
    void FD_CLR(int fd, fd_set *set);
    /* 功能: 判断set中fd对应的位是否被设置。
     * 返回值:0-表示没有被设置,1-表示已经被设置。
     */    
    int  FD_ISSET(int fd, fd_set *set);
    /* 功能: 设置set中fd对应的位。
     * 返回值:无。
     */    
    void FD_SET(int fd, fd_set *set);
    /* 功能: set置空。
     * 返回值:无。
     */    
    void FD_ZERO(fd_set *set);

      值得注意的是,不同的posix实现支持的最大fd_set不用,使用大描述字集应小心。另外Posix.1g还提供了系统调用pselect,如下:

    #include <sys/select.h>
    
    int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                fd_set *exceptfds, const struct timespec *timeout,
                const sigset_t *sigmask);

      pselect与select存在两点不同:1、pselect支持更细粒度的超时时间(纳秒级别),而select支持微妙级别的超时时间。2、支持第六个参数,pselect调用允许传入一个信号掩码,在pselect调用期间进程的信号掩码是由sigmask指定,pselect返回前将进程的信号掩码恢复为之前的。

      函数poll起源于SVR3,开始用于流设备上,现在支持任何类型的fd。poll以及相关数据结构:

    #include <poll.h>
    /* 功能:监听多个fd,等待感兴趣的事件发生或者超时。
     * fds:pollfd类型变量的集合。
     * nfds: 最大描述符加1。
     * timeout:超时时间,INFTIM-poll一直阻塞直到指定时间发生,0-select不阻塞立刻返回。
     * 返回值: 正常-返回可用fd数量,0-超时返回, -1-调用出错,errno被设置。
     */
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    
    struct pollfd {
        int   fd;         /* 文件描述符 */
        short events;     /* 对于fd,感兴趣的事件,值参数 */
        short revents;    /* 对于fd,发生的事件,结果参数 */
    };

    指定events和测试revents用的一些常值:

      1.用于读测试:{POLLIN:普通或优先级带数据可读、POLLRDNORM:普通数据可读、POLLRDBAND:优先级带数据可读}。

      2.用于写测试:{POLLOUT:普通数据可写、POLLWRNORM:同POLLOUT、POLLWRBAND:优先级带数据可写}。

      3.用于出错(仅用于测试revent):{POLLNVAL:描述字不是一个打开的文件、POLLHUP:被挂起、POLLERR:发生错误}。

      linux在2.6内核以后加入了epoll的支持,在高性能服务器领域得到了广泛的使用。epoll的使用较为复杂,再次暂略过。select、poll、epoll的比较:

      1.select ,对于几乎所有的平台都支持select,但是select存在两个缺点:a、可监听的描述符数量有限制。b、监听的描述符数量增大时,会影响程序的效率,三个描述符集都是值-结果的参数,存在两次内核-用户态之间的数据拷贝,select返回后需要遍历整个fd集合,才能知道是哪一个fd就绪。

      2.poll,poll克服的select对描述符数量的限制,但当描述符数量增大时,仍存在效率问题。

      3.epoll,即克服了对描述符数量的限制,同时又通过回调函数以及mmap的方式,克服了描述符数量增大时效率的问题,但是epoll的使用较为复杂。

      因此当监听的fd数量较少时,采用select或poll较好,当监听的fd数量较多时采用epoll较合适。

    2.基于I/O复用的回射客户端

      前面的回射客户端存在一个问题:当客户端阻塞与从标准输入读时,服务器的断开不能被客户端立刻感知,造成这个问题的原因是客户端需要同时监听两个I/O:sock和标准输入,不能因为某一个阻塞而当另一个就绪时不被感知。I/O多路复用可解决这一个问题。

    void
    str_cli(FILE *fp, int sockfd)
    {
        int            maxfdp1, stdineof = 0;
        fd_set        rset;  /* 监听的可读描述符集合 */
        char        sendline[MAXLINE], recvline[MAXLINE];
    
        FD_ZERO(&rset); /* rset清空 */
        for ( ; ; ) {
            if (stdineof == 0)
                FD_SET(fileno(fp), &rset); /* 设置标准输入fd对应的位 */
            FD_SET(sockfd, &rset);   /* 设置sockfd对应的位 */
            maxfdp1 = max(fileno(fp), sockfd) + 1; /* 计算监听的最大描述符 */
            if (select(maxfdp1, &rset, NULL, NULL, NULL) < 0) { /* select开始监听 */
                if (errno == EINTR)
                    continue;
                else
                    err_sys("select error");
            }
    
            if (FD_ISSET(sockfd, &rset)) {    /* sockfd变为可读 */
                if (Readline(sockfd, recvline, MAXLINE) == 0) {
                    if (stdineof == 1)
                        return;        /* sockfd写已经关闭,属于正常的结束 */
                    else
                        err_quit("str_cli: server terminated prematurely");
                }
    
                Writen(STDOUT_FILENO, recvline, strlen(recvline));
            }
    
            if (FD_ISSET(fileno(fp), &rset)) {  /* 标准输入可读 */
                if (Fgets(sendline, MAXLINE, fp) == NULL) { /* 读到结束符 */
                    stdineof = 1; /* 置1,说明已经关闭了sockfd写的一端 */
                    Shutdown(sockfd, SHUT_WR); /* 关闭sockfd写的一端,tcp发送fin */
                    FD_CLR(fileno(fp), &rset); /* 已经读到结束符,只有不需要再监听其变为可读 */
                    continue;
                }
    
                Writen(sockfd, sendline, strlen(sendline));
            }
        }
    }

      上面的程序中,值得注意的是当我们在输入I/O中读到结束符后,函数并没有立刻退出,而是shutdown关掉sock写的一端,因为之后可能还有数据从服务器端回射回来,因此继续从sock中读数据,直到在sock读到结束符。

    3.基于I/O复用的回射服务端

       

    int
    main(int argc, char **argv)
    {
        int                    i, maxi, listenfd, connfd, sockfd;
        int                    nready;
        ssize_t                n;
        char                buf[MAXLINE];
        socklen_t            clilen;
        struct pollfd        client[OPEN_MAX];
        struct sockaddr_in    cliaddr, servaddr;
        /* 创建并启动监听sock */
        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);
    
        client[0].fd = listenfd;  /* 初始化监听sock的pollfd结构 */
        client[0].events = POLLRDNORM; 
        for (i = 1; i < OPEN_MAX; i++)
            client[i].fd = -1;        /* -1 indicates available entry */
        maxi = 0;                    /* max index into client[] array */
        
        for ( ; ; ) {
            nready = Poll(client, maxi+1, INFTIM);
    
            if (client[0].revents & POLLRDNORM) {    /* 新的客户连接被创建,listenfd变为可读 */
                clilen = sizeof(cliaddr);
                connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
    #ifdef    NOTDEF
                printf("new client: %s
    ", Sock_ntop((SA *) &cliaddr, clilen));
    #endif
                for (i = 1; i < OPEN_MAX; i++)
                    if (client[i].fd < 0) {
                        client[i].fd = connfd;    /* 保存新的客户连接sockfd */
                        break;
                    }
                if (i == OPEN_MAX)
                    err_quit("too many clients");
    
                client[i].events = POLLRDNORM;
                if (i > maxi)
                    maxi = i;                /* 更新maxi */
    
                if (--nready <= 0)
                    continue;                /* 已经不存在就绪的fd,继续poll监听 */
            }
    
            for (i = 1; i <= maxi; i++) {    /* 遍历所有的监听fd,判断其是否就绪 */
                if ( (sockfd = client[i].fd) < 0) /* 未使用的pollfd结构 */
                    continue;
                if (client[i].revents & (POLLRDNORM | POLLERR)) { 
                    if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
                        if (errno == ECONNRESET) {
                                /* 连接被重置 */
    #ifdef    NOTDEF
                            printf("client[%d] aborted connection
    ", i);
    #endif

    Close(sockfd); client[i].fd = -1; } else err_sys("read error"); } else if (n == 0) { /* 连接被关闭 */ #ifdef NOTDEF printf("client[%d] closed connection ", i); #endif Close(sockfd); client[i].fd = -1; } else Writen(sockfd, buf, n); /* 已读到数据,写到sockfd,回射给客户 */ if (--nready <= 0) break; /* 已经不存在就绪的fd,继续返回poll监听 */ } } } }

     

  • 相关阅读:
    terminal
    变量提升、函数提升
    ssh传输文件
    mocha测试框架
    npm-run 自动化
    webpack
    浅析babel
    构建工具gulp
    C++中TRACE宏及assert()函数的使用
    memcpy函数-C语言
  • 原文地址:https://www.cnblogs.com/VincentXu/p/3331742.html
Copyright © 2020-2023  润新知