协议簇:TCP 解析: 建立连接

简介

接前文 协议簇:TCP 解析: 基础, 我们这篇文章来看看 TCP 连接建立的过程,也就是众所周知的”三次握手“的具体流程.

三次握手

最普通的 TCP 握手流程如下图:
协议簇:TCP 解析: 建立连接
下面描述中,***对应于上图中的行号.

  1. 初始状态时,TCP A 处于连接关闭状态, TCP B 处于监听状态. 也就是通常所说的 A 时 TCP 客户段,B 是服务端.
  2. A 发送 SYN 给 B, 并附有 SEQ, 请求建立 TCP 连接。
    A 发送 SYN 后,状态切换为 SYN-SENT, B 接收到 A 发送的 SYN 后状态切换为 SYN-RECEIVED.
  3. B 收到 A 的 SYN 之后,发送它的 SYN(注意,这里 A 和 B 的 SEQ 是相互独立的 ),并附上 ACK 标记用以表明 B 收到了 A 的 SYN 包。
    这里注意: B 发送的 ACK 的值为 101,它代表 B 收到了***为 100和100之前的所有字节数据,并告诉 A 自己期待下一次收到***以101开始的数据.
    A 在接收到 B 的 SYN 之后,状态转化为 ESTABLISHED.
  4. A 收到 B 的 SYN 之后, 需要发送 ACK 给 B 告诉 B 自己收到了它的 SYN + ACK 包.
    这里注意:A 发送的ACK的值为 301, 原因是 B 的 SYN 中的 SEQ 是 300. A 发送的 SEQ 是 101,原因是上一次的请求中***已经增长到了 100. 下一个可用的***就是 101.
    在B接收到 A 的 ACK 之后,它的状态切换为 ESTABLSHED. 至此,三次握手已经完成,一个 TCP 连接已经成功建立。
  5. 在这条 TCP 连接上可以进行数据传输.

在了解了基本的流程之后,我们来使用 wireshark 包应用以下所学:

如下图,忽略其中的黑色记录,一共四条记录,对应于上图中的 2-5.
协议簇:TCP 解析: 建立连接
接下来,我们详细看看每条记录

客户端 SYN

这是建立 TCP 连接三次握手中的第一次
协议簇:TCP 解析: 建立连接
这张图涵盖的信息很多,全部字段的含义在前文中已经描述过,这里我们仅仅关注个别字段.

  1. Flags 字段中 SYN 标记为 1. 表明当前 TCP 包是一个 SYN 包. 首先发送这个数据包的 TCP 段为请求建立 TCP 连接的端点.
  2. Sequence Number 字段的值为 2292773402. Wireshark 为了方便我们查看,引入另外一个字段 relative sequence number. 这个字段的值是基于 initial sequence number 计算所得. 正如前文 TCP 基础中所说,当当前 TCP 包是一个 SYN 包时,Sequence number 就是 Initial Sequence Number, 因此 这里 relative sequence number 的值是 0.
服务端 SYN + ACK

这是建立 TCP 连接三次握手中的第二次
协议簇:TCP 解析: 建立连接

  1. Flags 字段中 SYN 和 ACK 字段均为 1
  2. Sequence Number 字段的值为 4127119125 (前面说过,客户端的 SEQ 和 服务端的 SEQ 是独立的,他们之间没有联系), Relative Sequence Number 为 0.
  3. Acknowledge Number 字段的值为 2292773403. 客户段发送的 SYN 中 的 SEQ 值是 2292773402. *** 2292773403 告诉客户端 2292773403以及之前的所有数据已经收到。 在接收到这个响应之后, 客户端便可以确信服务段收到了自己发送的 SYN 包.
客户端 ACK

这是建立 TCP 连接三次握手中的第三次
协议簇:TCP 解析: 建立连接

  1. Flags 字段 ACK 为 1
  2. Sequence Number 的值为 2292773403. 这个值于服务端发送给我们 SYN+ACK 包中的 ACK 值相同.
  3. Acknowledge Number 的值为 4127119126. 这里需要注意服务第发送的 SYN+ACK 包中的 SEQ 的值为 4127119125.

