正常情况下客户端发送 SYN
,服务器很快返回 ACK
,但如果客户端很久没有收到 ACK
,应该怎么办呢?下面介绍 3 种减小等待时间的方法。
(1)tcp_syn_retries
当中途丢包时,最多会重复发送 SYN
6 次,由tcp_syn_retries
决定。
$ cat /proc/sys/net/ipv4/tcp_syn_retries
6
对应的内核源代码在这里:
https://elixir.bootlin.com/linux/v5.0/source/include/net/tcp.h#L105
#define TCP_SYN_RETRIES 6
https://elixir.bootlin.com/linux/v5.0/source/net/ipv4/tcp_ipv4.c#L2631
net->ipv4.sysctl_tcp_syn_retries = TCP_SYN_RETRIES;
可以在接收端用 iptables
丢弃掉SYN
包,再抓包观察。
$ sudo iptables -A INPUT -p tcp -s 192.168.136.128 -m tcp --tcp-flags SYN SYN -j DROP
发送端耗时:第 1 次重试发生在 1 秒钟后,接着会以翻倍的方式在第 2、4、8、16、32 秒共做 6 次重试,最后一次重试会等待 64 秒,如果仍然没有返回 ACK,才会终止三次握手。所以,总耗时是 1+2+4+8+16+32+64=127 秒。
$ time ssh 192.168.136.131
ssh: connect to host 192.168.136.131 port 22: Connection timed out
real 2m7.392s
user 0m0.100s
sys 0m0.000s
Wireshark抓包截图如下,第 1 次加上后面的重传 6 次一共是 7 次:

如果要 connect
更快报错,可以适当调低 tcp_syn_retries
的值。比如减少 1 次,时间少了很多:
$ sudo sh -c "echo 5 > /proc/sys/net/ipv4/tcp_syn_retries"
$ time ssh 192.168.136.131
ssh: connect to host 192.168.136.131 port 22: Connection timed out
real 1m3.130s
user 0m0.000s
sys 0m0.000s
另外建立连接时,本地端口号可用范围是由 ip_local_port_range
决定的:
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
(2)可以把套接字设置为非阻塞,再通过 select
设置超时时间,可以使 connect
函数快速返回错误:
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
int connect_timeout(int sockfd, const char * ip, int port, int second)
{
int flags, result;
fd_set fdw;
struct timeval timeout;
struct sockaddr_in servaddr;
if((flags = fcntl(sockfd, F_GETFL, 0)) < 0)
{
perror("fcntl...\n");
return -1;
}
if(fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0)
{
perror("fcntl...\n");
return -1;
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &servaddr.sin_addr);
servaddr.sin_port = htons(port);
if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
{
if(errno != EINPROGRESS)
{
perror("connect...\n");
return -1;
}
}
else
{
printf("Connected\n");
return 0;
}
FD_ZERO(&fdw);
FD_SET(sockfd, &fdw);
timeout.tv_sec = second;
timeout.tv_usec = 0;
result = select(sockfd + 1, NULL, &fdw, NULL, &timeout);
if(0 == result)
{
printf("Connect server timeout");
return -1;
}
if(1 == result)
{
if(FD_ISSET(sockfd, &fdw))
{
printf("Connected\n");
return 0;
}
}
perror("select...\n");
return -1;
}
int main(int argc, char *argv[])
{
if(argc != 4)
{
printf("error: argc != 4\n");
exit(1);
}
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("socket...\n");
return -1;
}
if (0 != connect_timeout(sockfd, argv[1], atoi(argv[2]), atoi(argv[3]) ) )
{
close(sockfd);
return -1;
}
send(sockfd, "HELLO WORLD", strlen("HELLO WORLD"), 0);
close(sockfd);
return -1;
}
这里设置为 5 秒超时,测试效果如下:
$ time ./send 192.168.136.131 6666 5
Connect server timeout
real 0m5.005s
user 0m0.000s
sys 0m0.000s
(3)通过 SO_SNDTIMEO
选项设置超时时间:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in addr;
struct timeval timeout = {5, 0};
socklen_t len = sizeof(timeout);
if (argc != 4)
{
printf("error: argc != 4\n");
exit(1);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("socket...\n");
return -1;
}
timeout.tv_sec = atoi(argv[3]);
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
perror("connect");
return -1;
}
printf("connected\n");
send(sockfd, "HELLO WORLD", strlen("HELLO WORLD"), 0);
close(sockfd);
return 0;
}
测试结果如下:
$ time ./send2 192.168.136.131 6666 5
connect: Operation now in progress
real 0m5.001s
user 0m0.000s
sys 0m0.000s
网友评论