非阻塞connect

TCP连接的建立涉及到一个三次握手的过程,且SOCKET中connect函数需要一直等到客户接收到对于自己的SYN的ACK为止才返回,这意味着每个connect函数总会阻塞其调用进程至少一个到服务器的RTT时间,而RTT波动范围很大,从局域网的几个毫秒到几百个毫秒甚至广域网上的几秒。这段时间内,我们可以执行其他处理工作,以便做到并行。在此,需要用到非阻塞connect。

非阻塞connect的作用:

  • 可以让三路握手的处理等同与一般数据的处理,而不是一直让 connect一直尝试重连或者花费一个RTT时间。而且RTT时间从几毫秒到几秒不等,万一有许多连接,不论是尝试重连还是花费一个RTT时间,都将是致命的延时。
  • 可以使用该技术同时建立多个连接。Web浏览器中常用。
  • 既然使用select等待连接的建立,我们就可以制定一个时间限制,使得我们能够缩短connect的超时。

使用selcet与非阻塞connect的一些注意事项:

  • 处理connect立即建立的情况。(比如我们连接的是同一个主机时)
  • 当连接建立成功时,套接口描述符变成可写;
  • 当一个套接口出错时,它会被 select 调用标记为既可读又可写

处理非阻塞 connect的步骤:

第一步,创建 socket,返回套接字描述符;

第二步,调用 fcntl 或 ioctlsocket 把套接口描述符设置成非阻塞;

第三步,调用 connect 开始建立连接;

第四步,判断连接是否成功建立:

  • 如果 connect 返回 0 ,表示连接成功(服务器和客户端在同一台机器上时就有可能发生这种情况);
  • 调用select 来等待连接建立成功完成;
    如果select 返回0,则表示建立连接超时。我们返回超时错误给用户,同时关闭连接,以防止三路握手操作继续进行下去。
    如果select 返回大于0的值,则需要检查套接口描述符是否可写,如果套接口描述符可写,则我们可以通过调用getsockopt来得到 套接口上待处理的错误(SO_ERROR)。如果连接建立成功,这个错误值将是0;如果建立连接时遇到错误,则这个值是连接错误所对应的errno值(比如:ECONNREFUSED,ETIMEDOUT等)。

移植性问题总结 :

  • 对于出错的套接口描述符,getsockopt 的返回值源自 Berkeley 的实现是返回 0 ,待处理的错误值存储在 errno 中;而源自 Solaris 的实现是返回 -1 ,待处理的错误存储在 errno 中。(套接口描述符出错时调用 getsockopt 的返回值不可移植)
  • 有可能在调用 select 之前,连接就已经建立成功,而且对方的数据已经到来,在这种情况下,套接口描述符是既可读又可写,这与套接口描述符出错时是一样的。(怎样判断连接是否建立成功的条件不可移植)

这样的话,在我们判断连接是否建立成功的条件不唯一时,我们可以有以下的方法来解决这个问题:

  1. 调用获取对端socket地址的 getpeername 代替 getsockopt 。如果调用 getpeername 失败,getpeername 返回 ENOTCONN ,表示连接建立失败,之后我们必须再以 SO_ERROR 调用 getsockopt 得到套接口描述符上的待处理错误;
  2. 调用 read ,读取长度为 0 字节的数据。如果连接建立失败,则 read 会返回 -1 ,且相应的 errno 指明了连接失败的原因;如果连接建立成功,read 应该返回 0 。
  3. 再调用一次 connect 。它应该失败,如果错误 errno 是 EISCONN ,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include"head.h"

using namespace std;

#define BUFFER_SIZE 1024

int setnonblocking(int fd) {
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}

//非阻塞连接,如果函数成功则返回连接的socket,不成功返回-1
int unblock_connect(const char* ip, int port, int time) {
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int fdopt = setnonblocking(sockfd);
//此处connect会立即返回
ret = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
if(ret == 0) {
printf("connect with server immediately\n");
fcntl(sockfd, F_SETFL, fdopt);
return sockfd;
}
else if(errno != EINPROGRESS) {
printf("unblock connect not support\n");
return -1;
}
fd_set readfds;
fd_set writefds;
struct timeval timeout;

FD_ZERO(&readfds);
FD_ZERO(&writefds);

timeout.tv_sec = time;
timeout.tv_usec = 0;

// 在指定时间内,select会监听用户感兴趣的文件描述符的可读、可写和异常等事件
ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);
if(ret <= 0) {
printf("connection time out\n");
close(sockfd);
return -1;
}

if(!FD_ISSET(sockfd, &writefds)) {
printf("no events on sockfd found\n");
close(sockfd);
return -1;
}

int error = 0;
socklen_t length = sizeof(error);
if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0) {
printf("get socket option failed\n");
close(sockfd);
return -1;
}

if(errno != 0) {
printf("connection failed after select with the error: %d\n", error);
close(sockfd);
return -1;
}

printf("connection ready after select with the socket: %d\n", sockfd);
fcntl(sockfd, F_SETFL, fdopt);
return sockfd;
}

int main(int argc, char** argv) {
if(argc <= 2) {
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int sockfd = unblock_connect(ip, port, 10);
if(sockfd < 0) return 1;
close(sockfd);
return 0;
}