计算机网络 - TCP 协议原理总结

本文总结 TCP 协议的原理,包括:

本文笔记目的,内容较多。 TCP 协议非常复杂, 读完本文需要许多耐心

网络是不可靠的

计算机网络是不可靠的,存在 丢包乱序延时

这是众多 TCP 协议机制的设计出发点,万恶之源,将贯穿全文。

TCP 概念和特点

TCP 协议全称 传输控制协议, 是一种 面向连接的可靠的面向字节流的 传输层通信协议。

TCP 在 TCP/IP 协议模型 中位于传输层。

图 1.1 - TCP 位于 TCP/IP 分层中的传输层

TCP/IP 模型中各层数据包结构的关系如下图。

图 1.2 - TCP/IP 模型中各层数据包结构的关系

TCP 协议的主要特点:

  1. 面向连接,一对一通信。
  2. 可靠交付:保序、不重复、不丢失。
  3. 全双工通信:双方均可收发。
  4. 和上层应用进程的交互方式是 面向字节流的

TCP 和上层应用进程的交互方式:

  1. 发送方可以发送不同大小的数据块到发送缓存。
  2. 先会对发送缓存内的数据分段,打包成 TCP 数据包再发送。
  3. 在接收一侧,数据包会先进入接收缓存区。
  4. 当一定数量的数据包全部到达,重排、组装,以数据流方式吐给接收方。
图 1.3 - TCP 和应用程序的交互方式

虽然 TCP 是面向字节流的,但是 TCP 所处理的数据单元却是面向报文段的, 也就是本文所说的数据包。

将看到,短短 可靠交付 四字背后并不简单。

可靠传输的基础机制

若不止考虑 TCP 协议本身的实现,如何设计可靠的网络通信?

首先,由于 丢包的可能性,要实现可靠通信:

  1. 发送方要知道对方接收成功,因此需要接收方回复确认 即 ACK。
  2. 如果丢包发生,发送方需要重传。

一种触发重传的方式是,超时重传 (也有其他触发方法,见后续 TCP 重传机制 )。

此外,无论往返中哪个包丢失或延迟, 发送方认为对方没有收到,就会重传

图 2.1 - 确认和超时重传机制的几种情况

网络延时发生时,重传可能会导致重复:

  1. 接收方会丢弃收到的重复数据包,但是仍然回复确认。
  2. 发送方会丢弃收到的重复确认包。

其次,对于如何发送确认包和重传包,有两种方式。

一问一答的方式

也叫做 停止并等待 ARQ 协议, 是指 发送方等到接收方的确认包后,再发下一个数据包

类似乒乓方式,具体来说:

  1. 如果时限内收到对方确认,才发送下一个数据包。
  2. 否则,重传当前数据包。
图 2.2 - 一问一答的方式

可以看到这种方式下,发送方大部分时间在等待,效率非常低。

流水线传输的方式

叫做 连续 ARQ 协议 , 是指 发送方会连续发送一组数据包,同时等待这些数据包的确认

具体来说:

  1. 发送方发送一批数据包。
  2. 同步地接收对方的确认包。

简单来说, 发送方不闲着,一边发送,一边等回复

图 2.3 - 流水线传输的方式

可以看到这种方式相对一问一答的方式,效率要高。

如果发生丢包或延时,需要重传,有两种方式

  1. 回退 N 重传

    发送方每发送一个数据包,都会发起一个定时器。

    一旦一个某个定时器触发,就会重传。

    发送指针回退到未拿到确认的数据包处,以实现重传。

    图 2.4 - 回退 N 重传的方式

    可以看到,此方法下, 会重传后面所有的数据包

  2. 选择重传

    同样,每发送一个数据包,都会发起一个定时器。

    不同点仅在于, 只重传未拿到确认的数据包,不回退发送指针。

    图 2.5 - 选择重传的方式

综合以上,得出的结论是,不考虑 TCP 协议的具体实现的话, 要实现可靠的网络通信,需要依赖确认和重传机制, 并且一个好的办法是采用流水线传输的方式。

