【网络】简单聊一下 TIME_WAIT

2023-12-14 00:32:05

问题背景

笔者在看自己服务状态数据的时候,会发现有很多 tcp 的连接,也会发现有很多处于不同状态下的 tcp 连接,TIME_WAIT 的连接数有83个,为了弄清楚这个 TIME_WAIT 是什么,整理了下面的笔记用于梳理概念
在这里插入图片描述

基础流程

TCP的三次握手和四次挥手是TCP协议建立和终止连接的基本过程。

在这里插入图片描述

三次握手过程如下:

  1. 客户端发送SYN包(同步序列编号)给服务器,等待服务器确认。
  2. 服务器收到SYN包后,会确认客户的SYN(发送一个ACK),同时也会发送一个SYN包,这个步骤称为SYN+ACK。
  3. 客户端收到服务器的SYN+ACK后,还需向服务器发送确认包ACK。至此,完成三次握手,客户端和服务器开始传送数据。

四次挥手过程如下:

  1. 当数据传送完毕后,客户端会发送一个FIN包给服务器,请求关闭连接。
  2. 服务器收到FIN包后,会发送一个ACK给客户端,确认接收到了FIN包,但不会立即关闭连接,因为服务器可能还有数据需要处理和发送。
  3. 当服务器数据发送完毕后,会向客户端发送FIN包,请求关闭连接。
  4. 客户端收到FIN包后,会发送一个ACK给服务器,然后进入TIME_WAIT状态。TIME_WAIT状态持续一段时间后,如果没有再收到服务器的消息,那么就关闭连接。至此,完成四次挥手。

为什么有 TIME_WAIT

TIME_WAIT状态在TCP四次挥手的最后阶段出现。在TCP连接被关闭后,操作系统会等待一段时间(通常是2倍的最大分段生存时间),以确保对方收到了关闭连接的确认。这个等待时间就是TIME_WAIT。对于复杂的网络状态,TCP 的实现提出了多种应对措施,TIME_WAIT 状态的提出就是为了应对其中一种异常状况。 此状态的存在主要有两个原因:

  1. 保证最后一个确认消息能被对方收到。如果直接关闭连接,那么对方可能会因为没有收到确认消息而无法关闭连接。
  2. 避免“旧的重复分组”在新的连接中被错误接收。由于网络原因,有可能会有一些旧的重复分组在网络中滞留,如果直接开启新的连接,这些旧的重复分组可能会被新的连接误认为是自己的数据。

为了理解 TIME_WAIT 状态的必要性,我们先来假设没有这么一种状态会导致的问题。暂以 A、B 来代指 TCP 连接的两端,A 为主动关闭的一端。

  • 四次挥手中,A 发 FIN, B 响应 ACK,B 再发 FIN,A 响应 ACK 实现连接的关闭。而如果 A 响应的 ACK 包丢失,B 会以为 A 没有收到自己的关闭请求,然后会重试向 A 再发 FIN 包。

    如果没有 TIME_WAIT 状态,那么A回复ACK立刻关闭,所以B重发的会让A响应重置,A 不再保存这个连接的信息,收到一个不存在的连接的包,A 会响应 RST 包,导致 B 端异常响应。

    此时, TIME_WAIT 是为了保证全双工的 TCP 连接正常终止。

  • 我们还知道,TCP 下的 IP 层协议是无法保证包传输的先后顺序的。如果双方挥手之后,一个网络四元组(src/dst ip/port)被回收,而此时网络中还有一个迟到的数据包没有被 B 接收,A 应用程序又立刻使用了同样的四元组再创建了一个新的连接后,这个迟到的数据包才到达 B,那么这个数据包就会让 B 以为是 A 刚发过来的。

    此时, TIME_WAIT 的存在是为了保证网络中迷失的数据包正常过期。

不同场景下的 TIME_WAIT 的具体影响

基于不同的使用场景,我们一般区分为“长连接”和“短连接”

长链接

长连接,指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。

是一种在客户端和服务器之间维持长时间连接的通信机制。与传统的短链接请求-响应模型不同,长链接允许客户端向服务器发出请求并保持连接打开,以等待服务器在有数据更新时立即响应。

长链接的工作原理通常如下:

  1. 客户端发起连接请求: 客户端向服务器发送一个连接请求。
  2. 服务器保持连接打开: 服务器接收到连接请求后,保持连接打开并等待数据变化或其他事件的发生。
  3. 服务器响应: 当服务器有数据更新或满足其他条件时,它立即响应客户端请求,并发送数据给客户端。
  4. 客户端处理响应: 客户端接收到服务器的响应后,可以处理数据更新或执行其他操作。
  5. 连接保持开放: 连接保持开放,客户端和服务器可以继续在连接上进行通信。

长连接的优点是:

