linux 实现非阻塞connect

Linux 怎样实现非阻塞connect

前言

凡是接触过socket编程的,对connect函数一定不陌生。因为socket是面向连接的,所以在进行读写操作前我们首先要进行连接,而所谓连接也就是我们常说的三次握手,这个过程就是在connect函数中完成的。 
虽然connect函数本身不具备阻塞的功能,但是我们可以通过对socket进行设置和使用select函数可以设置阻塞时间的特性实现非阻塞。

为什么要非阻塞

第一,我们可以在connect时去做些别的事,毕竟三次握手需要在网络中往返多层次,我们没有必要一直在那里闲着。 
第二,这一点很重要,因为connect的超时时间在75秒到几分钟之间,显然不可能去让程序阻塞那么久。

怎样实现非阻塞

1. 设置socket

int oldOption = fcntl(sockfd, F_GETFL);
int newOption = oldOption | O_NONBLOCK;
//设置sockfd非阻塞
fcntl(sockfd, F_SETFL, newOption);

2. 执行connect

如果返回0,表示连接成功,这种情况一般在本机上连接时会出现(否则怎么可能那么快)
否则,查看error是否等于EINPROGRESS(表明正在进行连接中),如果不等于,则连接失败
int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == 0)
{
    //连接成功
    fcntl(sockfd, F_SETFL, oldOption);
    return sockfd;
}
else if(errno != EINPROGRESS)
{
    //连接没有立即返回,此时errno若不是EINPROGRESS,表明错误
    perror("connect error != EINPROGRESS");
    return -1;
}

3. 使用select,如果没用过select可以去看看

用select对socket的读写进行监听
那么监听结果有四种可能
1. 可写(当连接成功后,sockfd就会处于可写状态,此时表示连接成功)
2. 可读可写(在出错后,sockfd会处于可读可写状态,但有一种特殊情况见第三条)
3. 可读可写(我们可以想象,在我们connect执行完到select开始监听的这段时间内,
    如果连接已经成功,并且服务端发送了数据,那么此时sockfd就是可读可写的,
    因此我们需要对这种情况特殊判断)
    说白了,在可读可写时,我们需要甄别此时是否已经连接成功,我们采用这种方案:
    再次执行connect,然后查看error是否等于EISCONN(表示已经连接到该套接字)。
4. 错误

if(FD_ISSET(sockfd, &writeFds))
{
    //可读可写有两种可能,一是连接错误,二是在连接后服务端已有数据传来
    if(FD_ISSET(sockfd, &readFds))
    {
        if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) != 0)
        {
            int error=0;
            socklen_t length = sizeof(errno);
            //调用getsockopt来获取并清除sockfd上的错误.
            if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0)
            {
                printf("get socket option failed\n");
                close(sockfd);
                return -1;
            }
            if(error != EISCONN)
            {
                perror("connect error != EISCONN");
                close(sockfd);
                return -1;
            }
        }
    }
    //此时已排除所有错误可能,表明连接成功
    fcntl(sockfd, F_SETFL, oldOption);
    return sockfd;
}

 

4. 恢复socket

因为我们只是需要将连接操作变为非阻塞,并不包括读写等,所以我们吃醋要将socket重新设置。
fcntl(sockfd, F_SETFL, oldOption);

 

到此,我们的非阻塞connect函数已经成功了。

代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//blokTimeMs表明非阻塞时的毫秒数,若值为-1表明阻塞
int connect(std::string ip, int port, int blockTimeMs = -1)
{
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);

    int sockfd = socket(PF_INET, SOCK_STREAM, 0);

    //阻塞
    if(blockTimeMs == -1)
    {
        int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
        if(ret < 0)
        {
            perror("connectBloking");
            close(sockfd);
            return -1;
        }
        close(sockfd);
        return 1;
    }

    //非阻塞
    int oldOption = fcntl(sockfd, F_GETFL);
    int newOption = oldOption | O_NONBLOCK;

    //设置sockfd非阻塞
    fcntl(sockfd, F_SETFL, newOption);

    int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == 0)
    {
        //连接成功
        fcntl(sockfd, F_SETFL, oldOption);
        return sockfd;
    }
    else if(errno != EINPROGRESS)
    {
        //连接没有立即返回,此时errno若不是EINPROGRESS,表明错误
        perror("connect error != EINPROGRESS");
        return -1;
    }

    fd_set readFds;
    fd_set writeFds;
    struct timeval timeout;

    FD_ZERO(&readFds);
    FD_ZERO(&writeFds);

    FD_SET(sockfd, &writeFds);
    FD_SET(sockfd, &readFds);

    timeout.tv_sec = blockTimeMs/1000;
    timeout.tv_usec = (blockTimeMs%1000)*1000;

    ret = select(sockfd+1, &readFds, &writeFds, NULL, &timeout);
    if(ret <= 0)
    {
        perror("select timeout or error");
        close(sockfd);
        return -1;
    }

    if(FD_ISSET(sockfd, &writeFds))
    {
        //可读可写有两种可能,一是连接错误,二是在连接后服务端已有数据传来
        if(FD_ISSET(sockfd, &readFds))
        {
            if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) != 0)
            {
                int error=0;
                socklen_t length = sizeof(errno);
                //调用getsockopt来获取并清除sockfd上的错误.
                if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0)
                {
                    printf("get socket option failed\n");
                    close(sockfd);
                    return -1;
                }
                if(error != EISCONN)
                {
                    perror("connect error != EISCONN");
                    close(sockfd);
                    return -1;
                }
            }
        }
        //此时已排除所有错误可能,表明连接成功
        fcntl(sockfd, F_SETFL, oldOption);
        return sockfd;
    }
    else
    {
        perror("connect failed");
        close(sockfd);
        return -1;
    }
}

 

 