而流水线传输方式,正是下一个部分 TCP 协议中的 滑动窗口机制 的引子。

TCP 滑动窗口机制

滑动窗口机制是 TCP 协议的精髓所在,它是 TCP 协议设计的基本框架

滑动窗口机制就是 流水线传输方式 在 TCP 协议中的细化设计, 发送方一边连续地发送数据包,一边等待接收方的确认。

滑动窗口分为两种:发送窗口接收窗口

由于 TCP 是全双工的, 所以通信的每一端都会同时维护两种窗口

数据包序号

可靠传输的基础机制 中, 接收的数据包可能是重复的、乱序的,因此 TCP 会对每一个数据包进行唯一标号, 叫做数据包的序号。

数据包的序号是 TCP 协议头 中的一个 32 比特大小的整数字段。

每次发送一个包,这个序号就会增加一。

TCP 是全双工的,两个通信端各自维护自己的序号。

因为 网络延时不可控, 如果两次连接建立时差很短、或者连接重建后老连接的数据包延迟到达, 会造成序号冲突。

所以,序号并非由固定数字初始化。可以综合时间、随机数来生成等。

确认号和累计确认

在 TCP 中,一个用以确认的回复包,会有确认号。

如果一个数据包同时也是一个确认包,那么它也会有确认号。

一个序号为 SEQ 的数据包,其确认包的确认号会是 SEQ+1

同样,确认号也是 TCP 协议头 中的一个 32 比特大小的整数字段。

可以理解为,接收方已收到序号为 SEQ 的数据包,期待发送方下一次给 SEQ+1 的包。

更广义的理解是, 确认号是接收方期望对方发送的下一个包的序号

下图中,发送方连续发送一组包,如果中间有丢包, 接收方则期待序号最小的丢失的包。当重传成功后, 接收方仍然期待下一个未拿到的数据包:

图 3.1 - 接收方的确认号

接收方所期待的是序号最小的没拿到的数据包

这种确认号的机制,即可实现累计确认机制:

接收方确认了标号为 SEQ 的数据包, 即代表确认了所有小于 SEQ 的数据包, 此时接收方给的确认号是 SEQ+1

图 3.2 - 累计确认机制

累计确认其实是一种批量确认的机制,以减少确认包的数量。

此外,如果接收方恰好需要发送数据,确认号可以直接标在数据包上,即捎带确认。

一个问题是,如何控制累计确认的时机?

TCP 协议中的 Nagle 算法 给出的办法是 延迟确认

其大概的原理是,未确认的包达到一定量、或者达到一个时间阈值,才回复一次确认。

所以说, 在默认的 TCP 协议中,确认不是立即回复的,而是延迟的

不过,接收方的延迟确认不应该过分延迟,否则会造成发送方的重传,浪费网络资源。

可以设置 TCP_NODELAY 选项 来禁用 Nagle 算法。

TCP 的累计确认机制,是累计确认和延迟确认两个策略的综合。

此外,TCP 协议还有另外一种确认机制,叫做 选择确认机制 ,将会后面讲到。

发送机制

已经讲过,TCP 协议默认的 Nagle 算法 采用了延迟确认的方法。

对应的,发送的时机如何确定?

办法是类似的,大概是,未发送的包达到一定量、或者达到一个时间阈值,才发送一次。

图 3.2.1 - Nagle 算法的发送机制

TCP 的发送机制,是累计发送和延迟发送两个策略的综合。

同样可以设置 TCP_NODELAY 选项 来关闭延迟发送的行为。

发送窗口

发送窗口的示意图如下,当收到对方的累计确认后,则向右滑动。

图 3.3 - 发送窗口

注意的是 窗口大小 是有限的(稍后将讨论它的受限情况), 发送方只能发送窗口内的数据包

接收窗口

接收窗口的示意图如下,当回复对方确认后,则向右滑动。

图 3.4 - 发送窗口

