蛮荆

为什么 TCP 建立连接需要三次握手

2018-12-08

前置知识

首先来看看 TCP 的控制位和状态机,这是理解 TCP 三次握手的基础。

TCP 报文控制位

TCP 报文头部中的控制位用于控制 TCP 连接的状态,可以指示各种控制信息,如连接建立、终止、重置等。

常见的控制位有 6 个:

  1. SYN (Synchronize Sequence numbers): 请求建立连接 (三次握手),在连接建立时的初始数据包中设置,表明发送端希望建立连接并同步序列号,TCP 是双向通信,所以建立连接时,双方都要发一个 SYN, 虽然 SYN 报文不能携带数据,但要消耗掉一个序号
  2. ACK (ACKnowledgment field significant): 确认接收到的数据,ACK 标识设置后,接收端会在确认字段中填入下一个期望接收的序列号,ACK 报文如果不携带数据,则不消耗序号
  3. FIN (No more data from sender): 请求终止连接 (四次挥手),在数据发送完毕后的数据包中设置,通知接收 (对) 方: 发送 (己) 方已发送完所有数据,TCP 是双向通信,所以关闭连接时,双方都要发一个 FIN, 虽然 FIN 报文不携带数据,但要消耗掉一个序号
  4. RST (Reset the connection): 重置连接,用于异常或错误的连接终止,当接收端收到 RST 标识时,会立即终止连接,不做任何数据确认,例如 之前的文章中提到的 TIME_WAIT 问题,注意: 生产环境中出现 RST 包往往意味着潜在的问题
  5. PSH (Push Function): 提示接收端将接收到的数据立即交付应用层,表明数据应当被快速处理,而不需要等待更多的后续数据到达
  6. URG (Urgent Pointer field significant): 指示数据具有高优先级,接收端应尽快处理
  7. ECE (ECN-Echo): 指示通信双方在三次握手时,协商两端是否都支持显示拥塞控制

Sequence Number (序号)

TCP 是双向通信,所以单个连接中的双方都可以向对方发送数据,所以需要各自维护自己的 Seq 字段。

图片来源: Wireshark 网络分析就是这么简单

Seq 是动态随机生成的,这样可以避免被伪造的报文重置连接 (RST 攻击)。

TCP 提供有序的传输,所以每个数据报文段都要加上一个 Seq 序号字段:

  • 当接收端收到乱序的包时,可以根据 Seq 重新排序
  • 当接收端收到重复的包时,可以根据 Seq 去重

如图所示,序号增长方式 (重要):

  • 数据段 1 起始 Seq 号为1,长度为 1448 (单位: 子节),那么数据段 2 的 Seq 号就等于 1 + 1448 = 1449
  • 数据段 2 的长度也是 1448,所以数据段 3 的 Seq 号为 1449 + 1448 = 2897

也就是说,一个 Seq 号的大小是根据上一个数据段的 Seq 号和长度相加而来的。

$$ Seq = 上一个数据段的 Seq + Len (长度) $$

所以在 TCP 数据传输过程中,任意一方发出的数据段应该是连续的: 后一个包的 Seq 号等于前一个包的 Seq + Len (三次握手和四次挥手除外)。

Len (数据段长度)

需要注意的是: Len 不包括 TCP 头部长度,所以不要认为 Len = 0 的数据包没有意义,TCP 头部本身携带的信息也很多。

ACK (确认号)

接收端告诉发送端自己已经收到了哪些数据段 (Seq 序号)。

  • 发送端 发送了 Seq:1 Len:100 的数据到 接收端, 然后 接收端 回复的 ACK 就是 1 + 100 = 101, 表示自己收到了 101 之前的所有数据
  • 发送端 发送了 Seq:101 Len:50 的数据到 接收端, 然后 接收端 回复的 ACK 就是 101 + 50 = 151, 表示自己收到了 151 之前的所有数据

比如甲发送了“Seq:x Len:y”的数据段给乙,那乙回复的确认号就是x+y,这意味着它收到了x+y之前的所有字节。

