TCP连接建立与断开过程(三次握手与四次挥手)
2021-01-193.1k 阅读
TCP 协议概述
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。在网络通信中,它确保数据能准确无误地从发送端传输到接收端。TCP 协议在现代网络应用中起着举足轻重的作用,像 HTTP、SMTP、FTP 等许多重要的应用层协议都依赖 TCP 来提供可靠的数据传输服务。
TCP 连接建立 - 三次握手
三次握手的过程
- 第一次握手:客户端向服务器发送一个 SYN(Synchronize Sequence Numbers,同步序列号)包,其中包含客户端初始序列号(Initial Sequence Number,ISN),假设为
x
。这个包的作用是告诉服务器客户端想要建立连接,并告知自己的初始序列号。此时客户端进入SYN_SENT
状态。 - 第二次握手:服务器接收到客户端的 SYN 包后,会向客户端发送一个 SYN + ACK 包。这个包中,SYN 部分包含服务器的初始序列号
y
,ACK 部分是对客户端 SYN 包的确认,确认号为x + 1
。服务器发送这个包表示它收到了客户端的连接请求,并同意建立连接。此时服务器进入SYN_RCVD
状态。 - 第三次握手:客户端收到服务器的 SYN + ACK 包后,向服务器发送一个 ACK 包。这个 ACK 包的确认号为
y + 1
,序列号为x + 1
。此包用于确认收到服务器的 SYN + ACK 包。当服务器收到这个 ACK 包后,连接正式建立,双方都进入ESTABLISHED
状态。
为什么是三次握手
- 确保双方序列号同步:通过三次握手,客户端和服务器都能确认对方的初始序列号,从而在后续的数据传输中能够准确地对数据进行编号和确认,保证数据的有序性和准确性。
- 防止旧连接的干扰:如果是两次握手,假设网络中存在一个旧的 SYN 包在网络中滞留,当这个旧包被服务器接收时,服务器会认为是新的连接请求并发送 SYN + ACK 包。但客户端可能并没有发起新的连接,这样就会造成错误连接。而三次握手可以避免这种情况,因为客户端在收到服务器的 SYN + ACK 包后,会根据自己是否发起过连接请求来决定是否发送 ACK 包。如果是旧的 SYN 包,客户端不会发送 ACK 包,从而不会建立错误连接。
三次握手的代码示例(以 Python 为例)
import socket
# 创建客户端 socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 12345)
# 尝试连接服务器(三次握手在此处发生)
try:
client_socket.connect(server_address)
print("三次握手成功,连接已建立")
# 在这里可以进行数据发送和接收
client_socket.sendall(b'Hello, Server!')
data = client_socket.recv(1024)
print("收到服务器响应:", data.decode())
except socket.error as e:
print("连接失败:", e)
finally:
client_socket.close()
import socket
# 创建服务器 socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 12345)
server_socket.bind(server_address)
server_socket.listen(1)
print("等待客户端连接...")
while True:
try:
client_socket, client_address = server_socket.accept()
print("客户端已连接:", client_address)
data = client_socket.recv(1024)
print("收到客户端消息:", data.decode())
client_socket.sendall(b'Hello, Client!')
except socket.error as e:
print("处理客户端连接错误:", e)
finally:
client_socket.close()
上述代码中,客户端通过 connect
方法发起连接请求,服务器通过 accept
方法接受连接。这两个操作背后实际上执行了 TCP 的三次握手过程。
TCP 连接断开 - 四次挥手
四次挥手的过程
- 第一次挥手:主动关闭方(假设为客户端)向被动关闭方(服务器)发送一个 FIN(Finish,结束)包,表示客户端想要关闭连接。此时客户端进入
FIN_WAIT_1
状态。 - 第二次挥手:服务器接收到客户端的 FIN 包后,会向客户端发送一个 ACK 包,确认收到客户端的 FIN 包。此 ACK 包的确认号为客户端 FIN 包序列号加 1。此时服务器进入
CLOSE_WAIT
状态,而客户端收到这个 ACK 包后进入FIN_WAIT_2
状态。 - 第三次挥手:服务器在处理完自己的数据后,向客户端发送一个 FIN 包,表示服务器也准备好关闭连接。此时服务器进入
LAST_ACK
状态。 - 第四次挥手:客户端收到服务器的 FIN 包后,向服务器发送一个 ACK 包,确认收到服务器的 FIN 包。此 ACK 包的确认号为服务器 FIN 包序列号加 1。客户端发送完这个 ACK 包后进入
TIME_WAIT
状态,而服务器收到 ACK 包后就进入CLOSED
状态。客户端在TIME_WAIT
状态停留一段时间(2MSL,Maximum Segment Lifetime,最长报文段寿命)后也进入CLOSED
状态。
为什么是四次挥手
- TCP 全双工特性:由于 TCP 是全双工通信,即数据可以在两个方向同时传输。当一方(如客户端)发送 FIN 包表示不再发送数据时,另一方(服务器)可能还在传输数据。所以服务器不能立即关闭连接,而是先发送 ACK 包确认收到客户端的 FIN 包,等自己数据传输完毕后再发送 FIN 包。这样就形成了四次挥手的过程。
- 确保数据传输完成:四次挥手可以保证在连接关闭之前,双方所有的数据都能正确传输和确认。特别是在网络存在延迟等情况下,通过这种方式可以避免数据丢失。
四次挥手的代码示例(以 Python 为例)
import socket
# 创建客户端 socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 12345)
client_socket.connect(server_address)
try:
client_socket.sendall(b'Hello, Server!')
data = client_socket.recv(1024)
print("收到服务器响应:", data.decode())
# 客户端发起关闭连接请求(第一次挥手)
client_socket.shutdown(socket.SHUT_WR)
print("客户端已发送 FIN 包")
# 等待服务器响应(第二次挥手的 ACK 包)
data = client_socket.recv(1024)
print("收到服务器 ACK 包:", data.decode())
# 等待服务器发送 FIN 包(第三次挥手)
data = client_socket.recv(1024)
print("收到服务器 FIN 包:", data.decode())
# 客户端发送 ACK 包(第四次挥手)
client_socket.sendall(b'ACK')
print("客户端已发送 ACK 包")
finally:
client_socket.close()
import socket
# 创建服务器 socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 12345)
server_socket.bind(server_address)
server_socket.listen(1)
print("等待客户端连接...")
while True:
try:
client_socket, client_address = server_socket.accept()
print("客户端已连接:", client_address)
data = client_socket.recv(1024)
print("收到客户端消息:", data.decode())
client_socket.sendall(b'Hello, Client!')
# 服务器接收客户端的 FIN 包(第一次挥手)
data = client_socket.recv(1024)
print("收到客户端 FIN 包:", data.decode())
# 服务器发送 ACK 包(第二次挥手)
client_socket.sendall(b'ACK')
print("服务器已发送 ACK 包")
# 服务器处理完数据后,发送 FIN 包(第三次挥手)
client_socket.shutdown(socket.SHUT_WR)
print("服务器已发送 FIN 包")
# 等待客户端的 ACK 包(第四次挥手)
data = client_socket.recv(1024)
print("收到客户端 ACK 包:", data.decode())
except socket.error as e:
print("处理客户端连接错误:", e)
finally:
client_socket.close()
在上述代码中,客户端通过 shutdown(socket.SHUT_WR)
方法发起关闭连接请求(第一次挥手),服务器依次处理来自客户端的 FIN 包并作出相应的 ACK 和 FIN 响应,模拟了 TCP 四次挥手的过程。
TCP 状态转换图
状态概述
- CLOSED:初始状态,表示没有任何连接。
- LISTEN:服务器端处于监听状态,等待客户端连接请求。
- SYN_SENT:客户端发送 SYN 包后进入此状态,等待服务器的 SYN + ACK 包。
- SYN_RCVD:服务器收到客户端的 SYN 包并发送 SYN + ACK 包后进入此状态,等待客户端的 ACK 包。
- ESTABLISHED:连接建立成功,双方可以进行数据传输。
- FIN_WAIT_1:主动关闭方(如客户端)发送 FIN 包后进入此状态,等待对方的 ACK 包。
- FIN_WAIT_2:主动关闭方收到对方的 ACK 包后进入此状态,等待对方的 FIN 包。
- CLOSE_WAIT:被动关闭方(如服务器)收到主动关闭方的 FIN 包并发送 ACK 包后进入此状态,等待自己处理完数据后发送 FIN 包。
- LAST_ACK:被动关闭方发送 FIN 包后进入此状态,等待主动关闭方的 ACK 包。
- TIME_WAIT:主动关闭方收到被动关闭方的 FIN 包并发送 ACK 包后进入此状态,停留 2MSL 时间后进入 CLOSED 状态。
状态转换过程
- 连接建立过程的状态转换:
- 客户端从
CLOSED
状态开始,发送 SYN 包后进入SYN_SENT
状态。 - 服务器从
CLOSED
状态开始,进入LISTEN
状态等待连接。收到客户端 SYN 包后,发送 SYN + ACK 包并进入SYN_RCVD
状态。 - 客户端收到服务器的 SYN + ACK 包后,发送 ACK 包并进入
ESTABLISHED
状态。服务器收到客户端的 ACK 包后也进入ESTABLISHED
状态。
- 客户端从
- 连接断开过程的状态转换:
- 主动关闭方(如客户端)发送 FIN 包,从
ESTABLISHED
状态进入FIN_WAIT_1
状态。 - 被动关闭方(如服务器)收到 FIN 包后,发送 ACK 包,从
ESTABLISHED
状态进入CLOSE_WAIT
状态。主动关闭方收到 ACK 包后进入FIN_WAIT_2
状态。 - 被动关闭方处理完数据后,发送 FIN 包,从
CLOSE_WAIT
状态进入LAST_ACK
状态。 - 主动关闭方收到 FIN 包后,发送 ACK 包,从
FIN_WAIT_2
状态进入TIME_WAIT
状态。被动关闭方收到 ACK 包后进入CLOSED
状态。主动关闭方在TIME_WAIT
状态停留 2MSL 时间后进入CLOSED
状态。
- 主动关闭方(如客户端)发送 FIN 包,从
深入理解三次握手和四次挥手的细节
初始序列号(ISN)的选择
- ISN 的作用:初始序列号用于标识 TCP 连接中发送的第一个字节的编号。在数据传输过程中,每个字节都有一个序列号,接收方可以根据序列号对数据进行排序和确认。ISN 的选择对于保证数据传输的准确性和安全性非常重要。
- 选择方式:在现代操作系统中,ISN 通常是一个随机数,这样可以防止攻击者通过猜测序列号来伪造 TCP 数据包。例如,Linux 系统中 ISN 是基于时钟和其他随机因素生成的。不同的操作系统可能有不同的 ISN 生成算法,但目的都是为了增加序列号的随机性和不可预测性。
重传机制在三次握手和四次挥手中的应用
- 三次握手重传:在三次握手过程中,如果客户端发送的 SYN 包或者服务器发送的 SYN + ACK 包没有得到对方的确认,发送方会在一定时间后重传该包。这个重传时间通常是动态调整的,初始值可能是一个固定值(如 3 秒),如果多次重传仍未成功,重传时间会逐渐加倍,以避免网络拥塞。
- 四次挥手重传:在四次挥手过程中,如果 FIN 包或者 ACK 包没有得到确认,同样会进行重传。例如,主动关闭方发送 FIN 包后,如果在规定时间内没有收到被动关闭方的 ACK 包,会重传 FIN 包。被动关闭方发送 FIN 包后,如果没有收到主动关闭方的 ACK 包,也会重传 FIN 包。重传机制保证了连接建立和断开过程的可靠性。
TIME_WAIT 状态的必要性
- 防止旧数据包干扰:在网络中,数据包可能会因为路由问题等原因在网络中滞留一段时间。当一个连接关闭后,可能存在旧的数据包仍然在网络中传输。如果新的连接使用了相同的端口号等信息,旧数据包可能会被错误地认为是新连接的数据包。客户端在
TIME_WAIT
状态停留 2MSL 时间,确保在这段时间内网络中所有旧的数据包都已经过期,不会干扰新的连接。 - 确保最后一个 ACK 包到达:在四次挥手的第四次挥手过程中,主动关闭方发送的 ACK 包可能会因为网络原因丢失。如果没有
TIME_WAIT
状态,主动关闭方直接进入CLOSED
状态,被动关闭方因为没有收到 ACK 包会重传 FIN 包,但此时主动关闭方已经关闭,无法接收这个 FIN 包,导致连接无法正常关闭。而TIME_WAIT
状态允许主动关闭方在发送 ACK 包后继续监听一段时间,确保能收到被动关闭方重传的 FIN 包并再次发送 ACK 包,从而保证连接正常关闭。
TCP 连接建立与断开过程中的常见问题及解决方法
连接超时问题
- 原因:在三次握手过程中,如果客户端发送的 SYN 包长时间没有得到服务器的响应,就会出现连接超时。这可能是由于网络故障、服务器负载过高、防火墙阻挡等原因导致。
- 解决方法:
- 增加客户端的重传次数和延长重传时间。可以通过调整操作系统的 TCP 参数(如
tcp_syn_retries
)来实现。 - 检查网络连接,确保网络畅通。可以使用
ping
命令等工具检测网络是否可达。 - 检查服务器状态,确保服务器没有因为负载过高而无法响应连接请求。可以通过监控服务器的 CPU、内存等资源使用情况来判断。
- 检查防火墙设置,确保 TCP 连接所需的端口没有被封禁。
- 增加客户端的重传次数和延长重传时间。可以通过调整操作系统的 TCP 参数(如
半连接状态问题
- 原因:在三次握手过程中,如果服务器收到客户端的 SYN 包并发送了 SYN + ACK 包,但没有收到客户端的 ACK 包,服务器就会处于半连接状态。这可能是由于客户端发送 ACK 包失败、网络延迟等原因导致。
- 解决方法:
- 服务器端可以设置一个半连接队列,当处于半连接状态的连接数超过一定阈值时,采取相应措施,如丢弃新的半连接请求或者加快重传 SYN + ACK 包的频率。
- 客户端确保 ACK 包能够成功发送,可以通过检查网络连接和调整 TCP 参数来提高 ACK 包发送的成功率。
四次挥手异常问题
- 原因:在四次挥手过程中,可能会出现各种异常情况,如 FIN 包丢失、ACK 包丢失等。这可能导致连接无法正常关闭,一方长时间处于
CLOSE_WAIT
或FIN_WAIT_2
等状态。 - 解决方法:
- 双方都要正确实现重传机制,确保 FIN 包和 ACK 包能够被对方正确接收。
- 对于长时间处于
CLOSE_WAIT
状态的一方,检查是否存在数据处理异常,导致无法及时发送 FIN 包。对于长时间处于FIN_WAIT_2
状态的一方,检查是否存在 ACK 包丢失的情况,可以适当延长等待时间或者主动重发 ACK 包。
不同操作系统对 TCP 连接建立与断开的实现差异
Linux 操作系统
- 三次握手实现:Linux 系统在实现三次握手时,对于 SYN 包的处理比较高效。它通过优化的算法来生成初始序列号,并且在重传机制上采用了动态调整重传时间的策略。例如,
tcp_syn_retries
参数可以控制 SYN 包的重传次数,默认值为 6 次,重传时间会随着重传次数增加而加倍。 - 四次挥手实现:在四次挥手过程中,Linux 系统严格遵循 TCP 协议规范。对于
TIME_WAIT
状态的处理,它会根据系统配置的tcp_fin_timeout
参数来决定在TIME_WAIT
状态停留的时间,默认值一般为 60 秒。同时,Linux 系统提供了一些参数(如tcp_tw_reuse
和tcp_tw_recycle
)来优化TIME_WAIT
状态的资源占用,但这些参数在某些网络环境下可能会导致问题,需要谨慎使用。
Windows 操作系统
- 三次握手实现:Windows 操作系统在三次握手实现上也遵循 TCP 协议标准。它同样会生成随机的初始序列号,但在重传机制上与 Linux 略有不同。Windows 系统的重传时间和次数可能会根据网络环境和系统配置动态调整。例如,在网络状况较差时,重传时间可能会相对较长,以避免不必要的重传。
- 四次挥手实现:在四次挥手方面,Windows 系统对
TIME_WAIT
状态的处理也有自己的特点。TIME_WAIT
状态的停留时间也可以通过系统注册表进行配置。与 Linux 不同的是,Windows 在处理CLOSE_WAIT
状态时,如果应用程序没有及时关闭连接,系统可能会采取一定的措施来强制关闭连接,以避免资源长时间占用。
其他操作系统
- macOS:macOS 在 TCP 连接建立与断开的实现上与 Linux 和 Windows 有一些相似之处,但也有自己的特点。例如,在初始序列号生成算法上,它会结合系统时钟和其他随机因素,以保证序列号的随机性。在
TIME_WAIT
状态处理上,macOS 也提供了一些参数供用户调整,以适应不同的网络环境。 - UNIX 系统:不同的 UNIX 系统在 TCP 实现上也存在一定差异。一些 UNIX 系统可能更注重性能优化,在三次握手和四次挥手过程中采用了更为高效的算法。例如,在重传机制上可能会采用更精确的时间计算方法,以提高数据传输的效率和可靠性。
总结
TCP 连接的建立与断开过程(三次握手与四次挥手)是网络编程中非常重要的基础知识。深入理解这些过程的原理、细节以及可能出现的问题,对于开发高性能、可靠的网络应用至关重要。不同操作系统在实现 TCP 连接建立与断开时存在一些差异,开发者需要根据具体的应用场景和目标操作系统进行适当的优化和调整。通过合理地运用 TCP 协议的特性和掌握解决常见问题的方法,能够编写出更加健壮的网络程序。