首页 > 代码库 > 非阻塞connect

非阻塞connect

一、用途:

1.提高程序效率
默认情况下connect函数是阻塞的,它发起TCP连接的三路握手。完成一个connect需要花费一个RTT时间(从本地主机到对端再回到本地),RTT的波动范围很大,局域网上几毫秒到几百毫秒,广域网上甚至能需要几秒,这对计算机来说是非常漫长的时间,这段时间可以用来执行其他的处理工作,提高效率。因此非阻塞的connect函数是必要的。

2.同时建立多个连接,不必串行的一直等待某个连接的结束,而是采用非阻塞的方式,连续建立连接,比串行更节省时间。例如,web浏览器。

3.为connect指定更短的超时时间。

connect有默认的超时时间,非阻塞connect的实现依赖于select,select的一个参数可以用来指定超时时间。


二、实现

非阻塞connect函数的实现,
1.调用fcntl将套接字设置为非阻塞

fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

2.对非阻塞套接字调用connect,引发三路握手
当对一个非阻塞TCP套接字调用connect时,将返回一个EINPROGRESS错误,表示连接已经启动,但是尚未完成

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

3.根据select判断连接结果
当连接建立时,描述符变为可写(因为并未发送数据,套接字发送缓冲区有可用空间)
当连接建立遇到错误时,描述符变为即可读,又可写(一个套接字上发生某个错误,待处理的错误总是导致该套接字变为既可读又可写),当连接建立成功,并且来自对端的数据到达时,描述符也变为既可读又可写,所以这里通过getsockopt取得套接字待处理的错误(使用SO_ERROR套接字选项)。连接成功建立,该值为0,否则为对应的错误值。

if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset))
	{
		socklen_t len = sizeof(error);
		/*SO_ERROR  获取错误状态*/
		if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) == -1)
		{
			close(sockfd);
			return -1;
		}
	}
	
	if (error)
	{
		close(sockfd);
		return -1;
	}
}

示例程序

/*
*connect_nowait 非阻塞connect建立连接并返回连接好的套接字
*addr 连接到的ipv4地址
*port 连接到的端口
*nsec 超时时间
*fp   函数指针
*返回值:成功返回套机字,出错返回-1
*/
int connect_nowait(const char* addr,const unsigned int port, 
				   const int nsec, void (*fp)())
{
	int    sockfd;
	struct sockaddr_in saddr;
	int    flags;
	int    r;
	int    error;
	fd_set rset, wset;
	struct timeval tval;

	if (addr == NULL)
		return -1;
	
	/*地址格式不合法*/
	if (inet_addr(addr) == -1)
		return -1;
	
	/*端口超出范围*/
	if (port > 65535)
		return -1;
		
	/*时间不合法*/
	if (nsec < 0)
		return -1;
		
	
	if (fp == NULL)
		return -1;
		
	bzero((void*)&saddr, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_addr.s_addr = inet_addr(addr);
	saddr.sin_port = htons(port);
	
	/*socket异常*/
	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		return -1;
	
	if ((flags = fcntl(sockfd, F_GETFL, 0)) == -1)
	{
		close(sockfd);
		return -1;
	}
	
	/*设置为非阻塞*/
	if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1)
	{
		close(sockfd);
		return -1;
	}
	
	/*对非阻塞的TCP套接字调用connect,期待返回EINPROGRESS错误,表示引发三路握手,但连接尚未完成*/
	if ((r = connect(sockfd, (SA*)&saddr, sizeof(saddr))) == -1)
	{
		if (errno != EINPROGRESS)
		{
			close(sockfd);
			return -1;
		}
	}
	
	/*建立连接期间,这里可以做一些其他的处理,提高时间利用率*/
	fp();
	
	/*如果客户-服务器都在一台主机上,那么这里可能会立即建立连接*/
	if (r == 0)
		goto done;
	
	/*将套接字添加到读写描述符集中*/
	FD_ZERO(&rset);
	FD_SET(sockfd, &rset);
	wset = rset;
	
	/*初始化超时时间*/
	tval.tv_sec = nsec;
	tval.tv_usec = 0;
	
	/*select 异常*/
	if ((r = select(sockfd+1, &rset, &wset, NULL, &tval)) == -1)
	{
		close(sockfd);
		return -1;
	}
	
	/*select 超时*/
	if (r == 0)
	{
		close(sockfd);
		return -1;
	}
	
	/*
	*三种情况
	*1.套接字成功建立连接,此时可写
	*2.连接出错,套接字既可读又可写
	*3.成功建立连接,并有对端数据到达,套接字既可读又可写
	*/
	if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset))
	{
		socklen_t len = sizeof(error);
		/*SO_ERROR  获取错误状态*/
		if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) == -1)
		{
			close(sockfd);
			return -1;
		}
	}
	
	if (error)
	{
		close(sockfd);
		return -1;
	}
		
done:
	if ((r = fcntl(sockfd, F_SETFL, flags)) == -1)
	{
		close(sockfd);
		return -1;
	}
	
	return sockfd;
}


#include <iostream>
#include "net.h"

using namespace std;

void while_connect()
{
	for(int i=0; i<10; i++)
		cout << "while_connect" << endl;
}

int main()
{
	int sockfd;
	char *addr = "182.92.78.165";
	unsigned int port = 33333;
	int nsec = 3;
	
	sockfd = connect_nowait(addr, port, nsec, while_connect);
	if (sockfd == -1)
		cout << "连接失败" << endl;
	cout << "连接成功" << endl;

	return 0;
}

执行结果:


总结:
非阻塞connect实际上与普通建立套接字并连接的步骤没有太大差别,只是调用fcntl将套接字设置为非阻塞后调用connect,同样引发三路握手,并不等待connect返回,,而去利用这段时间处理其他操作,更好的利用时间,之后再利用select检测套接字来判断连接是否完成,并且可以指定一个超时时间。