非阻塞式I/O

概述

套接字的默认状态是阻塞的。这就意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待响应操作完成。可能阻塞的套接字调用可分为以下四类:

  1. 输入操作,包括readreadvrecvrecvfromrecvmsg共5个函数。如果某个进程对一个阻塞的TCP套接字(默认设置)调用这些函数之一,而且该套接字的接收缓冲区中没有数据可读,该进程将被投入睡眠,直到有一些数据到达。如果想等到有固定数目的数据可读位置,那么可以调用readn函数或者指定MSG_WAITALL标志;而对于非阻塞的套接字,如果输入操作不能被满足,相应调用将立即返回一个EWOULDBLOCK错误;
  2. 输出操作,包括writewrtievsendsendtosendmsg共5个函数。内核将从应用进程的缓冲区到该套接字的发送缓冲区复制数据。对于阻塞的套接字,如果其发送缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。对于一个非阻塞的套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个EWOULDBLOCK错误。如果其发送缓冲区有一些空间,返回值将是内核能够复制到该缓冲区的字节数;
  3. 接受外来连接,即accept函数。如果对一个阻塞的套接字调用该函数,并且无新的连接到达,调用进程将被投入睡眠。如果对一个非阻塞的套接字调用accept函数,并且尚无新的连接到达,accept调用将立即返回一个EWOULDBLOCK错误;
  4. 发起外出连接,即调用TCP的connect函数(UDP使用connect,不能使一个“真正”的连接建立起来,它只是使内核保存对端的IP地址和端口号)。TCP连接经历“三次握手”过程,并且connect函数一直要等到客户收到对于自己的SYN的ACK为止才返回。若对一个非阻塞的TCP套接字调用connect,并且连接不能建立,那么连接的建立照样发起,不过会返回一个EINPROGRESS错误;

 

非阻塞读和写:str_cli函数

我们维护这两个缓冲区:

  1. to:容纳从标准输入到服务器去的书;
  2. fr:容纳来自服务器到标准输出来的数据;

下图展示了to缓冲区的组织和指向该缓冲区中的指针:

下图展示了fr缓冲区相应的组织:

str_cli的第一部分:

#include "unp.h"

void str_cli(FILE *fp, int sockfd){
    int macfdpl, val, stdineof;
    ssize_t n, nwritten;
    fd_set rset, wset;
    char to[MAXLINE], fr[MAXLINE];
    char *toiptr, *tooptr, *friptr, *froptr;
    
    //fcntl把所用3个描述符都设置为非阻塞,包括林艾尔到服务器的套接字、标准输入和标准输出
    val = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, val | O_NONBLOCK);

    val = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);

    val = fcntl(STDOUT_FILENO, F_GETFL, 0);
    fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);

    //初始化两个缓冲区的指针,并把最大描述符号+1,用作select的第一个参数
    toiptr = tooptr = to;
    friptr = froptr = fr;
    stdineof = 0;

    maxfdpl = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
    for( ; ; ){
        FD_ZERO(&rset);
        FD_ZERO(&wset);
        if(stdineof == 0 && toiptr < &to{MAXLINE})
            FD_SET(STDIN_FILENO, &rset);
        if(friptr < &fr[MAXLINE])
            FD_SET(sockfd, &rset);
        if(tooptr != toiptr)
            FD_SET(sockfd, &wset);
        if(froptr != friptr)
            FD_SET(STDOUT_FILENO, &wset);

        select(maxfdpl, &rset, &wset, NULL, NULL);

        if(FD_ISSET(STDIN_FILENO, &rset)){
            if((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0){
                if(errno != EWOULDBLOCK)
                    err_sys("read error on stdin");
            }else if(n == 0){
                fprintf(stderr, "%s: EOF on stdin\n",gf_time());
                stdineof = 1;
                if(tooptr == toiptr)
                    shutdown(sockfd,SHUT_WR);
            }else{
                fprintf(stderr, "%s: read %d bytes from stdin\n",gf_time(), n);
                toiptr += n;
                FD_SET(sockfd, &wset);
            }
        }
        
       if(FD_ISSET(sockfd, &rset)){
            if((n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0){
                if(errno != EWOULDBLOCK)
                    err_sys("read error on socket");
            }else if(n == 0){
                fprintf(stderr, "%s: EOF on socket\n",gf_time());
                if(stdineof)
                    return ;
                else
                    err_quit("str_cli: server terminated prematurely");
            }else{
                fprintf(stderr, "%s: read %d bytes from socket\n",gf_time(), n);
                friptr += n;
                FD_SET(sockfd, &wset);
            }
        }

        if(FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0)){
            if((nwritten = write(STDOUT_FILENO, froptr, n)) < 0){
                if(errno != EWOULDBLOCK)
                    err_sys("write error on stdin");
            }else{
                fprintf(stderr, "%s: wrote %d bytes from stdin\n",gf_time(), nwritten);
                froptr += nwritten;
                if(froptr == friptr)
                    froptr = friptr = fr;
            }
        }
    
        if(FD_ISSET(scokfd, &wset) && ((n = toiptr - tooptr) > 0)){
            if((nwritten = write(sockfd, tooptr, n)) < 0){
                if(errno != EWOULDBLOCK)
                    err_sys("write error on socket");
            }else{
                fprintf(stderr, "%s: wrote %d bytes from stdin\n",gf_time(), nwritten);
                tooptr += nwritten;
                if(tooptr == toiptr){
                    tooptr = toiptr = to;
                    if(stdineof)
                        shutdown(sockfd, SHUT_WR);
                }
            }
        }
    }
}
  • 非阻塞connect

