生命有开始就有结束,TCP 连接也不例外。
通过上一篇文章《Socket 与 TCP 三次握手》中的分析,现在我们知道,建立 TCP 连接需要 Client <–> Server 之间三个来回沟通并确认才行。一说到“连接”,我们可能会想到家里的自然水,有一条管道直通自然水厂。但这里的“连接”并没有一条所谓的虚拟网络管道,它只是在 Client <–> Server 两端程序中维护了一些数据状态而已,以此来标识对方。
同样,当双方的数据收发工作结束后,也需要清理这些状态数据,释放占用的系统资源,而这就是关闭 Socket,断开连接所干的事。与三次握手不同的是,断开 TCP 连接一般会经历四个来回,我们称之为四次挥手。
TCP 四次挥手
回到前面《Socket 与 TCP 三次握手》中的抓包截图,我们看看第 9-12 这四个包,它们就是四次挥手动作对应的数据报文。
通过前面的程序,我们知道客户端先调用的 int close(fd)
函数,该函数会关闭连接,但对于互联网服务来说,一般是服务端先关闭连接,以此来释放资源,因为它需要服务成千上万个连接。
Declared in: unistd.h
NAME
close – delete a descriptor
SYNOPSIS
#include <unistd.h>
int close(int fildes);
DESCRIPTION
The close() call deletes a descriptor from the per-process object reference
table. If this is the last reference to the underlying object, the object
will be deactivated. For example, on the last close of a file the current
seek pointer associated with the file is lost; on the last close of a
socket(2) associated naming information and queued data are discarded; on
the last close of a file holding an advisory lock the lock is released (see
further flock(2)).
When a process exits, all associated file descriptors are freed, but since
there is a limit on active descriptors per processes, the close() function
call is useful when a large quantity of file descriptors are being handled.
When a process forks (see fork(2)), all descriptors for the new child
process reference the same objects as they did in the parent before the
fork. If a new process is then to be run using execve(2), the process
would normally inherit these descriptors. Most of the descriptors can be
rearranged with dup2(2) or deleted with close() before the execve is
attempted, but if some of these descriptors will still be needed if the
execve fails, it is necessary to arrange for them to be closed if the
execve succeeds. For this reason, the call “fcntl(d, F_SETFD, 1)” is
provided, which arranges that a descriptor will be closed after a
successful execve; the call “fcntl(d, F_SETFD, 0)” restores the default,
which is to not close the descriptor.
RETURN VALUES
Upon successful completion, a value of 0 is returned. Otherwise, a value
of -1 is returned and the global integer variable errno is set to indicate
the error.
ERRORS
The close() system call will fail if:
[EBADF] fildes is not a valid, active file descriptor.
[EINTR] Its execution was interrupted by a signal.
[EIO] A previously-uncommitted write(2) encountered an
input/output error.
The close() call deletes a descriptor from the per-process object reference table. If this is the last reference to the underlying object, the object will be deactivated. For example, on the last close of a file the current seek pointer associated with the file is lost; on the last close of a socket(2) associated naming information and queued data are discarded; on the last close of a file holding an advisory lock the lock is released (see further flock(2)).
close()
调用会将描述符从每个进程对象引用表中删除。 如果这是对基础对象的最后一次引用,该对象将被停用。 例如,在最后一次关闭一个文件时,与该文件相关的当前寻址指针会丢失;在最后一次关闭一个套接字时,相关的命名信息和排队数据会被丢弃;在最后一次关闭一个持有咨询锁的文件时,锁会被释放。
从函数描述中,可以看到 int close(fd)
函数是非常简单的,而这就是它的强大之处:一个简单的接口,可以处理非常复杂的逻辑,而这些逻辑对程序员完全透明。调用该函数关闭 Socket 时,会对套接字引用计数减 1,当套接字引用变为 0 时,就会彻底释放该套接字,并关闭 TCP 两个方向上的数据流。
从抓包中我们看到,客户端先发的 FIN 报文,这是因为哪一方先调用 int close(fd)
函数,就会先发起关闭流程,整个流程大致如下图所示:
这个过程,举一个不恰当的例子,比如男女分手,我们来理解一下(A --> Host1,B --> Host2,下同):
A:我们分手吧。(A 先发起关闭连接,A 状态变成 FIN_WAIT_1)
B:知道了,让我冷静一下,想一想。另外,你之前还有一些东西放在我这,请拿回去。(B 收到关闭消息,状态变成 CLOSE_WAIT;A 收到回应,状态变成 FIN_WAIT_2)
B:分手吧,我想好了。(B 发起被动关闭,状态变成 LAST_ACK)
A:好聚好散,祝幸福。(A 收到消息,发送回应,状态变成 TIME_WAIT;B 收到回应,状态变成 CLOSED;A 过了 2MSL 时间后,状态再自动变成 CLOSED)
上面主要是正常的情况,由于这些状态变化是内核程序控制的,所以一般不太好观察,但通过 netstat
命令,我们其实可以看到一个大概的情况。
首先,我们简单调整一下程序,客户端在调用 close(socket)
之后,让它休眠 3 分钟,调整后代码如下(改动点在最后几行):
// echo_client2.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include "common/log.h"
#include "common/general.h"
int main(int argc, char **argv) {
if (argc != 3) {
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int client_socket = socket(PF_INET, SOCK_STREAM, 0);
if (-1 == client_socket) {
error_handling("create socket failed");
}
struct sockaddr_in server_addr;
memset(&server_addr, '\0', sizeof(server_addr));
server_addr.sin_family = PF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
int connect_ret = connect(client_socket, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (-1 == connect_ret) {
error_handling("connect socket failed");
} else {
printf("connect server success...\n");
}
char message[BUF_SIZE];
int message_len = 0, recv_len = 0, read_len = 0;
while (1) {
fputs("Input(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
printf("quit...\n");
break;
}
message_len = strlen(message);
if (message_len != write(client_socket, message, strlen(message))) {
printf("write message error, quit...\n");
break;
}
recv_len = 0;
// using '<' instead of '!=': prevent endless loop
while (recv_len < message_len) {
read_len = read(client_socket, &message[recv_len], BUF_SIZE);
if (-1 == read_len) {
error_handling("read from server failed");
} else if (0 == read_len) {
error_handling("server closed");
}
recv_len += read_len;
}
message[message_len] = '\0';
printf("Message from server: %s", message);
}
close(client_socket);
// sleep 180 seconds for testing
printf("close socket and sleep 180 seconds now...\n");
sleep(180);
return 0;
}
服务端程序,在关闭对应的连接之前,先休眠 10 秒,这样能让双方分别卡在 FIN_WAIT_2 和 CLOSE_WAIT 状态。
// echo_server2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include "common/log.h"
#include "common/general.h"
#define CONNECTION_SIZE 5
int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
int server_socket = socket(PF_INET, SOCK_STREAM, 0);
if (-1 == server_socket) {
error_handling("create socket failed");
}
struct sockaddr_in server_addr;
memset(&server_addr, '\0', sizeof(server_addr));
server_addr.sin_family = PF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
int bind_ret = bind(server_socket, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (-1 == bind_ret) {
error_handling("bind socket failed");
}
int listen_ret = listen(server_socket, CONNECTION_SIZE);
if (-1 == listen_ret) {
error_handling("listen socket failed");
}
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_socket, message_len;
char message[BUF_SIZE];
for (int i = 0; i < CONNECTION_SIZE; ++i) {
memset(&client_addr, '\0', sizeof(client_addr));
printf("waiting connection to accept...\n");
client_socket = accept(server_socket, (struct sockaddr *) &client_addr, &client_addr_len);
if (-1 == client_socket) {
error_handling("accept socket failed");
} else {
printf("Connected client[%d]: %s:%d\n", i, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
while (1) {
message_len = read(client_socket, message, BUF_SIZE);
if (message_len <= 0) {
if (-1 == message_len) {
printf("read from client[%d] error: [%s]\n", i, strerror(errno));
} else {
printf("client[%d] closed\n", i);
// sleep 10 seconds for testing
printf("server sleep 10 seconds\n");
sleep(10);
}
printf("server close client socket\n");
close(client_socket);
break;
}
if (message_len != write(client_socket, message, message_len)) {
printf("write to client[%d] failed: [%s]\n", i, strerror(errno));
}
}
}
close(server_socket);
return 0;
}
接着,笔者编写了一个循环打印的 shell 脚本,打印 Socket 的状态信息:
#!/bin/bash
while true
do
currTime=$(date '+%Y-%m-%d %H:%M:%S')
echo $currTime
netstat -a -finet|grep 11233
sleep 0.001
done
启动脚本,然后运行服务端程序,再运行客户端程序:
# echo_server2
➜ build git:(main) ✗ >./bin/echo_server2 11233
waiting connection to accept...
Connected client[1]: 127.0.0.1:61063
client[1] closed
# echo_client2
➜ build git:(main) ✗ >./bin/echo_client2 127.0.0.1 11233
connect server success...
Input(Q to quit): aa
Message from server: aa
Input(Q to quit): q
quit...
close socket and sleep 180 seconds now... ➜ build git:(main) ✗ >
监测脚本输出内容如下:
➜ > sh check-socket-state.sh
2022-07-10 17:03:20
...
...
2022-07-10 17:03:47
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 *.11233 *.* LISTEN
...
...
2022-07-10 17:03:54
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 localhost.11233 localhost.64426 ESTABLISHED
tcp4 0 0 localhost.64426 localhost.11233 ESTABLISHED
tcp4 0 0 *.11233 *.* LISTEN
...
...
2022-07-10 17:03:55
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 localhost.11233 localhost.64426 ESTABLISHED
tcp4 0 0 localhost.64426 localhost.11233 ESTABLISHED
tcp4 0 0 *.11233 *.* LISTEN
2022-07-10 17:03:55
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 localhost.11233 localhost.64426 CLOSE_WAIT
tcp4 0 0 localhost.64426 localhost.11233 FIN_WAIT_2
tcp4 0 0 *.11233 *.* LISTEN
...
...
2022-07-10 17:04:05
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 localhost.11233 localhost.64426 CLOSE_WAIT
tcp4 0 0 localhost.64426 localhost.11233 FIN_WAIT_2
tcp4 0 0 *.11233 *.* LISTEN
2022-07-10 17:04:05
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 *.11233 *.* LISTEN
tcp4 0 0 localhost.64426 localhost.11233 TIME_WAIT
...
...
2022-07-10 17:04:35
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 *.11233 *.* LISTEN
tcp4 0 0 localhost.64426 localhost.11233 TIME_WAIT
2022-07-10 17:04:35
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 *.11233 *.* LISTEN
...
...
这里笔者在输出中将每列的含义也补充上去了,可以看到:
1)服务端程序没启动之前,一条网络连接也没有;
2)服务端启动之后,开始有了监听 Socket,状态为 LISTEN;
3)双方建立连接后,又多了两条网络连接,并且状态为 ESTABLISHED;
4)当客户端先关闭连接后,对应的服务端连接状态变成了 CLOSE_WAIT,而客户端自己变成了 FIN_WAIT_2,这其实就是延迟服务端调用 close(client_socket)
产生的效果;
5)过了 10 秒后,对应的服务端连接关闭消失了,但监听的 Socket 一直在,此时客户端连接状态为 TIME_WAIT;
6)再过了 30 秒左右,客户端连接也关闭消失了,最后只剩下服务端的监听 Socket。
通过上面的演示,再回过头去看前面四次挥手的流程图,是不是开始有点感觉了。四次挥手的流程比较长,这就会导致每个中间状态都可能出现异常:
-
A 发 FIN 报文之前,B 已经跑路了(假设停电宕机,下同),当然 A 不知道 B 跑路了:
A 停在 FIN_WAIT_1 状态,会不断重发 FIN 报文,直到超时。
-
A 发 FIN 报文后,B 没来得及回复 ACK 就跑路了:
同上。
-
A 发 FIN 报文后,B 回复 ACK 后,B 跑路:
A 停在 FIN_WAIT_2 状态,TCP 规范未对该状态进行处理,但是 Linux 通过
/proc/sys/net/ipv4/tcp_fin_timeout
来控制 FIN 包的最长等待时间,过了这个时间,就强制关闭。tcp_fin_timeout (integer; default: 60; since Linux 2.2)
This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed. This is strictly a violation of the TCP specification, but required to prevent denial-of-service attacks. In Linux 2.2, the default value was 180.
-
B 开始发送 FIN 包,B 的状态由 CLOSE_WAIT 变成 LAST_ACK,A 收到后,从 FIN_WAIT_2 变成 TIME_WAIT,如果此时 A 跑路:
B 不断重发 FIN 报文,直到超时。
-
…
如果拆的更细,这里可以有更多,这里就不一一列举了,重要的是知道如何去分析。比如,A 为啥要在 TIME_WAIT 状态等待 2MSL 的时长,一个 MSL 又是多长时间?
魔鬼:TIME_WAIT
我们可能会遇到过这样的问题,某台机器存在大量 TIME_WAIT 状态的 Socket 连接没有关闭,导致机器响应慢,TPS/QPS 非常低,这是什么原因导致的?
在回答这个问题之前,首先需要回答的是:为啥要有 TIME_WAIT 这个状态?
TIME_WAIT 的作用
其实,按照前面的图示,当 B 发送 FIN 包,A 回复了 ACK 后,A 是可以跑路的,但这样会带来如下两个问题:
第一,A 跑路(即 A 状态从 FIN_WAIT_2 直接变成了 CLOSED),如果 B 没有收到 ACK 呢?前面也提到,B 会一直重发,直到报错。
所以,A 再等一等,如果 B 没收到 ACK,B 会再发 FIN 包,此时 A 会再次回复 ACK。简单来说,就是容错处理,保证被动关闭方 B 可以正常关闭。如果 A 啥也不管只管跑路,那么 B 可能只会收到一个 RST 报文,从而导致 B 出现错误。
第二,如果 A 跑路后,端口又被 A1 程序占用了(即 TCP 连接的四元组是相同的:source_ip, source_port, dest_ip, dest_port),那么 B 发的包可能就被 A1 接收了。
一般情况下,数据包的序列号会重新生成,但这里多加一道保险是有益处的,防止产生混乱。而等待 2MSL 时长,就能保证 B 之前发出的包都死翘翘了。
那这里的 MSL 又是啥?为何等待 2MSL 时长,B 发出的包都死翘翘了?
MSL(maximum segment lifetime),最长分节生命周期,这个翻译笔者感觉也有些别扭,我们这里还继续使用英文标识。为了解决上面的问题,TCP 便设计出了这样一个机制:
假设 t1 为收到数据包 p1 的时间戳,t2 为发送 p1 的时间戳,如果:
t1 - t2 > MSL
那么,p1 将被视为无效的数据包,被接收方丢弃。
需要额外说明的是,这里 2MSL 时长计时是从主机 A(主动关闭方) 接收到主机 B (被动关闭方)FIN 报文后发送 ACK 回应开始的。如果 2MSL 内 A 又收到 B 的 FIN 包,A 还会重新发 ACK,并且计时被重置,即重新开始 2MSL 的计时;如果超过 2MSL 的时长,A 再收到 B 的 FIN 包,此时 A 已经进入 CLOSED 状态,B 会收到一个 RST 包,表示要强制断开连接了。
为啥是 2MSL ,而不是 3MSL,4MSL? 这是因为 1MSL 是一个数据包在网络上的最大生存周期,而一来一回正好就是 2MSL,即来的 FIN 包最大时长(1MSL)加上去的 ACK 包最大时长(1MSL),本质上就是允许超时一次。
那么 1MSL 一般是多长呢?
协议规定 MSL 为 2 分钟,但在工程使用中,一般为 30 秒,1 分钟或 2 分钟。以 Linux 为例,有一个硬编码字段 TCP_TIMEWAIT_LEN
,在 Linux 内核 4.13.1 代码中定义如下:
// include/net/tcp.h
// ...
// ...
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
* state, about 60 seconds */
#define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LEN
/* BSD style FIN_WAIT2 deadlock breaker.
* It used to be 3min, new value is 60sec,
* to combine FIN-WAIT-2 timeout with
* TIME-WAIT timer.
*/
// ...
// ...
从代码中可以看到,TIME_WAIT 状态最大时长为 60 秒,那么 1MSL 就是 30 秒。另外,上文中提到的 TCP_FIN_TIMEOUT
也在这里定义了,其值与 TCP_TIMEWAIT_LEN
相同。
为啥
TCP_FIN_TIMEOUT
与TCP_TIMEWAIT_LEN
要相同?其实道理与前面提到的是一样的,就是允许报文丢失一次,如果报文在 1MSL 内没有收到,那就再给一次机会。但超过 2MSL 还没收到,那就不管了,如果继续容错,其所耗费的成本太高,是不值得的。
而前面提到的时间戳,通过抓包可以看到,在 TCP 头部的可选项(Options)中就有,其中 TSval (Timestamp value) 为当前包的发送时间,而 TSecr (Timestamp echo reply) 为对方上一个包的发送时间。
关于时间戳的更多内容可以查阅 RFC7323[1] ,另外这一篇《How TCP Works - The Timestamp Option》[2]文章依据 Wireshark 的抓包数据对时间戳进行了解读,推荐阅读。
MSL vs TTL
说到TCP 层的 MSL ,一般都会提到 IP 层的 TTL,这二者有什么关系?
我们先看一个抓包数据:
从截图中可以看到,在 IP 的头部有一个 TTL (Time to Live) 属性,这个字段的值一开始表示的是秒数,即数据包在网络上的生存期限。而现在的意思是指它最大能经过的路由数,每经过一个路由,数值就减 1,当值到 0 时,数据包将会被丢弃。这其实是为了防止路由环路而设计的,这个字段被设计为 8 bit,可以表示 0 ~ 255 ,即最多不会超过 255 个路由。通过抓包可以看到,当前这个值为 64,也就是说经过 64 个路由,数据包如果还没有到达目的地,将会被丢弃,同时会发送一个 ICMP 报文[3](ICMP 超时报文,错误号为 0)通知源主机。
而 MSL 计算的是时间,所以不要把这二者搞混[4],TTL 是 IP 层的概念,而 MSL 是 TCP 层的概念。
TIME_WAIT 的危害
从前面的状态图可以看到,只有主动发起连接关闭的一端才会出现 TIME_WAIT 状态,而不是双方。
说到大量 TIME_WAIT 状态的 TCP 连接的危害,主要有两点:
- 占用内存资源,因为内核需要保存这些数据结构的状态,但这还不是最关键的;
- 危害最大的,是这些连接将主机上的端口都占用完了,这样就无法创建新的连接。
从 TCP 的头部,我们知道端口占用 2 byte,也就是 16 bit,所以,端口号的取值范围是 0 ~ 65555。但有很多端口已经被默认占用了[5],作为 TCP/UDP 网络连接的本地端口,一般的取值范围为 32768 ~ 61000。当然,在 Linux 上可以通过 net.ipv4.ip_local_port_range
进行指定,比如在笔者的一台云 Linux 机器上,其默认配置为 32768 ~ 60999:
[root@i-mluwuwl1]/proc/sys/net/ipv4# hostnamectl
Static hostname: i-mluwuwl1
Icon name: computer-vm
Chassis: vm
Machine ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Boot ID: xxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyy
Virtualization: kvm
Operating System: CentOS Linux 8
CPE OS Name: cpe:/o:centos:centos:8
Kernel: Linux 4.18.0-305.19.1.el8_4.x86_64
Architecture: x86-64
[root@i-mluwuwl1]/proc/sys/net/ipv4#
[root@i-mluwuwl1]/proc/sys/net/ipv4# cat ip_local_port_range
32768 60999
[root@i-mluwuwl1]/proc/sys/net/ipv4#
The
/proc/sys/net/ipv4/ip_local_port_range
defines the local port range that is used by TCP and UDP traffic to choose the local port.[6]
解决方案
了解了 TIME_WAIT 状态的危害,那我们要如何避免呢?
目前主要有以下几个方案:
1、调低 net.ipv4.tcp_max_tw_buckets
的上限
tcp_max_tw_buckets - INTEGER
Maximal number of timewait sockets held by system simultaneously.
If this number is exceeded time-wait socket is immediately destroyed
and warning is printed. This limit exists only to prevent
simple DoS attacks, you must not lower the limit artificially,
but rather increase it (probably, after increasing installed memory),
if network conditions require more than default value.[7]tcp_max_tw_buckets (integer; default: see below; since Linux 2.4)
The maximum number of sockets in TIME_WAIT state allowed
in the system. This limit exists only to prevent simple
denial-of-service attacks. The default value of NR_FILE*2
is adjusted depending on the memory in the system. If
this number is exceeded, the socket is closed and a
warning is printed.[8]
tcp_max_tw_buckets
参数主要是为了防止 DoS 攻击,一旦 TIME_WAIT 状态的连接超过该数值,系统将会把这些连接强制关闭,并只打印出警告信息。
上述官方文档的说明,其实已经给出了答案:不建议调低系统的默认值,如果增加了内存,可以适当调高该值。该方案过于暴力,治标不治本,不予考虑。
笔者在几台 Linux 机器上查阅了对应的值,不尽相同,有的默认值为 5000,有的为 32768,官方给出的默认值是 NR_FILE*2。那这个 NR_FILE 又是啥呢?笔者也不了解,欢迎内行的小伙伴们补充。
NR_FILE is the limit on total number of files in the system at any given point in time.
2、调低 TCP_TIMEWAIT_LEN
,重新编译操作系统
关于 TCP_TIMEWAIT_LEN
参数,前面已经给出了说明,其实就是降低 TIME_WAIT 状态的等待时长,比如从 60 秒,调整为 30 秒,但这个方法耗费的成本太高了。如果去编译操作系统,大概率的情况是问题没解决,还会带来一堆新问题。
3、设置 Socket 选项,强行关闭
在 socket.h
中有一个 int setsockopt(int, int, int, const void *, socklen_t)
函数,通过设置参数,可以调整 close(socket)
或 shutdown(socket, how)
关闭连接时的行为。
通过设置,可以在 close(socket)
后,立刻发送一个 RST 包给对方,强制关闭当前 TCP 连接,跳过四次挥手,如此一来也就跳过了 TIME_WAIT 状态。由于被动方不知道对方已经关闭,只有接收数据或发送数据时,才知道对方已经跑路了。
笔者将在下一篇学习笔记中,给出具体的实战示例,结合代码来进一步理解。跟前面的方案 1 一样,强行关闭不是好办法,不予推荐。
4、启用 net.ipv4.tcp_tw_reuse
通过名字也能看出来,就是复用处于 TIME_WAIT 状态的 Socket 为新的连接所用。
tcp_tw_reuse (Boolean; default: disabled; since Linux 2.4.19/2.6)
Allow to reuse TIME_WAIT sockets for new connections when
it is safe from protocol viewpoint. It should not be
changed without advice/request of technical experts.
官方建议不要轻易修改,除非有专家建议,另外这个值应在“safe from protocol viewpoint”的情况下才考虑使用。那什么是从协议角度理解的安全可控呢?主要有两点:
- C/S 模型中的客户端,即连接发起方;
- 对应 TIME_WAIT 状态的连接的创建时间超过 1 秒才可以被复用。
使用这个选项还有一个前提,就是启用对 TCP 时间戳的支持(双方都要打开),即 net.ipv4.tcp_timestamps=1
,不过该值默认就是 1。
tcp_timestamps (integer; default: 1; since Linux 2.2)
Set to one of the following values to enable or disable
RFC 1323 TCP timestamps[9]:0 Disable timestamps.
1 Enable timestamps as defined in RFC1323 and use random
offset for each connection rather than only using the current time.2 As for the value 1, but without random offsets.
Setting tcp_timestamps to this value is meaningful since Linux 4.10.
最后补充一下,还有一个参数 net.ipv4.tcp_tw_recycle
,如果启用它,将会对 TIME_WAIT 状态的 Socket 进行快速回收,它对服务端和客户端都能起作用,但这个参数也会带来新的问题(比如造成端口接收数据混乱,NAT 环境下的丢包等),所以在 Linux 4.12 内核中又将其去掉了[10],故也不予考虑。
tcp_tw_recycle (Boolean; default: disabled; Linux 2.4 to 4.11)
Enable fast recycling of TIME_WAIT sockets. Enabling this
option is not recommended as the remote IP may not use
monotonically increasing timestamps (devices behind NAT,
devices with per-connection timestamp offsets). See RFC
1323 (PAWS) and RFC 6191.
综上所述,如果不到万不得已,最好的方案还是启用 tcp_tw_reuse
。
在下一篇学习笔记中,笔者将继续讨论如何优雅地关闭 TCP 连接。
参考资料
- 图解TCP/IP(第5版),by [日]竹下隆史 / [日]村山公保 / [日]荒井透 / [日]苅田幸雄
- 趣谈网络协议,by 刘超
- TCP/IP网络编程,by [韩] 尹圣雨
- 网络编程实战,by 盛延敏
https://www.networkdatapedia.com/post/2018/10/08/how-tcp-works-the-timestamp-option ↩︎
ICMP 全称为 Internet Control Message Protocol,互联网控制报文协议,是网络层的一个协议,其主要功能包括:确认 IP 包是否成功送达目的主机,通知传输过程中 IP 包被废弃的原因,改善网络设置等。ICMP 一个典型的应用是
ping
命令,笔者在后续的学习笔记中,也会对 ICMP 进行梳理和总结。 ↩︎https://networkengineering.stackexchange.com/questions/68468/is-tcps-msl-value-equivalent-to-ips-ttl-value ↩︎
https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers ↩︎
https://tldp.org/LDP/solrhe/Securing-Optimizing-Linux-RH-Edition-v1.3/chap6sec70.html ↩︎
https://github.com/torvalds/linux/blob/v5.18/Documentation/networking/ip-sysctl.rst ↩︎
这里的 RFC 1323 已经被前面提到的 RFC 7323 所替代。 ↩︎
https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=4396e46187ca5070219b81773c4e65088dac50cc ↩︎