结论 (重要): 接收端回复的 ACK 号正好等于发送端的下一个 Seq 号,所以我们可以看到 10377 号包的 ACK 正好等于 10378 号包的 Seq

如果通信中任意一方没有发送任何数据,那么对方返回的 ACK 号也不会发生变化 (也就是三次握手时的初始值)。


TCP 状态机

下面是经典的 TCP 状态机,每个 TCP 连接从最初建立到最后断开,整个生命周期中的所有状态都囊括其中。

下面是来自维基百科的彩色版本:

图片来源: https://en.wikipedia.org/wiki/File:Tcp_state_diagram.png


三次握手示例

下面是一个典型的三次握手示例图。

图片来源: https://coolshell.cn/articles/11564.html

  1. 第一次握手:客户端发起连接请求,设置请求报文控制位 SYN = 1, 同时初始化一个随机序列号 (ISN) Seq = x, 发送请求报文 SYN 后,客户端进入 SYN-SENT 状态
  2. 第二次握手:服务端收到客户端的连接请求后,初始化一个随机序列号 (ISN) Seq = y, 同时设置应答报文控制位 SYN = 1, 确认控制位 ACK = x + 1, 发送应答报文 SYN-ACK 后,服务端进入 SYN-RECEIVED 状态
  3. 第三次握手:客户端收到服务端的应答后,设置应答确认控制位 ACK = x + 1, 发送应答报文 ACK 后,客户端进入 ESTABLISHED 状态,服务端收到客户端的 ACK = y + 1 报文后,进入 ESTABLISHED 状态,TCP 连接建立完成

在整个连接过程中,客户端和服务端的状态变化如下图所示:

图片来源: tcpipguide.com

从图中可以看到,对于 ESTABLISHED (连接已建立) 这个状态,客户端和服务端的感知时间是不一样的:

  • 对于客户端来说,两次握手完成后,连接建立完成
  • 对于服务端来说,三次握手完成后,连接建立完成

Wireshark 抓包

为了更直观的感受 TCP 的三次握手过程,这里使用 Wireshark 进行抓包:

打开 WireShark 开始抓包,然后在终端执行下列命令:


$ curl -I -H "Connection: close" https://dbwu.tech

切换到 Wireshark 操作界面,使用 tcp 进行包过滤,可以看到如下的 TCP 三次握手过程,其中 192.168.3.68 是本级的局域网 IP 地址。

  1. 第一次握手:客户端发起连接请求时,使用的 Seq 字段为 3123802190
  2. 第二次握手:服务端发起应答报文时,使用的 Seq 字段为 1071295171, ACK = x + 1, 也就是 3123802191
  3. 第三次握手:客户端发起应答报文时,使用的 ACK = y + 1, 也就是 1071295172

Tips: Wireshark 默认情况下,显示的是 Seq 的相对值 (从 0 开始),如果想看到客户端和服务端的真实随机 Seq 值,可以在 Wireshark 操作界面的菜单进行如下设置:

Edit > Preferences > Protocols > TCP

取消勾选: Relative Sequence numbers

图片来源: Wireshark 网络分析就是这么简单


证明 (粗糙版本)

讲完了 TCP 三次握手的理论基础之后,接下来可以分析并证明如下命题:

TCP 建立连接时,至少需要三次握手。

这里我们使用一个非常基础的数学证明方法:反证法,既然至少需要三次握手?那么我们的反证命题如下:

TCP 建立连接时,不需要三次握手。

具体来说,我们假设 TCP 握手次数 N 少于三次就可以建立连接,这里又可以将 N 分为三种范围区间:

$$ \begin{align*} \begin{cases} N < 1 \ & \text {} \text{} \\ N == 1 \ & \text {} \text{} \\ N == 2 \ & \text {} \text{} \end{cases} \end{align*} $$

下面对三种范围区间分别进行证明。

1. N < 1

