为 connect 设置连接超时

连接超时

  调用connect连接到远程主机时,若网络发生拥塞,TCP可能会执行多次超时重传,这将耗费不短的时间,所以我们会想要为connect设置连接的超时。考虑到select可以设置一个超时时间,所以我们会使用select来实现这个功能。

  我们实现一个函数connect_timeout

1
2
3
// Return 1 on success, 0 on timeout and -1 on failed
int connect_timeout(int sockfd, const struct sockaddr *addr,
socklen_t addrlen, struct timeval *timeout);

  其实现并不复杂,首先我们将 socket 设置成非阻塞的,然后调用connect,如果connect返回0就说明连接已经建立了,如果返回-1并且错误码是EINPROGRESS,这说明connect正处在建立连接的过程中,返回其它错误码则表示connect调用失败。

  很明显,既然connect正在建立连接,那么它就有可能成功也有可能失败。那么我们可以调用select并且设置一个超时,等待connect建立连接、超时或失败。如果connect成功建立连接,则需要将 socket 重新设置成阻塞的。

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
int connect_timeout(int sockfd, const struct sockaddr *addr,
socklen_t addrlen, struct timeval *timeout)
{
int flags = fcntl( sockfd, F_GETFL, 0 );
if (flags == -1) {
return -1;
}
if (fcntl( sockfd, F_SETFL, flags | O_NONBLOCK ) < 0) {
return -1;
}
int status = connect(sockfd, addr, addrlen);
if (status == -1 and errno != EINPROGRESS) {
return -1;
}
if (status == 0) {
if (fcntl(sockfd, F_SETFL, flags) < 0) {
return -1;
}
return 1;
}
fd_set read_events;
fd_set write_events;
FD_ZERO(&read_events);
FD_SET(sockfd, &read_events);
write_events = read_events;
int rc = select(sockfd + 1, &read_events, &write_events, nullptr, timeout );
if (rc < 0) {
return -1;
} else if (rc == 0) {
return 0;
}
if (!isconnected(sockfd, &read_events, &write_events) )
{
return -1;
}
if ( fcntl( sockfd, F_SETFL, flags ) < 0 ) {
return -1;
}
return 1;
}

  在select成功返回之后,我们需要判断 socket 的可读写状况,如果它是不可读也不可写的,也就是说连接建立失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool isconnected(int sockfd, fd_set *rd, fd_set *wr)
{
if (!FD_ISSET(sockfd, rd) && !FD_ISSET(sockfd, wr)) {
return false;
}
int err;
socklen_t len = sizeof(err);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
return false;
}
errno = err; /* in case we're not connected */
return err == 0;
}

  注意,如果连接建立失败,那么 socket 是既可读又可写的。但是还有另一种情况,如果在调用connectselect之间,socket 接收到数据,那么在select返回之后,socket 也是既可读又可写的。所以这里要用到getsockopt来判断 socket 是否发生错误。

参考资料