【Socket编程】非阻塞connect

阅读Skynet源码的过程中,发现一种非阻塞connect方式。以前不知道,这次好好学习一下。

文章参考自:非阻塞connect编写方法介绍(董的博客)


TCP连接的建立涉及到一个三次握手的过程,鉴于RTT波动范围很大,从局域网的几个毫秒到几百个毫秒甚至广域网上的几秒。这段时间内,我们可以执行其他处理工作,以便做到并行。因此,非阻塞connect可以为我所用。


1、fcntl

fcntl函数可执行各种描述符的控制操作,对于socket描述符,可以设置其IO为阻塞或非阻塞。默认情况下connect为阻塞,现修改为非阻塞:

int flag = fcntl(fd, F_GETFL);	//获取当前flag
if ( -1 == flag ) 
{
	return;
}
fcntl(fd, F_SETFL, flag | O_NONBLOCK);	//设置为非阻塞


2、 connect

对于阻塞式套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或者出错时才返回;对于非阻塞式套接字,如果调用connect函数返回-1(表示出错),且错误为EINPROGRESS,表示连接建立中,尚未完成;如果返回0,则表示连接已经建立。


3、epoll

epoll作为Linux下出色的IO多路复用工具,它可以让内核监控多个socket。使用epoll与非阻塞connect相互配合,可以达到异步建立TCP连接的目的。需要注意:

[1] 当连接成功建立时,connect描述符变成可写; 

[2] 当连接建立遇到错误时,描述符变为即可读,也可写,遇到这种情况,可调用getsockopt函数查看连接状态。


于是,非阻塞connect步骤如下:

1)创建socket,设置为非阻塞;

2)调用connect函数,如果返回0,则连接建立;如果返回-1,检查errno ,如果值为 EINPROGRESS,表明连接正在建立,否则表明连接出错;

3)将connect用的fd置于epoll监控下;

4)待epoll监测到fd可写,同时用getsockopt查看连接状态,如没有错误,表明连接建立成功。


一个简单的例子如下:

客户端:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/epoll.h>

int main()
{
	struct addrinfo ai_hints;
	struct addrinfo *ai_list = NULL;
	struct addrinfo *ai_ptr = NULL;
	const char *host = "127.0.0.1";
	const char *port = "8888";
	memset(&ai_hints, 0, sizeof(ai_hints));
	ai_hints.ai_family = AF_UNSPEC;
	ai_hints.ai_socktype = SOCK_STREAM;
	ai_hints.ai_protocol = IPPROTO_TCP;

	if (getaddrinfo(host, port, &ai_hints, &ai_list))
	{
		printf("error\n");
		return 0;
	}

	int status = -1;
	int connect_fd = -1;
	for (ai_ptr = ai_list; ai_ptr != NULL; ai_ptr = ai_ptr -> ai_next)
	{
		connect_fd = socket(ai_ptr -> ai_family, ai_ptr -> ai_socktype, ai_ptr -> ai_protocol);
		if (connect < 0)
		{
			close(connect_fd);
			printf("error\n");
			return 0;
		}

		
		// 查看getaddrinfo函数后返回的目的IP和PORT,注意各种转换。inet_ntop、ntohs。
		char buffer[100];
		struct sockaddr * addr = ai_ptr->ai_addr;
		void * sin_addr = (ai_ptr->ai_family == AF_INET) ? (void*)&((struct sockaddr_in *)addr)->sin_addr : (void*)&((struct sockaddr_in6 *)addr)->sin6_addr;
		if (inet_ntop(ai_ptr->ai_family, sin_addr, buffer, sizeof(buffer))) {
			printf("sin_addr: %s, port: %d\n", buffer, ntohs(((struct sockaddr_in *)addr)->sin_port));
		}

		int flag = fcntl(connect_fd, F_GETFL);
		if (-1 == flag)
		{
			close(connect_fd);
			printf("fcntl error\n");
			return 0;
		}
		fcntl(connect_fd, F_SETFL, flag | O_NONBLOCK);
		status = connect(connect_fd, ai_ptr -> ai_addr, ai_ptr -> ai_addrlen);
		if(status != 0 && errno != EINPROGRESS)
		{
			close(connect_fd);
			printf("fcntl error\n");
			return 0;
		}
		break;
	}

	if (0 == status)
	{
		printf("Connected!\n");
		sleep(20);
		close(connect_fd);
		return 0;
	}
	else
	{
		int efd = epoll_create(10);
		struct epoll_event event;
		struct epoll_event wait_event[1];
		event.data.fd = connect_fd;
		event.events = EPOLLOUT | EPOLLIN;
		epoll_ctl(efd, EPOLL_CTL_ADD, connect_fd, &event);

		int ret = 0;
		while (1)
		{
			printf("epoll...\n");
			ret = epoll_wait(efd, wait_event, 1, -1);
			int err = 0;
			int errlen = sizeof(err);
			
			printf("ret: %d\n", ret);
			for (int i = 0; i < ret; ++i)
			{
				printf("%d %d\n", (wait_event[i].events & EPOLLOUT) == 0, (wait_event[i].events & EPOLLIN) == 0);
				getsockopt(wait_event[i].data.fd, SOL_SOCKET, SO_ERROR, &err, &errlen);
				if (err)
				{
					close(connect_fd);
					close(efd);
					printf("err: %d\n", err);
					return 0;
				}
				if (wait_event[i].data.fd == connect_fd && (wait_event[i].events & EPOLLOUT))
				{
					printf("Connected!\n");
					sleep(20);
					close(connect_fd);
					close(efd);
					return 0;
				}
			}
		}
	}

}




