MeiK/TCP 的三次握手与四次挥手

Created Fri, 17 Aug 2018 20:48:23 +0000 Modified Fri, 11 Oct 2019 17:13:01 +0800

TCP 协议提供了可靠的传输,但是网络是不可靠的,TCP 也无法保证数据一定会被对方接收,这是不可能做到的。TCP 实际提供的是数据的可靠递送或故障的可靠通知。

建立一个 TCP 连接需要进行三次握手,而终止一个 TCP 连接需要进行四次挥手。这篇博客就介绍一下关于 TCP 的三次握手与四次挥手与其他一些信息吧。

三次握手

建立一个 TCP 连接时会发生下列情形。

  1. 服务器必须准备好接收外来的连接。称之为被动打开(passive open)。在 Linux 上,一般是通过调用 socketbindlisten 这三个函数来完成的。可以参照 Tinyhttpd 的实现。
  2. 客户端通过调用 connect 函数发起主动打开(active open)。客户端将向服务器发送一个 SYN(同步)分节,它告诉服务器客户端将在(待建立的)连接中发送的数据的初始序列号。SYN 分节通常不携带数据,其 IP 数据报只含有一个 IP 首部、一个 TCP 首部以及可能有的 TCP 选项。
  3. 服务器必须确认(ACK)客户端的 SYN,同时自己也要发送一个 SYN 分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送 SYN 和对客户 SYN 的 ACK(确认)。
  4. 客户端必须确认(ACK)服务器的的 SYN。

因为这个过程中最少需要三个分组,因此称之为 TCP 的三路握手(three-way handshake)。虽然很多地方将其翻译为了三次握手,但其实际只有一次握手(不过分为了三步),如下图。

TCP 的三次握手

图中客户端的初始序列号为 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 连接有四步。

  1. 某个应用程序调用 close,我们称该端执行主动关闭(active close)。该端的 TCP 将会发送一个 FIN 分节,表示数据发送结束。
  2. 接收到 FIN 分节的对端执行被动关闭(passive close)。这个 FIN 由 TCP 确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为 FIN 的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
  3. 一段时间后,接收到这个文件描述符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN。
  4. 接收这个最终 FIN 的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN。

TCP四次挥手

这个过程通常需要四个分节,在某些情况下步骤一的 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 状态有两个存在的理由:

  1. 可靠地实现 TCP 全双工连接的终止;
  2. 允许老的重复分节在网络中消逝。

假设在终止 TCP 连接时,最后一个 ACK 分节丢失了,那么被动关闭的一方(在图中为服务器)将重发第三步的 FIN N,如果客户端已经清理了 TCP 连接的信息,那么将无法重发最后的 ACK。因此主动关闭的一方需要保持 TIME_WAIT 状态。

对于第二个理由,假设两台电脑在进行 TCP 连接的时候发生了路由异常,某个分组没有被及时送达。两台电脑通过重传恢复了连接,并正常终止了这个 TCP 连接。紧接着,在同样的 IP 和同样的端口,这两台电脑又开始了一次新的 TCP 连接,此时之前没有及时送达的分组姗姗来迟,就有可能被误认为是新的连接的分节。为了解决这个问题,同样需要保持 TIME_WAIT 状态。这也解答了 TIME_WAIT 状态为什么是 2MSL:因为在一个 IP 最长生存时间不会超过 MSL 的前提下, 2MSL 时间可以保证每个方向的分组都已经在网络中消逝了。

参考

工具