TCP 基础

704 阅读9分钟

前言

TCP 首部中比较重要的几个部分:

  • Sequence Number,报文的序号 Seq
  • Acknowledgement Number,或 Ack,表示确认收到 Seq 及之前的报文,期望值为 Seq+1
  • TCP Flags,表示报文的类型,用来控制 TCP 状态机
  • Window Size,表示接收端的可用缓冲区大小,用于流量控制

注意,ACK 是 TCP Flags 值,与 Ack Number 不是一个登西。

TCP 并不是对每个 Seq 都回复 Ack,实际采用的策略是对累计的多个 Seq 只回复一个 Ack。

握手和挥手

IP 协议是不保证数据接收顺序的,这个特性需要由上层的协议实现。TCP 协议提供了 Sequence Number 序列号,为发送的报文编号,这样在接收端就可以根据编号按顺序重新组合起来。

那么 TCP 连接建立时,就需要告诉对方自己的初始序列号 Initial Sequence Number,aka ISN。最直接的方式就是一来一回,一共四次,但是 TCP 握手过程把二三次合并到一起,简化为三次。

挥手也是为了同步序列号,不过这时就不能简化了,因为 TCP 是全双工的,其中一端完成数据的发送不代表另一端也已经完成,两端需要单独关闭连接。当一端关闭,另一端仍然发送数据时,处于半关闭 half-close 状态。

先关闭的一方收到后关闭的一方发送的第三次挥手后,会等待 2 MSL 再断开,这是为了防止第四次挥手没有送达,导致对端重新发送了第三次挥手。

MSL,Maximum Segment Lifetime,指一个片段在网络中最大的存活时间,2 MSL 就是一个发送和一个回复所需的最大时间

出于对安全和断开重连的情况的考虑,不能使用静态的 ISN,必须每次连接时都进行同步。并且 ISN 也不是有规律地增加的,而是有专门的 ISN 增长算法。

重传

这里说的是数据传输过程中的重传,握手和挥手过程也有重传但是机制不同。

超时重传

时间驱动的机制。

发送端会给发出去的报文设置定时器,当发生丢包,接收端没收到 Seq,当然也不会回复 Ack,这时就等待计时器超时触发重传。

由于 Ack 表示收到了 Seq 及之前的所有报文,所以当一个报文丢了,即使这之后的已经收到了,也都不能回复 Ack。既然发送端不知道后面的报文到底有没有收到,那么就面临一个问题,是只重传没收到 Ack 的报文,还是重传这之后的所有报文

  • 只重传一个,如果后面的报文也没有收到,那么就等后面的报文发生超时之后再重传。节省资源,但是效率低
  • 直接重传所有。如果带宽没满,那么效率比较高,但是浪费资源

快速重传/快重传

数据驱动的机制。不需要等到超时,比超时重传先发生,所以叫快重传。

整体思想是,不要发送端等到超时了再重传,当接收端缺少一个报文,但是却收到了后面的,就可以认为缺少的报文需要重传了。

大致逻辑是,当收到比期望的 Seq 更大的报文时,回复上一个已确认的 Ack。这样一来,当发送端收到重复的(一般是四个,包括第一个正常的)Ack 时就知道对应的报文丢了,然后重传丢失的报文。

这种方式仍然存在重传一个还是全部的问题。因为接收端只会发送第一个丢失的报文的重复的 Ack,并且是在收到新的报文时才会发送。所以,如果丢了多个报文,但是只重传了一个,而这时后面的已经接收完时,接收端不会再发送重复的 Ack,发送端也不会重传,两端都不会再发送任何数据了。

超时重传时间 RTO 和 RTT 算法

显然,对于重传机制,超时重传时间 Retransmission TimeOut,aka RTO 的设置是很关键的:

  • 如果太大,等很久才会重传
  • 如果太小,可能重传正常收到的报文,浪费带宽

这时就需要环回时间 Round Trip Time,aka RTT,指一个数据包从发出到回来的时间。

RTO 需要动态设置。对 RTT 采样并根据一定的算法计算 RTO。

滑动窗口和流量控制

滑动窗口 Sliding Window

TCP 和数据链路层都存在滑动窗口机制,TCP 中的数据为 bytes,数据链路层大多数为 packets。方便起见,下面不强调 byte 和报文两个概念的区别。

IP 协议既不能保证数据送达,也不能保证送达顺序。滑动窗口是 TCP 实现数据的可靠和有序传输的重要机制。

发送端有发送窗口,接收端有接收窗口。

注意,TCP 的通信是全双工的,两端都既是发送端又是接收端。

发送窗口

发送窗口在需要发送的数据上移动,当左侧已发送的数据得到确认后,窗口向右移动。