服务器:

#include <stdio.h>  
#include <string.h>
#include <unistd.h>  
#include <assert.h>
#include <stdlib.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
   
const int SERVER_PORT = 8888;  
  
int main()  
{  
    int server_socket;  
    int n;  
  
    server_socket = socket(AF_INET, SOCK_STREAM, 0);  
    assert(server_socket != -1);  
  
    struct sockaddr_in server_addr;  
    memset(&server_addr, 0, sizeof(server_addr));  
    server_addr.sin_family = AF_INET;  
    server_addr.sin_port = htons(SERVER_PORT);  
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
  
    assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);  
    assert(listen(server_socket, 5) != -1);  
      
    struct sockaddr_in client_addr;  
    socklen_t client_addr_len = sizeof(client_addr);  
  
    while(1)  
    {  
        printf("waiting...\n");  
        int connfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);   
        if(connfd == -1)  
            continue;  
		printf("Connected..\n");
		sleep(20);  
		close(connfd);  
        break;
    }

    close(server_socket);  
  
    return 0;  
}  


客户端运行结果:

sin_addr: 127.0.0.1, port: 8888
epoll...
ret: 1
0 1
Connected!

可以发现,开始第一次 connect 的结果表明连接建立中,后面通过 epoll 监控到TCP连接建立完成。



PS:getsockopt 函数很重要!在不启动服务器的情况下,客户端代码是可以正常运行到 getsockopt 函数所在地方的,而且此时epoll反馈connect_fd是可读可写的,其实已经出错了。通过该函数的 err 参数返回错误码 111 可知已经从出错。


Linux下常见的socket错误码:
EACCES, EPERM:用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败。
EADDRINUSE 98:Address already in use(本地地址处于使用状态)
EAFNOSUPPORT 97:Address family not supported by protocol(参数serv_add中的地址非合法地址)
EAGAIN:没有足够空闲的本地端口。
EALREADY 114:Operation already in progress(套接字为非阻塞套接字,并且原来的连接请求还未完成)
EBADF 77:File descriptor in bad state(非法的文件描述符)
ECONNREFUSED 111:Connection refused(远程地址并没有处于监听状态)
EFAULT:指向套接字结构体的地址非法。
EINPROGRESS 115:Operation now in progress(套接字为非阻塞套接字,且连接请求没有立即完成)
EINTR:系统调用的执行由于捕获中断而中止。
EISCONN 106:Transport endpoint is already connected(已经连接到该套接字)
ENETUNREACH 101:Network is unreachable(网络不可到达)
ENOTSOCK 88:Socket operation on non-socket(文件描述符不与套接字相关)
ETIMEDOUT 110:Connection timed out(连接超时)



此外,这次用了与之前不一样的初始化addr的方式,getaddrinfo 函数,该函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个addrinfo的结构(列表)指针。




  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值