unix网络编程卷1 第 16章描述

linux 实现非阻塞connect_第1张图片

 

非阻塞connect

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

(1)我们可以把三路握手叠加在其他处理上。完成一个connect要花一个RTT时间,波动范围很大,从局域网的几个毫秒到广域网的几秒。75秒或更长

(2)我们可以使用这个技术同时建立多个连接。这个用途已随着web浏览器变得流行起来,后面给出例子。

(3)使用select等待连接的建立,可以给select指定一个时间限制,使得我们就能够缩短connect的超时。实现方法就是使用非阻塞connect。

使用需要处理一些细节:

  • 尽管套接字是非阻塞的,如果连接的服务器是在同一个主机上,即局域网内,那么connect通常会立即连接,我们必须处理这种情形
  • 源自Brekeley的实现,有关于select和非阻塞connect的一下两个原则:1、当连接成功建立时,描述符变为可写;2、当连接遇到错误时,描述符变为即可读又可写  (下文select判断退出的依据)

本段代码结构:

1、先非阻塞调用connect
如果 返回值== 0 说明是内网,能够迅速的立即连接上
如果 返回值< 0且    !=    EINPROGRESS
说明出错返回 -1

2、调用select永久阻塞,最后一位传参为NULL,即(相当于等待connect 超时返回,最长约75秒到数分钟)
3、FD_ISSET 判断是否有读写
当连接建立成功,描述符变为可写,当连接遇到错误,描述符变为即可读又可写

以下附上代码

#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); 
    Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    error = 0;
    if ( (n = connect(sockfd, saptr, salen)) < 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 */                            // 1、此处的超时,是设置了select的 定时参数的超时
        errno = ETIMEDOUT;
        return(-1);
    }   

    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
        len = sizeof(error);
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)   // 2、得到error值,若连接成功,error = 0,若失败 error = 错误值,然后再下文if(error)做判断,退出
            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);
}

 

其中有两点需要注意:

    if ( (n = Select(sockfd+1, &rset, &wset, NULL,
                     nsec ? &tval : NULL)) == 0) {
        close(sockfd);      /* timeout */                            // 1、此处的超时,是设置了select的 定时参数的超时
        errno = ETIMEDOUT;
        return(-1);
    } 

这个超时是设置了select 定时参数之后的超时,若timeval值传参为NULL,则select永久阻塞,直到对端服务发来 

 

select函数返回的依据:

1、当连接建立成功后,描述符变为可写

2、RST  :表示复位,说明对端主机没有指定端口的服务等待与之连接,这是一种硬错误,客户一收到,立即返回ECONNREFUSED错误 。

产生RST有三种情况:

1、目的地为某端口的SYN到达,然而该端口没有正在监听的服务器。

2、TCP想取消一个已有连接。

3、TCP接收到一个根本不存在的连接上的分节。 (TCPv1第246~250有更详细的介绍)

3、当某个套接字上发生错误时,它将由select标记为即可读又可写

有个问题:

    当select永久阻塞时,对端一直超时,也没任何响应分节到达描述符,那么select怎么退出的,是根据以上第3点,描述符发生错误吗?

 

select 函数介绍

linux 实现非阻塞connect_第2张图片

linux 实现非阻塞connect_第3张图片

 

第二点需要注意:

if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
        len = sizeof(error);
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)   // 2、得到error值,若连接成功,error = 0,若失败 error = 错误值,然后再下文if(error)做判断,退出
            return(-1);         /* Solaris pending error */
    } else
        err_quit("select error: sockfd not set");

如果描述符变为可读或可写,我们就调用getsockopt 取得套接字的待处理错误(使用SO_ERROR套接字选项)

1、如果成功建立连接,error = 0 

2、如果连接发生错误,error = 对应的错误error值(ECONNREFUSED(说明对端发来RST)、ETIMEDOUT)等 ,

     这个ETIMEDOUT超时,是connect()函数超时之后返回的

3、移植性问题:Berkeley 实现 :error 返回待处理错误值,getsockopt 本身返回0;

                            Solaris实现: errno返回错误值, getsockopt 返回 -1;

但是本程序可以同时兼容处理以上两种移植性。

 

另一种采取 poll模型 判断非阻塞connect是否连接成功

1、connect失败后,返回值判断是否为 EINTR ()

2、调用poll 或者select 阻塞或者定时检测sockfd 事件发生

3、getsockopt()  取得 SO_ERROR 中错误码, 若ret == 0 , 则连接成功 ;否则 连接失败

poll:


int CheckConnectIsOn(int iSocket) {
	struct pollfd fd;
	int ret = 0;
	socklen_t len = 0;

	fd.fd = iSocket;
	fd.events = POLLOUT;

	while ( poll (&fd, 1, -1) == -1 ) {
		if( errno != EINTR ){
			perror("poll");
			return -1;
		}
	}

	len = sizeof(ret);
	if ( getsockopt (iSocket, SOL_SOCKET, SO_ERROR, &ret, &len) == -1 ) {
    	        perror("getsockopt");
		return -1;
	}

	if(ret != 0) {
		fprintf (stderr, "socket %d connect failed: %s\n",
                 iSocket, strerror (ret));
		return -1;
	}

	return 0;
}

调用时 如下:

if(connnect()) {
    if(errno == EINTR) {
        if(CheckConnectIsOn() < 0) {
              perror();
              return -1;
        }
        else {
             printf("connect is success!\n");
        }
    }
    else {
         perror("connect");
         return -1;
    }
}

 

 

你可能感兴趣的:(unix网络编程,UNPV探索)