仍需注意 窗口大小 是有限的(稍后将讨论它的受限情况), 接收方只能接收窗口内的数据包

由于 网络数据包是乱序的 , 所以接收后的数据包会按照序号重新排序,才可以交付给应用程序。

窗口大小

窗口的大小是受限的,也是动态的

窗口大小是 TCP 协议头 中的一个 16 比特大小的整数字段。

首先,显然 接收窗口受限于接收缓存区的大小, 发送窗口受限于发送缓存区的大小

其次指出, 发送窗口大小也受限于接收窗口大小

这其实比较容易理解,发送方是生产者,接收方是消费者, 如果生产速度快而消费速度慢,则会导致接收方来不及接收。

图 3.5 - 生产者和消费者

所以,发送方只能发送窗口内的数据包

其实,这就是 流量控制机制由接收方控制的、调节发送方生产速度的机制 , 其具体的实现方式,就是在回复时设置 TCP 协议头 中的窗口大小字段。

窗口的大小也是动态变化的, 因为两端接收和发送能力是动态变化的

  1. 接收能力的变化导致窗口大小的变化,即后面所讲的 TCP 流量控制机制
  2. 发送能力的变化导致窗口大小的变化,即后面所讲的 TCP 拥塞控制机制

可以看到,滑动窗口的大小是一个贯穿式的重要概念。

但是,如果窗口要缩小,窗口的前沿是不可以向前收缩的。

窗口收缩的方法则是,慢慢地,随着已发送的数据包得到确认, 保持窗口前沿不动,前移窗口后沿。

图 3.6 - 窗口前沿不可以收缩

因此, 发送窗口也并不总和接收窗口一样大

窗口大小的初始化,是在 连接建立 过程中两端协商确定的, 在后续的传输阶段,它会因主动或被动的原因而动态变化。

选择确认机制

和前面所讲的 累计确认机制 一样, 都是 TCP 协议中用来控制如何回复确认包的机制。

因为网络丢包、延时、乱序的不确定性,序号大的包可能先到达。

如果仅采用累计确认机制,发送方并不知道大号的包传输成功了, 它只会执行 回退 N 重传 , 将后面的所有包重传。

下图中,如果接收方的回复包丢失或延迟、 或发送方的速率较快,就会导致短时间内多次无效重传。 接收方则不得不丢弃重复数据。 造成信道资源浪费。

图 3.7 - 累计确认机制的无必要重传

选择确认机制,简记作 SACK,是指 接收方在回复确认包 ACK 的同时, 告诉对方已收到数据包的序号区间

比如,在回复对方 ACK=10 的时候,同时回复 SACK=20,39 , 这表示接收方已经收到 20~39 序号的数据包。 发送方只需要重传 10~19 就可以了。

下图中同样的场景,可以看到开启选择确认机制, 可以一定程度上优化无意义的重传。

图 3.8 - 选择确认机制

如果接收方采用了选择确认机制, 那么发送方就可以采用前面所讲的 选择重传

所以,选择确认机制是搭配选择重传机制一块使用的。

这个机制是可选的,需要两端都支持,在 TCP 连接建立 阶段, 两端协商确定是否采用此机制。

TCP 重传机制

重传机制是 TCP 协议中比较复杂的部分。

其触发方式有两种:超时重传快速重传

其具体实施方式又有两种,前面所讲的 回退 N 重传选择重传

超时重传和超时计算

前面 可靠传输的基础机制 有讲到超时重传, TCP 协议每发送一个数据包,就对这个数据包设置一次计时器 ,超时即触发重传。

超时重传中的一个问题是:超时的时间是多少呢?

这是 TCP 协议中又一个复杂的问题。

  1. 如果超时重传时间设置太短,会引起不必要的重传。
  2. 如果设置太长,会使得网络空闲时间增大,降低传输效率。

TCP 采用一种自适应算法,具体地,是一种加权平均的方法。

首先,记录每个数据包发出去后到收到确认包的时间差,叫做往返时间 RTT 。