N < 1 时,意味着双方都不发起第一次握手,此时双方甚至都不知道彼此的存在,更别谈建立连接进行通信了,所以 N < 1 不成立,此时进入下一个命题: N == 1

2. N == 1

N == 1 时,意味着双方需要一次握手,也就是 发送端 向 接收方 发起连接建立请求 (SYN),但是请求发出后就没有下文了,所以连接还是无法建立。

因为没有收到接收端的应答报文,所以 发送端 此时无法确认两件事情:

发送 接收
发送端
接收端
  1. 发送端 (自己) 的发送功能是否正常 (否可以将数据正常发送到接收端)
  2. 接收端的接收功能是否正常 (包括是否监听了对应的端口、数据缓冲区是否正常等)

因为无法确认这两件事情,所以发送端认为连接还未建立,自然也就不会向接收方继续发送数据了。所以 N == 1 不成立,此时进入下一个命题: N == 2

3. N == 2

N == 2 时,意味着双方需要两次握手,我们在 N == 1 的基础上继续证明。

在第一次握手后,接收端 收到 发送端 的连接建立请求报文后,会回复一个连接建立应答报文 (SYN-ACK),但是和 发送端 的报文一样,接收端 的应答发出后就没有下文了,所以连接还是无法建立。

1. 发送端 (自己) 的发送功能是否正常 (否可以将数据正常发送到接收端)

2. 接收端的接收功能是否正常 (包括是否监听了对应的端口、数据缓冲区是否正常等)

对于 N == 1 中存在的两个问题,此时已经得到了确认:

发送 接收
发送端
接收端
  1. 发送端的发送功能是正常的
  2. 接收端的接收功能是正常的

因为没有收到 发送端 的 (确认) 应答报文,所以 接收端 此时无法确认两件事情:

  1. 接收端 (自己) 的发送功能是否正常 (是否可以将数据正常发送到发送端)
  2. 发送端的接收功能是否正常

原命题证明

通过前文中的反证法,可以证明 TCP 建立连接时,至少需要三次握手。

我们沿着前文中的思路,来看看 N == 3 时,发送端 和 接收端 对应的状态变化。

在第二次握手后,发送端 收到 接收端 的连接建立应答报文后,会回复一个连接建立应答报文 (ACK)。

1. 接收端 (自己) 的发送功能是否正常 (是否可以将数据正常发送到发送端)

2. 发送端的接收功能是否正常

对于 N == 2 中存在的两个问题,此时已经得到了确认:

发送 接收
发送端
接收端
  1. 接收端的发送功能是正常的
  2. 发送端端接收功能是正常的

证明 (正确版本)

前文中通过 (粗糙地) 反证法 + TCP 报文状态 证明了 TCP 建立连接时,至少需要三次握手,除此之外,也可以利用 TCP 序列号 (严谨地) 证明。

为了实现可靠数据传输,TCP 协议的通信双方都必须维护一个序列号 Seq,用来标识发送出去的数据包中哪些是已经被对方收到的。

三次握手的过程等于: 通信双方相互发送初始序列号,并确认对方已经收到了初始序列号的必要过程。

  • 如果只有一次握手,接收端 没有发送初始序列号
  • 如果只有两次握手,只有 发送端 的初始序列号可以被确认,接收端 的初始序列号无法得到确认

除此之外,在只有两次握手的情况下,可能还会出现一种异常情况: 延迟包导致的无效连接。

如图所示,某个网络有多条路径,客户端建立连接请求的第一个数据包,正好被传输到一条延迟严重的路径,所以迟迟没有到达服务器。

客户端发送超时后,认为第一个数据包丢失了,于是重新发起请求,第二个请求被传输到正常的路径,所以很快就完成了连接。

对于客户端来说,本次通信过程似乎已经结束了,但是此时它的第一个数据包,延迟到达了服务器。因为服务器并不知道这是一个旧的无效请求,所以按照正常情况回复应答。

如果 TCP 只有两次握手,服务器上此时就建立了一个无效的 (过期的重复) 连接