至此,这个 TCP 连接成功建立。

特殊情况: 双方同时请求建立连接

在一个 TCP 建立成功之前,连接双方是没有任何关于对方的信息的。因此,存在一种巧合,那就是双方同时发起建立TCP连接的请求. 这里我们来看一下这个特殊情况.
协议簇:TCP 解析: 建立连接
图中 "… " 用来表示该数据包正在网络上传输,对方还未收到.

注意,这里图中虽然对数据包进行 1-7 的编号,但是对于双方任何一方,都是独立的. 也就是说,接收数据的先后是不确定的,有可能 B 先接收到 A 的包,也有可能 A 先接受到 B 的包。 这里我们并没有明确的指定哪一方为 服务端,哪一方为客户端,因此这里的结论都是成立的.

  1. 初始状态下,双方均处于 CLOSED 状态. 存在这种可能: 某一时刻双发同时发送自己的 SYN 给对方,请求建立一条 TCP 连接.
  2. 在某一时间点,A 发送 SYN 到 B 请求建立 TCP 连接. 此时,A 的状态切换为 SYN-SENT
  3. B 在未收到 A 发送的 SYN 包时,发送了自己的 SYN 给 A,请求建立一条连接. 此时,B 的状态也切换为 SYN-SENT. 在接收到 B 发送的 SYN 之后,A 的状态切换为 SYN-RECEIVED.
  4. 此时,A 发送的 SYN 到达 B 端。B 收到了 A 发送的 SYN 包,状态切换为 SYN-RECEIVED.
  5. 在第三步之后,A的状态一旦变成SYN-RECEIVED, 他就需要发送对应的 SYN+ACK 给 B,以确认自己接收到了 B 发送的 SYN. 并将自己的状态切换为 ESTABLISHED. 这里注意,它发送完 SYN+ACK 之后,只是单方的进入 ESTABLISHED 状态,对应状态依然为 SYN-RECEIVED.
  6. 在第四步之后, B的状态一旦变成SYN-RECEIVED,它也要发送 SYN+ACK. 并切换状态为 ESTABLISHED。
  7. B 收到 A 发送的 SYN+ACK。 至此,双方均进入 ESTABLISHED 状态,一个连接已然建立成功.

特殊情况: 旧的重复的 SYN

TCP 协议的设计为三次握手的一个很重要原因就是处理旧的重复的SYN包

RFC793 原文: The principle reason for the three-way handshake is to prevent old
duplicate connection initiations from causing confusion

这里我们就来看看它是如何处理的。

看下图的 TCP 包流程:
协议簇:TCP 解析: 建立连接

  1. 初始状态下, A 为客户端,处于 CLOSED 状态。 B 为服务端,处于LISTEN状态
  2. 某一时刻,A 发送 SYN 给 B,请求建立 TCP 连接
  3. B 在收到 A 刚刚发送的 SYN 包之前, 收到了一个旧的重复的来自 A 的 SYN.
  4. 对于 B 段来说,在收到来自 A 的 SYN 包时,它是不知道那是一个旧的重复的 SYN 包的,因此它就想就受到一个普通的 SYN一样响应这个 SYN 包. 发送 SYN+ACK 进行确认.
  5. A 端收到来自 B 段的 SYN+ACK, 发现其中的 ACK 字段的值不正确. 因为自己发送的 SYN 中 SEQ 值为 100,响应SYN+ACK包中的 ACK的值应该为 101,但是它收到的 SYN+ACK包中的ACK的值却是91. 在收到这个非法的SYN+ACK之后,A 段发送 RST,并附上错误的SEQ***. B 端收到A发送过来的 RST 之后,便得知 A 重置了上一个SYN想要建立的TCP连接,B 段重置所有关于为上一个 SYN 所记录的状态,并重新回到 LISTEN 状态.
  6. 对于 TCP 协议来说,数据是不会丢失的,也就是说 B 段迟早会收到 A 发送的 SYN(SEQ=100). B 端收到 A 的 RST 之后,状态切换到了 LISTEN之后, A 发送 SYN(SEQ=100)到达,此时B依然回像通常情况一样. 如果 SYN(SEQ=100)到达 B 段早于 A 发送的 RST,将会是一个更复杂的情况,涉及到双发均发送 RST 的场景. 这里能力有限,略掉.
  7. B 在收到正确的 SYN+ACK之后,发送自己的 SYN 给 A
  8. A 在发送自己的 SYN+ACK给B,这样之后,一个 TCP 连接成功建立.