图 4.1 - 往返时间的方差可能会很大

具体的,假设加权平均后的数据叫做 $T$ ,其计算方式:

\[{T}_{n} = (1 - \alpha) \times {T}_{n-1} + \alpha \times {RTT}_{n}\]

其中 $ 0 \leq \alpha < 0$ 。

这个计算式的意思是,一部分权重考虑实时数据,另一部分权重考虑历史沉淀。

可以知道,$\alpha$ 越大,新数据的贡献越大,$T$ 跟随 $RTT$ 的就越快。 反之,$\alpha$ 越小,新数据的贡献越小,$T$ 跟随 $RTT$ 的就越慢。

按这个计算式迭代下去,历史数据的权重会越来越小,$T$ 会跟随实时数据的趋势。

同时,由于综合了历史数据,又可以消除毛刺。

快速重传机制

TCP 的另一种重传机制,是快速重传机制,它不再以超时作为触发标准, 而是观察确认包的情况。

具体来说, 如果收到同一个数据包的多次确认,立即发起重传

一般取 3 次作为阈值,常叫做 3ACK 方法。

快速重传机制的作用在于,将有可能在超时触发之前,提前发起重传。

图 4.2 - 收到同一个数据包的 3 次确认即发起重传

细节地, 因为每发送一个数据包,就会对这个包设置一次定时器, 所以,快速重传触发后不会造成超时重传的重新触发。

TCP 流量控制机制

前面 窗口大小 部分已经提及, 如果数据发送过快,接收方就会来不及接收。

流量控制,就是接收方调控对方的发送速度不要太快的机制

发送窗口的大小受限于接收窗口 , 接收方在回复时通过设置 TCP 协议头 中的窗口大小字段, 来限制发送方的发送窗口大小。

需要注意的是, 流量控制过程是动态的

其原因在于,两端的发送和接收数据的能力是动态变化的

以接收方为例, 哪怕我们假设缓存区的大小未发生变化,上层应用程序读取数据数据的能力也是动态变化的。

比如,接收方应用程序因为负载升高,无法及时读取缓存区数据, 导致已回复数据包大量积压,从而挤压接收窗口的大小。

图 5.1 - 接收窗口缩小的示例

零窗口死锁问题

一个特殊的场景是,如果接收方的窗口变为 0 ,发送方的窗口也会被限制到 0

此时,发送方窗口是 0 ,无法发送新数据包。而接收方在等待数据包到来, 也不会回复任何确认包,即使接收方的窗口已经可以变大,也没办法告知对方。

这样,两端都在等待,造成零窗口死锁问题。

图 5.2 - 零窗口死锁问题

在 TCP 协议中,打破死锁的方法,是采用 持续计时器机制。

具体来说,发送方收到零窗口通知时,启动计时器, 当计时器超时,就发送一个 仅携带一个字节数据的探测数据包

如此,接收方就可以回复确认包,如果此时接收方的窗口可以恢复, 双方就回到正常轨道。

图 5.3 - 持续计时器解决零窗口死锁问题

一个细节是,在发送探测数据包时,持续定时器会重新开始计时。 也就是说, 发送方会周期性地进行窗口探测,这样可以应对探测数据包丢失的情况

如果接收方拿到探测数据包后, 仍坚持零窗口, 那么发送方的计时器就重新计时。 如此往复一定次数后仍无法恢复,则关闭连接。

糊涂窗口综合征

糊涂窗口综合征 的通俗理解是, 双方处理速度不一致,会导致通信演化为小包通信的问题

如果接收方的处理速度跟不上发送方的生产速度, 最终接收方的数据积压在缓存区来不及拿走,接收窗口宣告为 0 , 此时发送方停止发送。

一旦接收方的应用进程处理了一个字节,接收窗口变为 1 , 发送方就可以发送一个字节。

如果发送方总是足够快地填充接收方的窗口, 这个过程会不断循环下去,导致每次通信的数据包都只有一个字节, 显然降低了通信效率。

