TCP 协议是 面向连接 的协议,通信前需要先在双端建立 逻辑信道 ,即使是断开连接也不能说断就断,还需双方同意。所以下面我们详细说明 TCP 的连接管理。

在TCP的连接建立过程中一般需要处理下面三个问题

  1. 要使每一方能够确知对方的存在。
  2. 要允许双方协商一些参数(如最大报文段长度,最大窗口大小,服务质量等)。
  3. 能够对传输实体资源(如缓存大小等)进行分配

TCP建立连接最常见的方式就是通过三次握手(three-way handshake) ,连接释放最常见的方式则是 四次挥手(four-way handshake) ,下面我们先介绍这两种最常见的连接管理机制。

三次握手

  1. 客户端会随机初始化序号(client_ISN),将此序号置于 TCP 首部的 序号 字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据 ,之后客户端处于 SYN-SENT 状态。
  2. 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的 序号 字段中,其次把 TCP 首部的 确认应答号 字段填入 client_isn + 1 , 接着把 SYNACK 标志位置为 1 ,同时写入 rwnd ,以告知对方自己的窗口大小。最后把该报文发给客户端,该报文也不包含应用层数据 ,之后服务端处于 SYN-RCVD 状态。
  3. 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次 确认应答号 字段填入 server_isn + 1 ,最后把报文发送给服务端。这次报文可以携带客户到服务器的数据 ,之后客户端处于 ESTABLISHED 状态。
  4. 服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。之后双方便可进行通信。

SYNSYN+ACK 段不携带数据,所以不消耗序列,ACK 段可携带数据,若携带数据,则消耗序列 ,而 TFO 可以在前两次交换数据,参考此处

三次握手之前的连接都称之为 半连接

ISN 为什么要随机?

  1. 为了防止历史报文被下一个相同四元组的连接接收(主要方面);

    四元组唯一标识一个 TCP 连接,当一个 TCP 连接在经历四次挥手关闭时,假如有一个数据包延迟特别大,而这个连接在关闭后又马上以相同的四元组建立起来,那么先前这个连接的 TCP 数据包到达的时候,大概率系列号还落在接收窗内,那么这个数据包就可能会被错误接收。因此 RFC0793 指出 ISN 应该每 4μs 自增 1,从而防止同一个连接的不同实例的数据包混淆。
    ISN = M + F(localhost, localport, remotehost, remoteport)

    • M 是一个计时器,这个计时器每隔 4 微秒加 1。

    • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。若四元组相同,则随机数相同。示意图点这里

  2. 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;

    TCP 系列号欺骗如下图所示:

    示意图

    假设 A 是服务器,B 是拥有特殊权限的客户端,C 是攻击者。

    1. 第一条消息 C 冒充 B 来向服务器 A 请求建立连接,此时 C 发出的数据包的 IP 地址会填写成 B 的;
  3. 第二条消息假设 A 没有其他手段来验证 B,而仅仅根据 IP 地址判断 C 发过来的建立连接的请求是 B 发过来的,因此向 B 发送 SYN+ACK ,此时假设 B 被 C 进行了 DOS 攻击或者处于其他异常状态而不能响应第二条消息(如果 B 处于正常状态会响应一个 RST 包来重启 TCP 连接);

  4. 第三条消息假如 C 能正确的猜测出 A 在第二条消息中的 ISN,就可以冒充 B 和 A 完成三次握手的过程,让 A 误以为和 B 建立了连接。接下来 C 就可以冒充 B 给 A 发送一些危险数据或者指令而实现攻击。

另外提一句,一些朋友做实验用 wireshark 抓包时会发现 SYN 报文的 ISN 为 0,如下:

打开条目后就可以看到,这其实是相对序列号,wireshark 为了方便而帮我们生成的:

为什么是三次握手?不是两次、四次?

  1. 三次握手才可以阻止重复历史连接的初始化(主要原因)

    我们考虑一个场景,客户端先发送了 SYN(seq = 90) 报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100) 报文。

    示意图

    客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:

    • 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
    • 那么此时服务端就会回一个 SYN + ACK 报文给客户端;
    • 客户端收到后可以根据自身的上下文,判断这是一个历史连接,那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。
    • 收到新 SYN 报文后,才重新建立起新连接。

    而两次握手则会直接建立历史连接,服务器可能直接发送数据,造成资源的浪费:

    两次握手无法阻止历史连接
  2. 三次握手才可以同步双方的初始序列号

    本质而言,TCP 握手握的是通信双方数据原点的序列号 ,通过同步双方初始序列号来实现 双向可靠传输

    1
    2
    3
    //  A  --SYN=9        -->  B
    // A <-SYN=4 ACK=10 -- B
    // A -- ACK=5 --> B

    两次握手只能实现 A 方和 B 方就 A 方的同步,也就是说,两次握手只能保证 A 到 B 的数据传输是可靠的,反向则无法保证。这就相当于,A -> B是 TCP,而B -> A是 UDP(类比不是很准确)。如果要保证 B 到 A 的数据传输也可靠,则还需要第三次握手。

  3. 防止资源浪费

    在 SYN 泛洪攻击下,两次握手就建立了连接,而后服务器便向对端发送数据,然而对方压根不会回应,白白造成资源的浪费。