发送窗口及其两侧的结构是这样的:

  • #1 已发送已确认的
  • #2 已发送还没收到确认的
  • #3 准备发送的
  • #4 暂时不允许发送的

#2 和 #3 组成发送窗口。

接收窗口

接收窗口在接收的数据上移动,当左侧已接收的数据传递给上层程序处理后,窗口向右移动。

接收窗口及其两侧的结构是这样的:

  • #1 已接受并确认,并且已被上层应用读取的
  • #2 已接收并确认,但还没有被上层应用读取的,仍然需要占用窗口空间
  • #3 已接收但是不连续的
  • #4 暂时无法接收的

#2 和 #3 组成接收窗口。

流量控制 Flow Control

上面只说有个窗口,现在要说窗口的大小了。

如果发送数据过快,接收方来不及处理,就会造成分组丢失。为了使接收方有能力接收所有发送过来的数据,需要发送方控制发送速度,这就是流量控制。流量控制的根本目的是防止分组丢失。

流量控制通过滑动窗口实现。

接收端计算自己缓冲区剩余的可用空间 AdvertisedWindow 的大小,通过 TCP 首部的 Window Size 告诉发送端,发送端根据这个值调整发送的数据量 EffectiveWindow 的大小,最小可以是 0。

发送端实际发送的数据量除了受流量控制影响,也受发送端的拥塞控制影响,实际值取 AdvertisedWindow 和 cwnd 中较小的一个。

零窗口 Zero Window

发送端收到 Window Size 为 0 的应答后就不再发送数据,要等到收到不为 0 的应答时才会恢复发送。如果接收端发送了不为 0 的应答,但是这个报文丢失了,这时双方就会互相等待,形成死锁。

解决方法是,当发送端收到零窗口的应答时启动一个计时器,定时询问接收端。

拥塞控制 Congestion Control

拥塞是指,到达子网某一部分的分组过多,使得该部分网络来不及处理,导致该部分网络乃至整个网络出现性能下降。

拥塞发生后,唯一的方法是降低流量

拥塞控制的整个过程包括拥塞发生前、发生后,涉及到以下几种算法。

发生前:

  • 慢启动/慢开始
  • 拥塞避免

发生后:

  • 快重传和快恢复

拥塞发生前,总是处于慢开始和拥塞避免两个状态中的一个。

两个概念:

  • cwnd,Congestion Window,拥塞窗口,由发送端主动探查网络状况计算得到
  • MSS,Maximum Segment Size,最大分段大小,设备所能接受的分段的最大数据量,一般由操作系统指定

慢启动/慢开始 Slow Start

  • cwnd 初始化为较小的值,比如 1 MSS。通常不会初始化为 1,太小
  • 每收到一个 Ack,cwnd 加一
  • 每经过一个 rtt,cwnd 翻倍,这是增长的主要来源

整体上是指数增长的。

如果网络状况好,Ack 返回快,rtt 也小,cwnd 的增长就会很快。

这里存在一个门限/阈值 Slow Start Threshold,aka ssthresh。cwnd 到达这个值之前使用慢启动算法,这之后改为拥塞避免算法。

拥塞避免 Congestion Avoidance

  • 每收到一个 Ack, cwnd = (cwnd + 1) / cwnd
  • 每经过一个 rtt,cwnd 加一,这是增长的主要来源

整体上是线性增长的。

快重传 Fast retransmit 和快恢复 Fast Recovery

这里用到了前面提到的两种重传机制。TCP 拥塞控制默认认为网络丢包是由于网络拥塞导致的,那么发生重传就等于发生拥塞了。

每当发送出一个报文,都会启动一个计时器,用于超时重传。不过如果发生丢包,更先发生的是快重传。

快重传发生时,考虑到还能正常接收到重复的 Ack,或者叫 dup-Ack,所以认为网络也不是非常糟糕。这时:

  1. 阈值 ssthresh = cwnd / 2
  2. cwnd = cwnd / 2 + 3 * MSS
  3. 重传 dup-Ack 对应的丢失报文
  4. 重传之后可能有两种情况:
    4.1. 仍然收到 dup-Ack,没有重传成功,cwnd + 1
    4.2. 收到了新的 Ack,重传成功,设置 cwnd = ssthresh(重传成功了反而减小?),进入拥塞避免算法

超时重传发生时,说明网络问题有点严重了。这时:

  1. ssthresh = cwnd / 2
  2. cwnd = 1
  3. 进入慢启动算法

上面的流程又叫做 TCP Reno 算法,是早期的 TCP Tahoe 的改进版本,后者只有超时重传过程。

参考链接