TCP 协议提供了可靠的传输,但是网络是不可靠的,TCP 也无法保证数据一定会被对方接收,这是不可能做到的。TCP 实际提供的是数据的可靠递送或故障的可靠通知。
建立一个 TCP 连接需要进行三次握手,而终止一个 TCP 连接需要进行四次挥手。这篇博客就介绍一下关于 TCP 的三次握手与四次挥手与其他一些信息吧。
三次握手
建立一个 TCP 连接时会发生下列情形。
- 服务器必须准备好接收外来的连接。称之为被动打开(passive open)。在 Linux 上,一般是通过调用
socket
、bind
和listen
这三个函数来完成的。可以参照 Tinyhttpd 的实现。 - 客户端通过调用
connect
函数发起主动打开(active open)。客户端将向服务器发送一个 SYN(同步)分节,它告诉服务器客户端将在(待建立的)连接中发送的数据的初始序列号。SYN 分节通常不携带数据,其 IP 数据报只含有一个 IP 首部、一个 TCP 首部以及可能有的 TCP 选项。 - 服务器必须确认(ACK)客户端的 SYN,同时自己也要发送一个 SYN 分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送 SYN 和对客户 SYN 的 ACK(确认)。
- 客户端必须确认(ACK)服务器的的 SYN。
因为这个过程中最少需要三个分组,因此称之为 TCP 的三路握手(three-way handshake)。虽然很多地方将其翻译为了三次握手,但其实际只有一次握手(不过分为了三步),如下图。
图中客户端的初始序列号为 J,服务器的初始序列号为 K。ACK 中的确认号是发送这个 ACK 的一端所期待的下一个序列号(即接收到的 SYN + 1)。
建立连接涉及到的函数
socket
:创建一个套接字,用于建立 TCP 连接(客户端)或者监听端口(服务器)bind
:服务器使用,绑定套接字与端口listen
:服务器使用,开始监听端口connect
:客户端使用,尝试建立一个到指定服务器的连接accept
:服务器使用,接收连接并响应,可以获得一个三次握手完成后的连接
TCP 选项
每一个 SYN 可以含有多个 TCP 选项。下面是常用的TCP 选项。
- MSS 选项。发送 SYN 的 TCP 一端使用本选项通告对端它的最大分节大小(maximum segment size),即 MSS。也就是它在本连接的每个 TCP 分节中愿意接受的最大数据量。发送端 TCP 使用接收端的 MSS 作为所发送分节的最大大小。
- 窗口规模选项。TCP 首部表示窗口大小的字段占 16 位,因此最大只能表示 65535。为了适应当今普及的高速网络连接,这个字段指定了窗口大小需要左移的位数(0~14),可以提供最大接近 1GB(65535 * 2^14)的窗口大小。
- 时间戳选项。用以防止重复的失而复得的分组对数据造成破坏。
为什么是三次握手而不是两次或者四次
关于这个问题,我在网上也看了不少回答。知乎上有一个问题就是对这个的讨论,里面有很多答主回答的都很不错。
我自己对这个问题的理解是这个样子的:三次握手中的第二次,在一个分节中同时递送了对客户端的确认(ACK)和自己的 SYN,而如果将这个分节拆开来看的话,就是客户端和服务器都分别发送了一个 SYN 分节和对其的 ACK。
网络环境是不能确定可靠的,三次握手后,客户端仍然不知道服务器有没有接收到自己最后一个 ACK。但这两个 SYN 和 ACK 可以测试双方的发送与接收能力,即使有再多的握手次数也不过是重复罢了。而这四个分节少了一个就无法测试到所有情况了。因此三次握手(两个 SYN 与两个 ACK)是最为合理的方式。
四次挥手
在 Linux 上,终止一个 TCP 连接有四步。
- 某个应用程序调用
close
,我们称该端执行主动关闭(active close)。该端的 TCP 将会发送一个 FIN 分节,表示数据发送结束。 - 接收到 FIN 分节的对端执行被动关闭(passive close)。这个 FIN 由 TCP 确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为 FIN 的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
- 一段时间后,接收到这个文件描述符的应用进程将调用
close
关闭它的套接字。这导致它的 TCP 也发送一个 FIN。 - 接收这个最终 FIN 的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN。
这个过程通常需要四个分节,在某些情况下步骤一的 FIN 会随着数据一起发送;另外,步骤二与步骤三的分节有可能被合并为一个分节。
为什么是四次挥手
挥手时双方需要明确的通知到对方连接已经结束了,因此双方各自发送一个结束的信号与对结束信号的确认。
TIME_WAIT 状态
在关闭一个 TCP 连接的过程中,执行主动关闭的一端需要维持一个长达 2MSL 的 TIME_WAIT 状态。
MSL 全称为 maximum segment lifetime,即最长分节生命期。RFC 1122 [Braden 1989] 的建议值为 2 分钟,而源自 Berkeley 的实现传统上使用 30 秒这个值。虽然 IP 数据报在网络中存活的限制是跳限(hop limit)而非时间,但是一般认为,一个 IP 数据报在网络中不能存活超过 MSL。基于这个前提,设立了时长为 2MSL 的 TIME_WAIT 状态。
TIME_WAIT 状态有两个存在的理由:
- 可靠地实现 TCP 全双工连接的终止;
- 允许老的重复分节在网络中消逝。
假设在终止 TCP 连接时,最后一个 ACK 分节丢失了,那么被动关闭的一方(在图中为服务器)将重发第三步的 FIN N,如果客户端已经清理了 TCP 连接的信息,那么将无法重发最后的 ACK。因此主动关闭的一方需要保持 TIME_WAIT 状态。
对于第二个理由,假设两台电脑在进行 TCP 连接的时候发生了路由异常,某个分组没有被及时送达。两台电脑通过重传恢复了连接,并正常终止了这个 TCP 连接。紧接着,在同样的 IP 和同样的端口,这两台电脑又开始了一次新的 TCP 连接,此时之前没有及时送达的分组姗姗来迟,就有可能被误认为是新的连接的分节。为了解决这个问题,同样需要保持 TIME_WAIT 状态。这也解答了 TIME_WAIT 状态为什么是 2MSL:因为在一个 IP 最长生存时间不会超过 MSL 的前提下, 2MSL 时间可以保证每个方向的分组都已经在网络中消逝了。
参考
- UNPv1
- TCP 为什么是三次握手,而不是两次或四次? - 知乎问题
工具
- draw.io,在线作图工具