图 5.4 - 糊涂窗口综合征

此问题的解决措施有多种,其中包括 Nagle 算法的 累计与延迟确认累计与延迟发送

  1. 接收方 过一定时间 或 累计一定数量的包 后再确认。
  2. 发送方 过一定时间 或 累计一定数量的包 后再发送。

此外,还有一种关键的措施,就是 延迟宣告窗口

大概意思是,接收方的窗口稍稍变大时,不要急于告知对方, 而是达到一定阈值才告知对方,如此发送方就会等接收方窗口足够大时将数据包一并发送, 从而解决小包传输问题。

或者是,接收方的窗口变的足够小的时候,直接宣告窗口关闭,阻止发送方再发数据。 直到接收窗口可以变的足够大,再恢复大包传输。

下图是延迟宣告窗口的一种推演过程。

图 5.5 - 延迟宣告窗口 - 解决糊涂窗口综合征

可以看出, 窗口探测实际是探测一个足够大的窗口,而不是完全非零的窗口

TCP 拥塞控制机制

网络拥塞是指,对网络中某种资源的总需求量大于总可用量的情况。

直接的理解就是, 要传输的数据量超过网络负荷

在网络拥塞的情况下,TCP 协议的重传机制反而会加剧拥塞情况。

虽然网络拥塞是一个全局的、宏观的问题, 但是 TCP 协议对网络拥塞的缓解措施,是从个体角度出发的。

简而言之,TCP 协议的拥塞控制的办法是, 发送方主动减少发送量

拥塞控制和 流量控制 是不同的事情:

  • 拥塞控制是发送方主动减少数据传输,解决的宏观网络的超负荷问题的机制。
  • 流量控制是接收方调控发送方数据传输量,以平衡生产消费速度的机制。
网络拥塞的判断

拥塞判断的方法有许多种,这里主要说明两种。

这两种就是 TCP 协议中重传机制的两种触发方式

  1. 超时标准

    前面 超时重传和超时计算 部分有讲, TCP 协议每发送一个数据包,就会对它设置一次计时器。

    当超时发生时,意味着数据包丢失、或延迟,此时网络可能发生拥塞。

  2. 3ACK 标准

    前面 快速重传机制 部分有讲, 如果收到同一个数据包的多次确认,意味着数据包丢失、或延迟, 此时网络可能发生拥塞。

拥塞窗口算法

TCP 协议仍然基于滑动窗口的机制,进行拥塞控制。

发送方会维护一个拥塞窗口 CWND ,此窗口范围内的数据才允许被发送。

因此实际的发送窗口的大小是:Min(对方接收窗口大小,自身拥塞窗口大小)

拥塞窗口算法就是如何调整拥塞窗口大小的办法。

它主要有四个策略:慢开始、拥塞避免、快重传、快恢复。

虽然有四个策略,但是它们其实是 一套算法

图 6.1 - 拥塞控制算法图示 - 图片来自 计算机网络(谢希仁第七版)-方老师

首先,「快重传」就是前面所讲的 快速重传 , 也就是说, 如果遭遇 3ACK ,就立即发起重传

慢开始

发送方每收到一个对新数据包(就是不包括重传包)的确认, 就让拥塞窗口增一。

虽然叫做慢开始,但是其实并不慢, 拥塞窗口是倍增的

简而言之,如果没有遭遇超时 或 3ACK, 拥塞窗口就扩大一倍。

图 6.2 - 拥塞控制 - 慢开始策略

拥塞避免

慢开始的增长速度是非常快的, 发送量越大,就离拥塞越近。

所以有一个门限值 ssthresh 的概念。

拥塞窗口倍增到门限值后,就改为线性增长,比如每次增大一

其后,线性增大拥塞窗口的过程,就是拥塞避免阶段。

慢开始和拥塞避免,都是在探测拥塞边缘 : 一开始疯狂试探,到达门限值后,慢慢试探。

拥塞控制的惩罚