但是在 TCP 三次握手的机制下,客户端收到服务器的回复后,发现这个 (已经超时的) 连接不是它想要的,所以就应答一个 RST 数据包,服务器收到 RST 数据后,同时关闭连接。

重点: 三次握手,到底握的是什么?发送方和接收方的初始化 Seq 值 (ISN, Initial Sequence Number), 通过这个值就可以区分当前的本次连接和历史旧连接。


结论

理论上讲 3 次以上,不论握手多少次,都无法确认一个 TCP 连接是 “完全可靠” 的。

但通过 3 次握手,至少可以确认连接是 “基本可用” 的,再增加握手次数,也只不过是提高 “连接可用” 这个结论的可信程度而已。

TCP 3 次握手后,发送端 和 接受端状态变化如下:

  • 发送端确认了: 自己发送、接收正常,对方发送、接收正常
  • 接收端确认了: 自己发送、接收正常,对方发送、接收正常
发送 接收
发送端
接收端

综上所述,TCP 三次握手也是软件工程中的一个经典的 “Trade-Off” 案例。


扩展阅读


FQA

第一次握手时可以携带应用数据吗?

不行,因为此时连接还没有建立。

是否可以由接收方将应用数据缓存起来,等到三次握手完成,连接建立之后,再由接收方交给上层应用呢?

也不行,这样会放大 TCP SYN Flood 攻击,如果攻击者伪造了大量的携带数据报文,那么接收方就需要大量的内存来临时存储应用数据,最终导致内存耗尽。

第二次握手时可以携带应用数据吗?

第二次握手是接收方向发送方发送数据,虽然可以携带数据,但是没有任何实质意义。

第三次握手时可以携带应用数据吗?

可以。

发送第三次握手之前,发送方此时已经进入 ESTABLISHED 状态,所以只要第三次握手的报文到达接收方,那么接收方的状态也会进入 ESTABLISHED 状态,连接就算建立完成了。

此时接收方将发送方在第三次握手时携带的应用数据,转交给上层应用即可。

那这样就不会引发 TCP SYN Flood 攻击吗?

作为攻击者来说,也是需要考虑攻击成本的,如果在第三次握手携带应用数据,就会建立起正常的 TCP 连接,攻击者同样需要资源来存储建立的连接,对于攻击者来说,这是本末倒置的。如果攻击者是远程操纵 “肉鸡” 进行攻击的话,直接在连接建立完成后,让 “肉鸡” 发送海量应用请求就可以了,没有必要在第三次握手时携带应用数据。

TCP 连接失败抓包

TCP 握手失败一般分两种类型,要么被拒绝,要么丢包。因此在 Wireshark 中可以用两个过滤表达式定位出大多数失败的握手。

1. 表达式 1

(tcp.flags.reset == 1) && (tcp.Seq == 1)

从表面上看,它只是过滤出 Seq 号为1,且含有 Reset 标志的包,似乎与握手无关。

但在 (默认) 启用 Relative Sequence Numbers 的情况下,这往往表示握手请求被对方拒绝了,结果如下图所示。

图片来源: Wireshark 网络分析的艺术

接下来只需右键选中过滤出的包,再点击 Follow TCP Stream 就可以把失败的全过程显示出来,如下图所示。

图片来源: Wireshark 网络分析的艺术

此次握手失败的原因是服务器没有在监听 80 端口,所以拒绝了客户端的握手请求。

2. 表达式 2

(tcp.flags.syn == 1) && (tcp.analysis.retransmission)

这个表达式可以过滤出重传的握手请求。

一个握手请求之所以要重传,往往是因为对方没收到,或者对方回复的确认包丢失了。这个重传特征正好用来过滤,结果如下图所示。

图片来源: Wireshark 网络分析的艺术

接下来只需右键选中过滤出的包,再点击 Follow TCP Stream 就可以把失败的全过程显示出来,如下图所示。

图片来源: Wireshark 网络分析的艺术

此次握手失败的原因是丢包,所以服务器收不到握手请求。

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。