这种长链接机制的优势在于实时性更强,因为服务器可以立即将更新推送给客户端,而无需等待客户端发起新的请求。这对于实时通信、即时消息推送以及需要及时获取数据更新的应用场景非常有用。

  • 减少了建立连接和断开连接的开销,提高了传输效率。
  • 减少了连接数,节省了服务器资源。
  • 便于维护和管理。

长连接的缺点是:

长链接也有一些缺点,例如在一些网络环境中可能存在连接超时的问题,而且在维持大量长连接时可能增加服务器的负载。因此,开发人员需要根据具体的应用场景和需求来选择适当的通信机制。

  • 可能会导致连接泄漏,造成资源浪费。
  • 可能会导致连接超时,影响传输效率。

长连接的应用场景包括:

  • 聊天软件:客户端和服务器之间需要保持长连接,以便及时交换消息。
  • 文件传输:服务器和客户端之间需要保持长连接,以便传输大文件。
  • 实时数据传输:服务器和客户端之间需要保持长连接,以便实时传输数据。
package main

import (
	"fmt"
	"net"
	"time"
)

func handleLongConnection(conn net.Conn) {
	defer conn.Close()

	for {
		// 读取客户端发送的数据
		buffer := make([]byte, 1024)
		_, err := conn.Read(buffer)
		if err != nil {
			fmt.Println("Error reading:", err)
			return
		}

		// 处理数据
		fmt.Println("Received:", string(buffer))

		// 模拟长连接,等待一段时间再回复客户端
		time.Sleep(time.Second * 5)

		// 向客户端发送响应
		response := []byte("Server response")
		conn.Write(response)
	}
}

func main() {
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("Error listening:", err)
		return
	}
	defer listener.Close()

	fmt.Println("Server listening on :8080 for long connection")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Error accepting connection:", err)
			continue
		}

		// 启动长连接处理协程
		go handleLongConnection(conn)
	}
}

短连接

短连接是指在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。

在计算机网络中,短连接(short connection)是指在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。

短连接的优点是**:**

  1. 资源释放: 短连接在完成数据传输后会立即释放资源,不需要维持长时间的连接状态,因此能够更有效地释放系统资源。
  2. 简单易实现: 短连接模型相对简单,易于实现和维护。每个请求都是独立的,不需要维持连接状态。
  3. 连接灵活性: 短连接适用于一些场景,如HTTP请求,每次请求都是独立的,适合短暂的数据交互。
  4. 适用于并发: 短连接模型适用于并发连接,因为每个连接都是独立的,不会影响其他连接。

短连接的缺点是:

  1. 连接建立开销: 每次建立连接都需要进行握手过程,包括TCP的三次握手,这会增加网络开销。
  2. 频繁的连接断开和建立: 对于高频率的短连接,频繁的连接断开和建立可能会增加系统开销,尤其是在高并发的情况下。
  3. 维护开销: 如果应用中存在频繁的连接建立和断开,服务器需要维护大量的连接状态信息,可能会增加服务器的负担。
  4. 实时性差: 短连接可能无法满足实时性要求较高的应用场景,因为连接建立和断开的开销可能影响数据的实时传输。

短连接的应用场景包括:

  • 浏览器和服务器之间的HTTP请求。
  • 邮件服务器和客户端之间的SMTP/POP3/IMAP协议。
  • 聊天软件客户端和服务器之间的聊天协议。
package main

import (
	"fmt"
	"net"
)

func handleShortConnection(conn net.Conn) {
	defer conn.Close()

	// 读取客户端发送的数据
	buffer := make([]byte, 1024)
	_, err := conn.Read(buffer)
	if err != nil {
		fmt.Println("Error reading:", err)
		return
	}

	// 处理数据
	fmt.Println("Received:", string(buffer))

	// 向客户端发送响应
	response := []byte("Server response")
	conn.Write(response)
}

func main() {
	listener, err := net.Listen("tcp", ":8081")
	if err != nil {
		fmt.Println("Error listening:", err)
		return
	}
	defer listener.Close()

	fmt.Println("Server listening on :8081 for short connection")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Error accepting connection:", err)
			continue
		}

		// 启动短连接处理
		handleShortConnection(conn)
	}
}

正常的TCP客户端连接在关闭后,会进入一个TIME_WAIT的状态,持续的时间一般在1~4分钟,如果短时间内(例如1s内)进行大量的短连接,则可能出现这样一种情况:客户端所在的操作系统的socket端口和句柄被用尽,系统无法再发起新的连接!如果 TIME_WAIT 连接过多,会消耗大量的系统资源,会耗尽可用的网络端口,从而阻止新的连接建立。因此,对于处于 TIME_WAIT 状态的连接,需要进行合理的管理和控制。