如果剔除上述流程中 3-5,我们会发现这就是一个普通的 TCP 连接的三次握手流程. 厉害的地方在于,引入三次握手建立连接的机制,TCP 可以优雅的处理掉由于网络不可靠所导致的非法SYN的数据包。 厉害????

Half-Open connections

这里我们将 “Half-Open connection” 称作 “半连接状态的链接”.

什么是半连接状态的链接?
B方链接已经关闭或者由于其他原因而奔溃掉,但是A方不知道B方的情况,此时 A 方持有的就是一个半连接状态的链接.

我们通过一个例子来描述半连接状态的链接
协议簇:TCP 解析: 建立连接

  1. 在某一时刻,A段的链接崩溃掉。假设奔溃的原因是A端物理内存不足. B 端未收到任何通知
  2. 在A端从错误中回复过来之后,(假设)用户尝试发送数据从原先的 TCP 连接上,A 端TCP模块会返回类似的 “connection not open”错误,此时 A 端TCP状态为 CLOSED, B 端不知道 A 端的情况,因此状态依然为 ESTABLISHED.
  3. A 端发现 TCP 错误后,尝试重新建立原先的 TCP 链接,发送SYN到 B 端
  4. B 端收到 A 的 SYN 之后,发现收到的 SEQ(400) 于自己期待的 SEQ(100) 无法匹配, 因此不对这个 SYN 进行确认,而是发送自已认为正确的 SEQ(100) 给 A 端.
  5. A 端收到 B 端发送的 SYN+ACK 之后,发现收到的 SEQ(100) 与自己刚刚发送的 SYN 中的 SEQ(400)不一致,因此得知自己的TCP状态已经与B端不一致了。因此,发送 RST 重置当前的 TCP 链接.
  6. 在上述步骤之后,上一个 TCP 链接依然被重置,双方不再维护上一个TCP链接的任何状态.
  7. 这里 A 端重新发送 SYN 请求建立一个全新的 TCP 链接. 至此,已经走上了正路,在正常的三次握手之后,一个新的 TCP 链接即将建立.
另外一种情况#####

这里还有一个更有趣的情况. 考虑下图的形况
协议簇:TCP 解析: 建立连接

  1. A 端已然崩溃掉. B 端对此一无所知.
  2. 此时, B 端认为链接一切正常,在该TCP链接上往 A 发送数据.
  3. A 端收到这个数据之后,发现这个 TCP 链接并不存在,因此直接发送 RST 通知对方重置链接.

Reset

何时 TCP 会发送 RST?
通常来说,当 TCP 接收到一个明显不是给当前连接的数据时,必须发送 RST。 当不是很明显时,不应该发送 RST.

发送 RST的原则:#####
  1. 当当前 TCP 连接不存在时,对方发送任何数据到此(除过RST),我们会发送 RST.

    在这种情况下,如果从对方收到的数据包是包含ACK,那么 RST 中的 SEQ 会被置为 ACK 中包含的 SEQ,其他情况下 RST 中的 SEQ 会被置为0且 ACK 字段被置为 SEQ + 接收到的数据的长度. 连接状态维持为 CLOSED.

  2. 当当前 TCP 连接处于正在建立的状态(LISTEN,SYN-SENT 或 SYN-RECIEVED, RFC中成为 非同步状态), 接收到的数据却包含了非法的 ACK 值,我们会发送 RST.

  3. 当当前 TCP 处于同步的状态(ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT), 此时收到一个非法的TCP数据包(错误的 SEQ或者错误的 ACK值), 我们应该发送一个ACK,其中包含当前的发送SEQ和一个 ACK值表明我们期待收到的下一个TCP数据包的***. 此时,维持当前 TCP 的状态不变.

    这种情况的一个例子就是我们在半连接状态的连接小节中第一张图的 步骤 4.

END!