如果出现 拥塞的征兆,拥塞窗口的大小会受到惩罚,让它减小。

  1. 超时条件

    超时未确认,意味着发生丢包或者较大延迟,所以处罚更为严厉。

    拥塞窗口打回初始值 1 , 门限值改为当前拥塞窗口大小的一半。

    总而言之,从新进入慢启动

  2. 3ACK 条件

    收到 3 个相同的确认包,意味着发生少许延迟,所以处罚相对柔和。

    同样,慢启动的门限值减半。

    不同的是,此时不必要从新进入慢启动, 而是减少一些拥塞窗口的值,跳过慢启动,直接重新进入拥塞避免阶段。

    减少到多少呢?减少到最新门限值大小,也就是从新进入拥塞避免。

    这种直接跳过慢启动,直接进入拥塞避免的方式,就叫做 快恢复

拥塞控制算法总览
图 6.3 - 拥塞控制算法总览 - 图片来自 计算机网络(谢希仁第七版)-方老师

总结如下:

  1. 慢开始阶段,倍增到门限值,激进探测拥塞边缘。
  2. 到达门限值,进入拥塞避免,线性增加,小心探测拥塞边缘。
  3. 遭遇超时,惩罚严厉,从新进入慢开始。
  4. 遭遇 3ACK,惩罚柔和,快恢复,门限减半,直接进入拥塞避免。

    同时,3ACK 时,立即重传,即 快重传。

TCP 连接建立和释放

TCP 的三次握手和四次握手,已经是老生常谈了。

连接建立 - 三次握手

首先,TCP 连接建立过程要解决的问题:

  1. 确定双方都可以正常收发 TCP 数据包
  2. 双方协商一些参数和可选项(例如前面所讲的 选择确认窗口大小 等), 尤其是交换初始序号
  3. 对资源进行分配,比如缓存区、端口号。

下面是 TCP 协议三次握手建立连接的过程示意图,其中

  1. SYN(synchronous) 是同步标志位 ,表示开始传输数据的意思。
  2. ACK(acknowledgement) 是确认标志位,表示是否启用确认机制。
  3. ack(小写)是前面所说的 确认号
  4. seq 是前面所说的 序号
图 7.1 - TCP 协议三次握手过程

具体过程的描述如下:

  1. 客户端发送一个 TCP 数据包,主动发起连接

    设置 SYN=1 标志位,表示发起连接,想要传输数据。

    客户端初始化自己的数据包序号 seq=x ,一同发送给对方。

  2. 服务端收到对方的连接请求,回复对方确认包

    设置回复标志位 ACK=1 ,表示确认。

    根据 确认号的规则, 同时设 ack=x+1 表示期待对方下一次发过来的数据包序号为 x+1

    因为 TCP 是全双工的,服务端也会设置 SYN=1 , 并初始化自己的数据包序号 seq=y ,捎带回复给客户端。

    此时,服务端可以确定: 客户端可以发送 TCP 数据包,自身可以接收 TCP 数据包

  3. 客户端收到对方的确认包,并回复对方确认

    同样,设置回复标志位 ACK=1,表示确认。

    对方的序号是 y , 回复对方 ack=y+1 表示期待对方下一次发来的数据包序号为 y+1

    同时,自身的数据包序号需要自增 seq=x+1

    自己发送上一个数据包,对方收到了,并且收到了对方回复。

    所以客户端可以确定: 服务端可以正常收发 TCP 数据包, 自身也可以正常收发 TCP 数据包

  4. 服务端收到客户端的确认,三次握手结束

    此时服务端可以进一步确定:

    客户端端可以正常接收 TCP 数据包, 自身也可以正常发送 TCP 数据包

三次握手结束后,双方都可以确定对方是可以正常收发 TCP 数据的。

图 7.2 - 三次握手过程中双方逐步确定收发能力正常

三次握手的必要性

一个经常出现的问题是: 两次握手可以吗?

