计算机网络之运输层
可靠数据传输原理
RDT协议的演进:可靠性的逐步构建
RDT协议的构建是一个逐步解决底层信道缺陷的过程,每引入一个机制都是为了解决一个特定的问题。
RDT 1.0 完全可靠信道
- 设计需求: 仅需传输数据。
- 信道假设: 底层信道完美可靠,不丢包,不发生位错误。
- 实现原理: 发送方和接收方状态机简单,直接发送和接收数据,无需任何反馈或定时器。
RDT 2.0 引入位错误(差错检测)
- 设计需求: 识别数据在传输过程中被破坏(位错误)。
- 信道假设: 可能发生位错误,但不丢包。
- 引入机制:
- 校验和 (Checksum): 发送方计算并附加到数据包,接收方检查以判断数据是否正确。
- 反馈控制 (ACK/NAK):
- ACK (肯定确认): 接收方告知发送方数据已正确收到。
- NAK (否定确认): 接收方告知发送方数据有误,请求重传。
- 遗留问题: 如果 ACK 或 NAK 报文本身在回传过程中出错或被破坏,发送方将无法判断是数据丢失还是反馈丢失。
RDT 2.1/2.2 解决反馈报文的错误
- 设计需求: 区分新数据包和重传的数据包(即,解决反馈丢失导致的歧义)。
- 引入机制:
- 序列号 (Sequence Number): 发送方在每个数据包中添加序列号。在 RDT 2.2 中,只需使用 0 和 1 两个序列号即可(因为它是停等协议)。
- 2.2 简化: RDT 2.2 取消了 NAK,转而使用 ACK n 来指明期望收到的下一个报文段的序列号。如果收到一个错误的包,它会发送上一个正确接收包的 ACK,这相当于隐式请求重传。
- 原理: 接收方通过检查序列号,可以丢弃重复到达的包(即,防止数据重复交付给上层应用)。
RDT 3.0 解决数据包和反馈的丢失(定时器)
- 设计需求: 处理最困难的问题——丢包(数据包或 ACK 在信道中消失)。
- 引入机制: 定时器 (Timer)
- 发送方: 发送数据包时启动定时器。
- 超时动作: 如果定时器超时,发送方假定数据包或 ACK 丢失,并重传该数据包。
- 代价: RDT 3.0 仍然是一个停等协议。每发送一个数据包,必须等待一个 RTT(往返时间)才能继续发送。这造成了巨大的信道利用率瓶颈。
流水线机制:效率的飞跃
流水线机制(Pipelining)是解决 RDT 3.0 效率瓶颈的核心思想。
RDT 3.0 效率分析
信道利用率 U(吞吐量)的计算:
U = (L/R) / (RTT + L/R)
其中,L 是数据包长度,R 是信道带宽。当 RTT 远大于 L/R(即网络延迟高,或带宽非常高)时,U 会趋近于 0。
流水线的基本原理
流水线允许发送方在未收到确认之前,连续发送最多 N 个报文段。
- 发送窗口 (N): 限制了在途(未确认)报文段的最大数量。
- 效率提升: 通过保持 N 个报文段在传输中,信道利用率被提高到原来的 N 倍:
Pipelining U 约等于 N * ((L/R) / RTT)
GBN 与 SR 深入解析
流水线机制衍生出了两种处理差错和重传的具体协议:回退 N 步(GBN)和选择重传(SR)。
回退 N 步 (Go-Back-N, GBN)
GBN 是一种以简化接收方为目标,但以牺牲重传效率为代价的协议。
| GBN 角色 | 关键机制与原理 | 细节与逻辑 |
|---|---|---|
| 发送方 | 单一定时器 | 仅维护一个定时器,用于最早发送但未确认的报文段(基序号 base)。一旦定时器超时,整个窗口内的数据都会被重传。 |
| 发送方 | 累积确认 | 接收 ACK $n$ 意味着确认了所有序列号 <= n 的报文段。发送方将窗口基序号 base 移至 n+1。 |
| 接收方 | 无失序缓存 | 接收方只关注按序到达的报文段(即序列号等于 expectedseqnum 的包)。 |
| 接收方 | 处理失序包 | 收到失序包(或出错包)时,接收方立即丢弃,并发送对上一个按序报文段的 ACK(即重复 ACK)。 |
| 成本 | 带宽浪费 | 丢失一个报文段会导致后续正确到达的 N-1 个报文段被接收方丢弃,最终导致它们全部被重传,严重浪费带宽。 |
选择重传 (Selective Repeat, SR)
SR 是一种以最小化重传为目标,但以增加协议复杂性为代价的协议。
| SR 角色 | 关键机制与原理 | 细节与逻辑 |
|---|---|---|
| 发送方 | 独立定时器 | 为窗口内每一个未确认的报文段设置独立定时器。只有当特定报文段的定时器超时,才重传该报文段。 |
| 发送方 | 独立确认 | 接收方对每一个正确收到的报文段发送 ACK,发送方标记相应报文段为已确认。 |
| 接收方 | 接收方缓存 | 接收方会缓存所有正确接收到的失序报文段。 |
| 接收方 | 交付上层 | 只有当丢失的报文段重传到达,使接收方缓存中形成一个连续序列时,才将该序列一次性交付给上层应用。 |
| 成本 | 序列号约束 | SR 必须严格保证 N <= K/2 ,否则会造成新旧报文段的歧义(见下文详解)。 |
SR 的关键约束:N <= K/2
这是 SR 协议设计中最重要的安全机制。
问题的引入:新旧数据的歧义
假设序列号空间 K=4(序列号为 0, 1, 2, 3)。
| 场景 | GBN 窗口大小 | SR 窗口大小 |
|---|---|---|
| GBN | N=4 是可行的,因为 GBN 丢弃失序包,不会造成歧义。 | SR |
歧义演示:当 N = K 时
假设我们违反规则,让 N=4。
发送方: 成功发送报文段 0, 1, 2, 3。
网络状态: 所有 0, 1, 2, 3 全部正确到达接收方。
灾难发生: 所有 0, 1, 2, 3 的 ACK 全部丢失。
超时重传: 发送方定时器超时,重传 0, 1, 2, 3(此时是第二轮 0, 1, 2, 3$)。
接收方状态: 接收方认为自己已经交付了 0, 1, 2, 3(第一轮),现在它期待下一个序列号是 4 (mod 4) = 0。
当它收到重传的报文段 0 时,接收方无法区分:
- 情况 A: 这是一个新窗口的报文段 0。
- 情况 B: 这是旧窗口报文段 0 的重传(因为旧的 ACK 丢失了)。
由于接收方期待 0,它会误将重传的报文段 0 视为新数据,并交付给上层应用,导致数据重复交付。
N <= K/2 的作用
将窗口大小限制在 K/2 内,保证了在任何时候:
- 发送方窗口中的序列号(未发送或未确认)与
- 接收方窗口中的序列号(已交付或正在等待交付)
这两组序列号集合永远不会重叠,从而排除了新旧报文段的歧义。
常见面试题进阶总结
1:请解释 TCP 的 SACK 机制是如何弥补 GBN 缺陷的?
进阶回答:
TCP 默认采用 GBN 的累积确认。当数据包丢失时,会造成 GBN 式的效率损失。
SACK (Selective Acknowledgement) 是 TCP 的一个可选机制,它允许接收方在 ACK 报文中不仅包含累积确认号,还包含一个或多个 SACK 块。
- SACK 块作用: SACK 块精确地告诉发送方,哪些失序到达的报文段已经被接收方成功缓存。
- SR 效果: 结合 SACK,发送方只需重传那些 既没有被累积确认,也没有被 SACK 确认 的报文段。这实现了 SR 协议的精确重传效果,极大地提高了高丢包环境下的吞吐量。
2:在 GBN 中,如果发送方收到一个重复 ACK,它应该做什么?
进阶回答:
发送方收到重复 ACK 后,不应该立即重传,而是应该继续执行以下动作:
- 不启动定时器: 仅在发送新报文段时才启动定时器。
- 不改变窗口:
base和nextseqnum保持不变,因为重复 ACK 只是对旧数据的确认。 - 等待超时: 持续等待直到定时器超时,然后才执行回退 N 步重传。
注意: 快速重传(TCP 特有机制)是收到三个重复 ACK 后才立即重传,这与 GBN 基础协议处理重复 ACK 的方式不同。
3:为什么说 GBN 协议牺牲了带宽,提高了什么?
进阶回答:
- 牺牲了带宽: 是的,由于其“回退 N 步”的机制,丢失一个包可能导致 $N$ 个包被重传,造成明显的带宽浪费。
- 提高了什么: GBN 极大地简化了接收方的实现逻辑。接收方不需要维护复杂的缓存结构来存储失序报文段,也不需要为每个包发送独立确认。它只需要一个变量
expectedseqnum。在早期的、资源受限的计算机系统中,这种简化是巨大的优势。
面向连接的运输:TCP
TCP连接
在这台主机之间的网络元素(路由器、交换机和中继器)中,没有为该连接分配任何缓存和变量。
TCP的报文段结构
TCP报文段:首部字段 + 数据字段
序号和确认号
TCP把数据看成⼀个⽆结构的、有序的字节流。从TCP对序号的使⽤上可以看出这⼀点,因为序号是建⽴在传送的字节流之上,而不是建立在传送的报文段的序列上。
一个报文段的序号是该报文段首字节的字节流编号。
初始序号总是随机的
这样做可以减少将那些仍在⽹络中存在的来⾃两台主机之间先前已终⽌的连接的报⽂段,误认为是后来这两台主机之间新建连接所产⽣的有效报⽂段的可能性(它碰巧与旧连接使⽤相同的端口号
- TCP 随机初始序号 ISN 的必要性在于:
- 避免旧连接报文误被新连接接受,确保数据流无歧义。
- 防止攻击者预测序号并伪造 TCP 报文,实现连接劫持。
因此随机 ISN 是 TCP 可靠性与安全性的双重基础。
往返时间的估计与超时
超时间隔加倍
每次TCP重传时都会将下一次的超时间隔设为先前值的两倍。
这种修改提供了一个形式受限的拥塞控制
在拥塞的时候,如果源持续重传分组,会是拥塞更加严重。
不过这样会增加端到端的时延。
快速重传
快速重传是基于冗余ACK的,冗余ACK就是,接收方重复收到相同序号的ACK。
一旦收到3个冗余ACK,TCP就执行快速重传。
流量控制
- 流量控制与拥塞控制的区别:
| 项目 | 流量控制 (Flow Control) | 拥塞控制 (Congestion Control) |
|---|---|---|
| 解决的问题 | 快发方 → 慢收方(防止接收缓冲区溢出) | 发方 → 中间网络(防止网络链路/路由器拥塞) |
| 关注的对象 | 端到端(发送方和接收方) | 整个网络(发送方 + 所有中间路由器) |
| 核心变量 | 接收窗口 | rwnd(Receiver Window) |
| 实际塞谁 | 塞接收方的缓冲区 | 塞网络中的路由器队列 |
| 典型机制 | 滑动窗口(由接收方通告) | 慢启动、拥塞避免、快速重传、快速恢复等 |
| 谁决定窗口大小 | 接收方决定并告诉发送方(ACK里的Window字段) | 发送方自己根据网络状况动态估计 |
- 窗口关闭(rwnd=0)与“零窗口探测”(Zero Window Probe)
接收方通告 rwnd=0 时:
- 发送方立刻停止发送数据
- 但不能一直傻等 → 启动一个“持久定时器”(Persist Timer,通常几十秒)
- 定时发送1字节的探测报文(Zero Window Probe)
- 接收方必须回复当前的rwnd
- 一旦rwnd > 0,发送方立刻恢复发送(称为“窗口更新” Window Update)
这避免了“死锁”:如果接收方处理完了但忘了通知发送方,连接就永远卡死了。
- 傻窗口回避(Silly Window Syndrome Avoidance)
问题:接收方一次只处理几个字节,就通告一个很小的rwnd(比如20字节),导致发送方也只发20字节 → 大量小包 → 效率极低。
回避策略:
- 接收方:除非缓冲区至少空出 max(4096字节, 1个MSS) 或者 50% 的缓冲区,否则不发窗口更新(Clark算法)
- 发送方:除非能发1个满长的MSS,否则先攒着不发(Nagle算法配合)
TCP连接过程
TCP 三次握手(建立连接)
目的:确保双方都能发能收,并协商初始序号(防止旧连接的乱序包干扰)
| 次数 | 方向 | 报文 | 关键标志位 | 含义(通俗说) |
|---|---|---|---|---|
| 1 | 客户端 → 服务端 | SYN | SYN=1, seq=x | 客户端:我想跟你建立连接!我的初始序号是x |
| 2 | 服务端 → 客户端 | SYN+ACK | SYN=1, ACK=1, seq=y, ack=x+1 | 服务端:收到!我也想连,我的初始序号是y,你的下个字节应该是x+1 |
| 3 | 客户端 → 服务端 | ACK | ACK=1, seq=x+1, ack=y+1 | 客户端:OK!你的下个字节应该是y+1,连接建好了! |
为什么是三次,而不是两次?
两次会出问题:如果客户端发的第一个SYN在网络中延迟了很久,服务端以为是新的连接请求就建立了半连接(浪费资源),三次可以彻底杜绝历史残留SYN的影响。
三次握手结束后,双方进入 ESTABLISHED 状态,可以开始传输数据。
TCP 四次挥手(释放连接)
目的:确保双方数据都发完,优雅关闭(不能丢数据)
| 次数 | 方向 | 报文 | 关键标志位 | 含义(通俗说) |
|---|---|---|---|---|
| 1 | 客户端 → 服务端 | FIN | FIN=1, seq=u | 客户端:我发完了,你看你还有没有要发的? |
| 2 | 服务端 → 客户端 | ACK | ACK=1, ack=u+1 | 服务端:收到你发完了,我这边还在发数据,先确认一下 |
| 3 | 服务端 → 客户端 | FIN | FIN=1, seq=v | 服务端:我这边也发完了,可以关闭了 |
| 4 | 客户端 → 客户端 | ACK | ACK=1, ack=v+1 | 客户端:好,关闭! |
为什么是四次,而不是三次?
因为TCP是全双工的,A说“我发完了”和B说“我发完了”是两件事。
第2次和第3次可以合并成一个FIN+ACK(很多实现也这么做),但逻辑上仍然是“四次”信息交换,所以叫四次挥手。
状态变化简图
客户端TCP连接:
服务端TCP连接:
关键的 TIME_WAIT 状态(第4次挥手后客户端要等2MSL ≈ 1~4分钟)
原因:
- 可靠实现TCP全双工终止(确保最后一个ACK不丢失)
- 防止上一次连接的旧包干扰下一次相同四元组的新连接
拥塞控制与TCP拥塞控制
经典的TCP拥塞控制
我们称之为“经典”的TCP使⽤端到端拥塞控制⽽不是 ⽹络辅助的拥塞控制,因为IP层对端系统不提供明确的涉及⽹络拥塞的反馈,即为[RFC2581]和近期的[RFC5681]定义的TCP标准版本。
运⾏在发送⽅的TCP拥塞控制机制跟踪⼀个额外的变量,即拥塞窗口(congestionwindow)。拥塞窗口表⽰为cwnd,它对⼀个TCP发送⽅能向⽹络中发送流量的速率进⾏了限制。特别是,在⼀个发送⽅中未被确认的数据量不会超过cwnd与rwnd中的最⼩值,即
TCP的发送限制:[LastByteSent-LastByteAcked < min ewnd, rwnd]
既不会淹没接收方(rwnd约束),又不使网络拥塞(cwnd约束)⬆️
根据“确认”到达的速率动态调整cwnd和发送速率,因此TCP被说成是自计时的。
- 慢启动
- 拥塞避免
- 快速恢复