当在一个非阻塞TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的TCP三路握手继续进行。我们接着使用select检测这个连接或成功或失败的已建立条件。非阻塞的connect的三个用途是:

  1. 可以把三路握手叠加在其他处理上。完成一个connect需要一个RTT,而RTT根据环境的不同波动比较大,所以波动的这段时间内我们可以执行我们想要处理的工作;
  2. 可以使用这个技术同时建立多个连接;
  3. 既然使用select等待连接的建立,我们可以给select指定一个时间限制,使得我们能够缩短connect的超时;

非阻塞connect需要我们处理的细节:

  1. 尽管套接字是非阻塞的,如果连接到的服务器在同一主机上,那么当我们调用connect时,连接通常立刻建立;
  2. 源自Berkeley的实现(和POSIX)有关于select和非阻塞connect的以下两个规则:
    1. 当连接成功建立时,描述符变为可写;
    2. 当连接建立遇到错误时,描述符变为既可读又可写;

 

  • 非阻塞accept

当有一个已完成的连接准备好被accept时,select将作为可读描述符返回该连接的监听套接字。因此,如果我们使用select在某个监听套接字上等待一个外来连接,那就没有必要把该监听套接字设置为非阻塞,这是因为如果select告诉我们该套接字上已有连接就绪,那么随后的accept调用不应该阻塞。

不幸的是,这里存在一个可能让我们掉入陷阱的定时问题。问题例子如下:

#include "unp.h"

int main(int argc, char **argv){
    int sockfd;
    struct linger ling;
    struct sockaddr_in servaddr;

    if(argc != 2)
        err_quit("usage: tcpcli <IPaddress>");
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

    //这样设置导致连接被关闭时在TCP套接字上发送一个RST
    ling.l_onoff = 1;
    ling.l_linger = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
    close(sockfd);

    exit(0);
}

我们在TCP回射程序中添加如下:

if(FD_ISSET(listenfd, &rset)){
    printf("listening socket readable"); //+
    sleep(5):                            //+
    clilen = sizeof(cliaddr);
    connfd = accept(listenfd, (SA *) &cliaddr, &cliaddr);
}

上述代码模拟了一个繁忙的服务器,它无法在select返回监听套接字的可读条件后就马上调用accept。通常情况下服务器的这种迟钝不成问题(实际上这就是要维护一个已完成连接队列的原因),但是结合上连接建立之后到达的来自客户的RST,问题就出现了。

我们知道当客户在服务器调用accept之前中止某个连接时,源自Berkeley的实现不把这个中止的连接返回给服务器,而其他实现应该返回ECONNABORTED错误,却往往代之以返回EPROTO错误。考虑一个源自Berkeley的实现上的如下例子:

  1. 客户建立一个连接并随后中止它;
  2. select向服务器进程返回可读条件,不过服务器要过一小段时间才调用accept;
  3. 在服务器从select返回到调用accept期间,服务器TCP接收到来自客户的RST;
  4. 这个已完成的连接被服务器TCP驱除出队列,我们假设队列中没有其他已完成的连接;
  5. 服务器调用accept,但是由于没有任何已完成的连接,服务器于是阻塞;

【解决办法】

  1. 当使用select获悉某个监听套接字上何时有已完成连接准备好被accept时,总是把这个监听套接字设置为非阻塞;
  2. 在后续的accept调用中忽略一下错误:
    1. EWOULDBLOCK(Berkeley实现,客户中止连接时);
    2. ECONNABORTED(POSIX实现,客户中止连接时);
    3. EPROTO(SVR4实现,客户中止连接时);
    4. EINTR(如果有信号被捕捉);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值