当然是不可以的,可以分几个方面解释:

  1. 三次握手是为了确定双方的收发能力

    如果缺少最后一次 ACK 的话,服务端就无法知道客户端是否可以接收 TCP 数据, 也无法知道自己的发送是否成功,就无法做到可靠传输。

  2. 序号同步的确定性

    如果缺少最后一次 ACK 的话,服务端就无法确定对方有无收到自己的初始序号。

    前面讲到,数据包的序号是 TCP 滑动窗口机制的基本字段,它如果是不可靠的, 后续的确认机制、重传机制等都无从谈起。

    网络是不可靠的,存在丢包,所以连接建立时, 双方都必须确定初始序号交到了对方手中。

    三次握手过程,也是可靠地交换初始序号的过程

    图 7.3 - 三次握手也是可靠地交换初始序号的过程

  3. 历史失效连接请求的乱序问题

    网络中,数据传输可能存在延迟、乱序。

    如果一个老的失效的连接请求,延迟到达服务端,倘若缺少第三次握手, 服务端会建立一条新连接,直接进入 ESTABLISHED 状态。

    客户端收到服务端的回复包时,可以根据序号是否过期判断是否是失效连接。

    在三次握手的情况下,客户端就进一步可以发送 RST 标志,终止连接。

    如果不是历史失效连接,客户端则发送 ACK 标志,正常建立连接。

三次握手中丢包的情况

另一个常见的问题是: 如果第三次 ACK 丢包呢?

无论哪一次通信发生丢包或延迟,都出发 TCP 的 重传机制

简单说,再发一次,如果达到一定次数,则放弃连接。

对于第三次 ACK 丢包的情况,重试一定次数后,服务端会关闭连接,回收资源。

不过,服务端单方面关闭连接,客户端并不知晓,它认为连接已经建立。

如果客户端向服务端发送数据,会被服务端打回 RST 报文, 借此客户端可知道连接失效。

图 7.3.1 - 第三次 ACK 包丢失的最坏情况

数据传输的开始时机

上面的 三次握手图示 中,一个细节是:

  • 客户端在两次握手之后就进入了 ESTABLISHED 状态。
  • 服务端则是在三次握手之后才进入 ESTABLISHED 状态。

其原因在于, 在 第二次握手 后, 客户端已经可以确定双方都可以正常收发数据包

所以,客户端可以提前进入 ESTABLISHED 状态,分配端口,开始通信。

因此, 第三次 ACK 是可以捎带客户端的数据一并发送的

服务端直到 第三次握手 成功之后,才可以确定双方都正常, 所以它会稍晚进入 ESTABLISHED

连接关闭 - 四次挥手

TCP 的连接释放过程,需要注意两个点:

  1. TCP 是全双工的。
  2. 双方都可以主动释放连接。

释放连接的过程有一个重要的目的:

双方都要确定地知道对方不再发送数据了,连接才正式关闭

假设客户端主动关闭,下面是 TCP 协议四次挥手释放连接的过程示意图。

其中 FIN(finish)是结束数据传输的标志位。

图 7.4 - TCP 协议四次挥手过程

具体过程的描述如下:

  1. 客户端发送 FIN 数据包,不再发送数据

    设置 FIN=1 标志位,表示想要关闭连接,不再发送数据。

    但是,此时服务端还可以继续发送数据

  2. 服务端收到 FIN 数据包,回复确认

    服务端对于 FIN 包也会进行确认。

    此时服务端明确知道,客户端不再发送数据了

  3. 服务端传输完剩余数据

    由于 TCP 是全双工的, 连接关闭由客户端主动发起,并不意味着服务端的数据已传输完。

    服务端需要把剩余数据传输完毕。

    此时服务端知道,双方都不再发送数据了。

  4. 服务端发送 FIN 数据包,不再发送数据包

    同样,设置 FIN=1 标志位,表示服务端不再发送数据。

  5. 客户端收到对方 FIN 包后,回复确认

    任何一方结束数据发送,都要确保对方是知道的,就需要对方确认。

    此时客户端知道,双方都不再发送数据了。

  6. 客户端等待 2MSL 时间等待后,四次挥手结束,释放所有资源