不使用「两次握手」和「四次握手」的原因:

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

四次挥手

四次握手是在半关闭场景下进行的断连接操作,还有另一种断开操作只需要三次挥手。

半关连接-四次挥手

  1. 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  2. 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
  3. 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  4. 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  5. 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  6. 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。
  7. 客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。主动关闭连接的,才有 TIME_WAIT 状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

半关连接 :TCP 的半关连接是指 TCP 连接只有一方发送了 FIN ,另一方没有发出 FIN 包,仍然可以在一个方向上正常发送数据 。这种场景并不常见,一般来说调用 shutdown() 接口时候就会进入半关闭状态,调用常规的 close() 一般是期待完整的双向关闭这个 TCP 连接。shutdown() 接口相当指示程序,本端已经没有数据待发送,所以发送一个 FIN 到对端,但是仍然可以从对端接收数据,直到对端发送一个 FIN 指示关闭连接为止,详见后文。同时关闭参考此处

断开连接为什么需要四次挥手?

因为 TCP 是 全双工协议 ,双方都可以接收和发送数据,所以断开连接时,也需要服务端和客服端都确定对方将不再发送数据。

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送 ,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

四次挥手可以变成三次吗?

可以。实际上,三次挥手比四次挥手更为常见。 以下情形会转为三次挥手:

  • 当接收端收到 FIN 报文时没有需要发送的数据,且开启了延迟确认,则会将 ACK (第二次)和 FIN (第三次)合并发送。

  • 当接收端收到 FIN 报文时只有少于 MMS 的少量数据,且开启了延迟确认,则会将 ACKFIN 报文和数据合并发送。

  • 当发送方调用 close() 时,表明自己不再接收和发送数据,之后接收方如果还发送数据,发送方内核会发送 RST 报文强制释放连接,所以此方式比较粗暴,勉强算得上三次挥手。

    图片

FIN 报文一定得调用关闭连接的函数,才会发送吗?

不一定。如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手。

为什么需要设置 TIME_WAIT ?

  1. 防止历史连接中的数据,被后面相同四元组的连接错误的接收;

    当序列号发生回绕时,就没法直接通过序列号来判断新老报文了。这时,一个历史报文可能刚好落在在同一四元组的新连接下的窗口中,此时窗口就会接收历史报文,从而引起数据错乱。于是,最直接的办法就是 等待网络中的所有包消失 ,所以需要设置 TIME_WAIT

    可以通过 时间戳 的方式判断新老报文。详细内容会在其他文章中讲解。

  2. 保证被动关闭连接的一方,能被正确的关闭;

    假设客户端收到服务器的 FIN 后不等待 TIME_WAIT 而直接关闭连接,那么如果最后客户回复的 ACK 丢失,则服务器会超时重发 FIN,此时由于客户端的客户状态已经丢失(因为连接已关闭),所以它将直接回复 RST,从而被服务器解释为异常终止。

为什么 TIME_WAIT 等待的时间是 2MSL?

因为 2MSL 可以保证网络上所有的历史报文全部消失 ,参见下图:

示意图

假如现在 A 发送 ACK 后,最坏情况下,这个 ACK 在 1MSL 时到达 B;此时 B 在收到这个 ACK 的前一刹那,一直在重传 FIN,这个 FIN 最坏会在 1MSL 时间内消失。因此从 A 发送 ACK 的那一刹那开始,等待 2MSL 可以保证 A 发送的最后一个 ACK,和 B 发送的最后一个 FIN 都在网络中消失。附带也能够保证上条问题第二点。详细参见:知乎文章

MSL 应大于 IP 协议 TTL 换算的时间,RFC793 建议 MSL 设置为 2 分钟,Linux 遵循伯克利习惯设置为 30 s。

握手和挥手的异常处理

参考本系列另一篇文章:三握四挥的异常处理

文章参考:
《计算机网络自顶向下》,《UNP》,小林codingTCP系列五知乎文章