非阻塞式socket

引言

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

( 1 ) 输入操作,包括read 、 readv、 recv、 recvfrom和 recvmsg共5个函数。如果某个进程对一个阻塞的TCP套接字(默认设置)调用这些输入函数之,而且该套接字的接收缓冲区中没有数据可读,该进程将被投入睡眠,直到有一些数据到达。既然TCP是字节流协议,该进程的唤醒就是只要有一些数据到达,这些数据既可能是单个字节,也可以是一个完整的TCP分节中的数据。如果想等到某个固定数目的数据可读为止,那么可以调用我们的readn函数或者指定MSG_WAITALL标志。
对于非阻塞的套接字,如果输入操作不能被满足(对于TCP套接字即至少有一个字节的数据可读,对于UDP套接字即有一个完整的数据报可读),相应调用将立即返回一个EWOULDBLOCK错误。重新再调用一次,系统通过这种错误码,提示程序员

(2) 输出操作,包括write、writev、send、sendto和sendmsg共5个函数。对于一个TCP套接字,内核将从应用进程的缓冲区到该套接字的发送缓冲区复制数据(数据从用户空间复制到内核发送缓冲区即返回成功,至于啥时候发送出去,这是内核的事情,不由我们控制)。对于阻塞的套接字,如果内核发送缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。对于一个非阻塞的TCP套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回EWOULDBLOCK错误。如果其发送缓冲区中仅仅有一些空间,返回值将是内核能够复制到该缓冲区中的字节数,也叫做不足计数。

(3)接受外来连接,即accept函数。如果对一个阻寨的套接字调用accept函数,并且尚无新的连接到达,调用进程将被投入睡眠。如果对一个非阻塞的套接字调用accept函数,并且尚无新的连接到达,accept调用将立即返回一个EWOULDBLOCK错误。

(4) 发起外出连接,及用于TCP的connect函数,TCP连接的建立涉及一个三路握手过程,而且connect函数一直要等到客户收到对于自己的SYN的ACK为止才返回。这意味者TCP的每一次connect总会阻塞其调用进程至少一个到服务器的RTT时间。对于一个非阻塞套接字调用connect,并且连接不能立即建立,连接的建立能照样发起,不过会立刻返回一个EINPROGRESS(在处理中的错误)错误。注意有些连接可以立即建立,通常发生在服务器和客户机处于同一个主机的情况下。因此即使对于一个非阻塞的connect ,我们也得预备connect成功返回的情况发生。

对于以上系统调用,Nginx里面都是通过非阻塞处理的,所以处理非阻塞是非常重要的地方。

非阻塞connect,实现异步的connect

当在一个非阻塞的TCP套接字上调用connect时。connect将立即返回一个EINPROGRESS错误(建立启动但是尚未完成),不过己经发起的TCP三路握手继续进行,我们接着使用 select设定对应的超时时间去检测这个连接成功或者失败即可。非阻塞的connect有三个用途:

(1) 我们可以把三路握手叠加在其他处理上。完成一个connect要花一个RTT时间。而RTT波动范围很大,从局域网上的几个毫秒到几百个毫秒甚至是广域网上的几秒。这段时间内也许有我们想要执行的其他处理工作可执行。

(2) 我们可以使用这个技术同时建立多个连接。WEB浏览器都使用这种模式,包括Nginx也是这样。后期重点关注这里。

(3) 既然使用select等待连接的建立,我们可以给select指定一个时间限制,使得我们能够缩短connect的超时。

重点关注的细节:

  • 尽管套接字是非阻塞的,如果连接到的服务器在同一个主机上,那么当调用connect时,连接通常立刻建立,必须处理这种情形。
  • 关于select和非阻塞connect的以下两个规则:(1)当连接成功建立时,描述符变为可写(2) 当连接建立遇到错误时,描述符变为既可读又可写
#include    "unp.h"
int connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
    int             flags, n, error;
    socklen_t       len;
    fd_set          rset, wset;
    struct timeval  tval;

    flags = Fcntl(sockfd, F_GETFL, 0);//获取原始sockfd属性
    Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);//添加非阻塞

    error = 0;
    if ( (n = connect(sockfd, saptr, salen)) < 0)//返回值,小于0
        if (errno != EINPROGRESS)//出错了
            return(-1);

    /* Do whatever we want while the connect is taking place. */

    if (n == 0)//本机,直接连接成功
        goto done;  /* connect completed immediately */

    FD_ZERO(&rset);
    FD_SET(sockfd, &rset);
    wset = rset;
    tval.tv_sec = nsec;
    tval.tv_usec = 0;

    if ( (n = Select(sockfd+1, &rset, &wset, NULL,
                     nsec ? &tval : NULL)) == 0) {
        close(sockfd);      /* timeout */
        errno = ETIMEDOUT;
        return(-1);
    }//阻塞与select调用,如果返回0,则超时发生,返回给用户错误。

    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {//测试描述符可读或可写。
        len = sizeof(error);
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)//套接字有错误,则证明出现问题了。
            return(-1);         /* Solaris pending error */
    } else
        err_quit("select error: sockfd not set");

done:
    Fcntl(sockfd, F_SETFL, flags);  /* restore file status flags */

    if (error) {
        close(sockfd);      /* just in case */
        errno = error;
        return(-1);
    }
    return(0);
}

被中断的connect

对于一个正常的阻塞式套接字,如果其上的connect调用在TCP三路握手完成前被中断(譬如说捕获了某个信号),将会发生什么呢?假设被中断的connect调用不由内核自动重启,那么它将返回EINTR。我们不能再次调用connect等待未完成的连接继续完成。这样做将导致返回EADDRINUSE错误。这种情形下我们只能调用 select。就像本节对于非阻塞connect所做的那样。连接建立成功时select返回套接字可写条件,连接建立失败时select返回套接字既可读又可写条件。

你可能感兴趣的:(UNPV1)