举例来说:假设每秒建立了1000个短连接(Web场景下是很常见的,例如每个请求都去访问),假设TIME_WAIT的时间是1分钟,则1分钟内需要建立6W个短连接,由于TIME_WAIT时间是1分钟,这些短连接1分钟内都处于TIME_WAIT状态,都不会释放,而Linux默认的本地端口范围配置是:net.ipv4.ip_local_port_range = 32768 ~ 61000 不到3W,因此这种情况下新的请求由于没有本地端口就不能建立了。

缓解办法

代码侧

GOLANG

go 里面 Transport 默认参数有个 MaxIdleConns 是100,还有 Timeout 是 90s,这两个参数会导致 client 自动发起 FIN,代码侧可以优化这边的逻辑,不同的语言应该有对应的优化方法。

在这里插入图片描述

系统侧

在TCP连接中,TIME_WAIT状态是在连接关闭后等待一段时间的状态,以确保对方收到了最后的ACK。这是为了处理网络中的滞留数据报文(可能在网络中延迟到达),防止它们被新的连接误认为是旧的连接的问题。

降低TIME_WAIT状态对于提高连接的重用速度是有风险的,因为它可能导致旧的数据报文在网络中被混淆。然而,如果你仍然想要降低TIME_WAIT状态的等待时间,可以在系统上进行一些调整。请注意,对于这样的操作,你应该非常小心,因为它可能会对网络稳定性产生负面影响。

在Linux系统上,你可以通过修改内核参数来调整TIME_WAIT状态的等待时间。

  • net.ipv4.tcp_max_tw_buckets:指定系统同时保持 TIME_WAIT 的最大数量。
  • net.ipv4.tcp_tw_reuse:如果设置为 1,则允许 TIME_WAIT 状态的连接被重用。
  • net.ipv4.tcp_tw_recycle:如果设置为 1,则允许 TIME_WAIT 状态的连接在 FIN_WAIT-2 状态时被快速回收。

以下是一些可能的方法:

方法一:通过sysctl修改

通过sysctl可以动态地调整内核参数。

sudo sysctl -w net.ipv4.tcp_tw_reuse=1
sudo sysctl -w net.ipv4.tcp_tw_recycle=1
  • net.ipv4.tcp_tw_reuse=1 允许将TIME_WAIT状态的连接端口重用。
  • net.ipv4.tcp_tw_recycle=1 尝试根据时间戳来快速回收TIME_WAIT状态的连接。

这种方法修改是暂时的,重启后会失效。

方法二:修改sysctl配置文件

将上述配置添加到 /etc/sysctl.conf 文件中,以便在系统启动时应用。

echo "net.ipv4.tcp_tw_reuse=1" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_tw_recycle=1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

这种方法在系统重启后仍然有效。

请注意,在一些情况下,使用 net.ipv4.tcp_tw_recycle 可能会导致问题,因为它可能与一些网络设备不兼容。在生产环境中,修改这些参数前应该进行充分测试,以确保它们不会对系统的稳定性和性能产生负面影响。

总的来说,TIME_WAIT 是TCP连接管理中的一个重要环节,尽管它可能会引发一些问题,但适当的管理和调优可以最大程度地减轻这些问题的影响,保证网络连接的正常运行。

总结

在 Go 中,HTTP客户端(例如**http.Client**)发起的HTTP请求的完成过程可能涉及到TCP连接的正常关闭,从而触发发送TCP FIN(Finish)标志。这是正常的TCP连接关闭行为,其触发条件包括:

  1. HTTP请求完成: 当HTTP请求完成(例如,成功获取到响应或发生错误)时,客户端的**http.Transport**可能会将连接放回到连接池以便复用,或者决定是否关闭连接。
  2. 连接池空闲时: 如果连接池中没有空闲的连接,并且该连接也没有被标记为永久保持活跃(通过**Transport.DisableKeepAlives**设置),那么连接可能会被关闭。
  3. 达到最大空闲连接数: 如果连接池中的连接数量达到了 http.TransportMaxIdleConnsMaxIdleConnsPerHost 设置的最大空闲连接数,那么一些连接可能会被关闭。
  4. 连接过期: http.Transport 会根据连接的空闲时间进行管理。如果连接在一段时间内未被使用,可能会被关闭。

这些操作是为了维护连接池的健康状态,确保连接的新鲜性,并防止长时间空闲的连接占用资源。触发TCP FIN标志是连接关闭的一部分,用于通知对端连接即将关闭。

引用

CHATGPT

BARD

https://www.xiaolincoding.com/network/3_tcp/tcp_tcpdump.html#解密-tcp-三次握手和四次挥手

https://claire-chang.com/2020/03/01/tcp連線階段與time_wait意義/

https://zhenbianshu.github.io/2018/12/talk_about_tcp_timewait.html

文章来源:https://blog.csdn.net/wanto4rom/article/details/134889141
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。