再次明确, FIN 标志位的意思是,不再发送数据

挥手四次的必要性

双方都必须对 FIN 包做确认, 这样对方才可以确定地知道自己不再发送数据。

TCP 连接的建立需要三次握手,是因为, 第二次握手过程服务端将设置 SYNACK 合并为一个数据包发送,所以是三次

而对于释放连接的情况, 无法将第二次和第三次合并, 因为中间还要传输数据, 所以是四次。

如果任何一个确认包发生丢包或延迟,主动设置 FIN 的一方会触发重传。

等待 2MSL 时间的原因

首先,MSL 是指报文在网络中最大生存时间。

2MSL 就是两倍的 MSL ,也被叫做 TIME_WAIT 时间, 在 linux 中常被设置为 2*60s

2MSL 的值一定需要大于重传超时阈值

为何要等待这么长时间呢?

  1. 如果最后一次 ACK 丢包呢?

    前面有说过,任何一方的 FIN 包丢失后,如果超时未收到对方确认, 就会触发重传。

    图 7.5 - 最后一次 ACK 丢失,会导致服务端重传 FIN 包

    如果客户端在一定时间内未收到服务端的 FIN 包重传,说明对方已经收到 ACK

    否则,如果收到服务端的 FIN 包重传,自然回复 ACK

    一次 ACK 包的发送时间,再算上重传 FIN 的时间,所以叫做 2MSL

    不过,无论如何,2MSL 时间至少要比重传超时阈值长。

  2. 防止新老连接数据包混乱

    如果立即释放端口资源,此端口可能被一个新连接立即使用。

    如此的话,假设网络中存在老数据包的延迟,那么老数据包会被新连接接收,造成混乱。

    所以, 等待一定时间,使得网络中潜在的、可能延迟传输的数据包悉数殆尽, 才不会影响到后面的连接

综合来看,还是因为网络是不可靠的。

TCP 连接的有限状态机

综合连接建立和连接关闭的过程,整个 TCP 连接的状态机图示如下。

图 8.1 - TCP 有限状态机 - 图片来自 网络

两个常见的问题:

  1. TIME_WAIT 非常多的情况

    已经知道,TIME_WAIT 是主动关闭连接一方,发送完最后一次 ACK 数据包后进入的状态。

    并且 TIME_WAIT 的时间默认是 120s

    TIME_WAIT 非常多,说明 连接建立的并发数比较大,端口来不及回收

    比如,对于高并发的 TCP 短连接,例如 未开启 keep-alive 选项 的 HTTP 短连接。

    补充,HTTP 协议中是服务端主动关闭连接

  2. CLOSE_WAIT 非常多的情况

    已经知道,CLOSE_WAIT 是被动关闭连接的一方,传输最后的剩余数据的状态。

    它是当服务端收到对方 FIN 包时,主动进入的一个状态。

    所以,一般地,是由于应用程序未调用或太忙未来得及调用 close 关闭连接导致。

    比如程序编写遗忘连接关闭调用代码,或者负载过高,未及时执行关闭连接, 导致 CLOSE_WAIT 状态的 socket 积压。

TCP 协议头

TCP 的协议头部总共有 20 个字节。

下面是 TCP 协议头的栏位示意图,其中大部分字段已经在上面讲到。

图 9.1 - TCP 协议头 - 图片来自 网络

概念图谱总览

最后,对本文的知识做一个树状图梳理。

图 10.1 - TCP 总结图谱 - 图片来自 网络

结语

纵观全文,TCP 协议的众多机制源于,万恶之源:网络是不可靠的,丢包、乱序、延时。

(完)

本文原始链接地址: https://writings.sh/post/network-tcp

王超 ·
喜欢这篇文章吗?
微信扫码赞赏
评论 首页 | 归档 | 算法 | 订阅