#计算机网络学习笔记3
传输层概述
传输层协议为运行在不同主机上的应用进程之间提供逻辑通信
传输层 vs 网络层:
- 网络层:主机之间的逻辑通信
- 传输层:进程之间的逻辑通信
传输层对网络层的报文做了扩展:将主机-主机交付扩展到了进程-进程交付
传输层在端系统中工作:
- 发送端:接收应用层报文,将其分成报文段(segment),填写传输层首部字段,再交给网络层 IP
- 接收端:从 IP 接收报文段,检查首部字段,将载荷重新组装成应用层报文,并通过 socket 交给正确的进程
可靠数据传输、流量控制、拥塞控制这些功能并不只能放在 TCP 里;协议可以把其中一些功能拆到其他层或应用层实现,例如 HTTP/3/QUIC 在 UDP 之上重新实现可靠传输、拥塞控制、加密和连接建立
因特网传输层协议:
- TCP:可靠的、按序的交付(拥塞控制、流量控制、连接建立)
- UDP:不可靠的、无序的交付(尽力而为服务)
- 两者都不保证:延迟、带宽
多路复用与多路分解
多路复用(multiplexing):在源主机,从不同套接字中收集数据块,为每个数据块封装上首部信息(用于后续分解),生成报文段,传递到网络层
多路分解(demultiplexing):在目的主机,将传输层报文段中的数据交付到正确的套接字
实现方式:通过报文段首部中的源端口号和目的端口号
端口号为 16 位整数,范围 0~65535,其中 0~1023 为周知端口号
主机接收一个 IP 数据报时,至少会看到:
- IP 首部中的源 IP 地址和目的 IP 地址
- 传输层报文段首部中的源端口号和目的端口号
- 传输层载荷中的应用数据
多路分解就是利用这些首部字段,把同一台主机收到的多个应用流量分派给正确 socket
多路复用/分解并不是传输层独有的思想,链路层、网络层、应用层也都有类似的“用首部字段区分不同上层/下层实体”的机制。
无连接的多路分解(UDP)
UDP 套接字由二元组标识:(目的 IP 地址, 目的端口号)
来自不同源 IP/端口但具有相同目的 IP 和目的端口的 UDP 报文段会被定向到同一个套接字
创建 UDP socket 时通常需要绑定本地端口号。发送 UDP 数据报时,应用必须显式指定目的 IP 地址和目的端口号;接收方若要回复,则从收到的数据报中提取发送方 IP 和端口。
面向连接的多路分解(TCP)
TCP 套接字由四元组标识:(源 IP 地址, 源端口号, 目的 IP 地址, 目的端口号)
服务器可能同时支持多个 TCP 套接字,每个套接字与不同的客户相对应
Web 服务器为每个连接的客户创建不同的套接字(即使目的端口都是 80)
因此,同一台 Web 服务器可以同时拥有许多目的端口均为 80 的 TCP 连接,只要它们的源 IP 或源端口不同,四元组就不同,服务器就能把报文段交给对应连接 socket。
UDP
用户数据报协议(User Datagram Protocol)
UDP 是”尽力而为”的传输协议:报文段可能丢失、乱序
无连接:UDP 发送方和接收方之间没有握手,每个 UDP 报文段被独立处理
为什么要有 UDP?
- 无需建立连接(减少延迟)
- 简单:发送方和接收方无需维护连接状态
- 首部开销小(8 字节 vs TCP 的 20 字节)
- 没有拥塞控制:可以任意速率发送
UDP 是一种“no frills / bare bones”的传输协议:基本思路是“发送并尽力而为”。报文段可能丢失或乱序,但协议本身不负责恢复
UDP 不保证可靠并不意味着“大部分都错也能用”;现实网络的前提仍然是大多数分组能正确到达,只是 UDP 不在传输层提供额外保证
UDP 的应用:
- 流媒体(容忍丢失、速率敏感)
- DNS
- SNMP
- HTTP/3(基于 QUIC)
如果应用需要在 UDP 之上获得可靠传输或拥塞控制,可以在应用层自行实现,例如 QUIC/HTTP/3 就在 UDP 之上实现可靠性、拥塞控制、连接建立和加密。
UDP 发送方操作:
- 从应用层接收报文
- 确定源端口、目的端口、长度、校验和等首部字段
- 创建 UDP 报文段
- 将报文段交给 IP
UDP 接收方操作:
- 从 IP 接收 UDP 报文段
- 检查校验和和首部字段
- 根据目的端口号多路分解到对应 socket
- 将应用数据交给应用进程
UDP 报文段结构
1 | 0 15 16 31 |
- 长度:UDP 报文段的字节数(首部 + 数据)
- 校验和:用于检测传输中的差错
UDP 长度字段覆盖首部和数据,校验和不仅覆盖 UDP 首部和数据,还会把源 IP、目的 IP、协议号等 IP 层信息作为伪首部参与计算,用于发现“交付到错误主机/错误协议”等问题
UDP 校验和
发送方:
- 将报文段内容视为 16 位整数序列
- 求所有 16 位字的和(溢出回卷)
- 对和取反码得到校验和
接收方:
- 将收到的所有 16 位字(包括校验和)加在一起
- 如果结果全为 1,则无差错;否则检测到差错
Internet 校验和的规则是 1 的补码加法:如果最高位产生进位,需要把进位回卷加到最低位。接收方校验时,所有字加和后应得到全 1
e.g.: 两个 16 位整数相加
1
2
3
4 1 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0
+ 1 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
---------------------------------
1 1 0 1 1 1 0 1 1 1 0 1 1 1 0 1 1 (17位,最高位溢出)溢出回卷:\(1 1 0 1 1 1 0 1 1 1 0 1 1 1 0 1 1 \to 1 0 1 1 1 0 1 1 1 0 1 1 1 1 0 0\)
取反码:\(0 1 0 0 0 1 0 0 0 1 0 0 0 0 1 1\) → 这就是校验和
校验和只能检测一部分差错,保护能力较弱
例如两个 16 bit 字发生相互抵消的 bit 翻转,或者某些字的顺序交换,可能仍得到相同的 1 的补码和
因此 UDP 校验和用于基本差错检测,不等价于加密完整性校验,也不能保证一定发现所有错误
可靠数据传输原理
可靠数据传输是网络中最重要的问题之一
可靠服务抽象:上层看到的是一条可靠信道,数据不会出错、不会丢失、不会乱序;但底层信道可能有 bit 错误、丢包、延迟和乱序,因此 rdt 协议需要在不可靠信道之上实现可靠服务
rdt 的常见接口:
rdt_send(data):上层调用发送数据udt_send(packet):rdt 协议把分组交给不可靠底层信道rdt_rcv(packet):底层信道向 rdt 协议交付分组deliver_data(data):rdt 协议向上层交付正确数据
研究 rdt 时通常先只考虑单向数据传输,但 ACK/NAK 等控制信息仍然需要反向传输
协议用有限状态机(FSM)描述:状态表示协议当前记住的信息,事件触发状态转换,转换时执行发送分组、启动定时器、交付数据等动作
rdt 1.0:经完全可靠信道的可靠数据传输
假设底层信道完全可靠(无比特差错、无分组丢失)
发送方和接收方的 FSM 各只有一个状态:
- 发送方:等待上层调用 → 打包发送 → 等待上层调用
- 接收方:等待下层调用 → 解包交付 → 等待下层调用
rdt 2.0:经具有比特差错信道的可靠数据传输
底层信道可能翻转分组中的比特(但不会丢失分组)
引入差错检测和恢复机制:
- 校验和:检测比特差错
- 确认(ACK):接收方告诉发送方分组已正确接收
- 否定确认(NAK):接收方告诉发送方分组有差错
- 重传:发送方收到 NAK 后重传分组
rdt 2.0 是一个停等协议(stop-and-wait):发送方发出一个分组后,等待接收方响应
rdt 2.0 的缺陷:ACK/NAK 本身可能受损
rdt 2.1:处理受损的 ACK/NAK
解决方法:为分组添加序号(sequence number)
发送方:
- 在每个分组上添加序号
- 如果 ACK/NAK 受损,重传当前分组
- 停等协议只需 1 bit 序号(0 和 1 交替)
接收方:
- 检查序号判断是否为重复分组
- 收到重复分组 → 丢弃并重新发送 ACK
发送方状态数翻倍(需要记住当前序号是 0 还是 1)
关于 rdt 2.1 的讨论:
- 停等协议中一次最多只有一个未确认分组,因此 1 bit 序号(0/1)足以区分“新分组”和“旧分组重传”
- 接收方必须记住期望收到的下一个序号,才能检测重复
- 接收方不知道发送方是否正确收到了 ACK/NAK,因此在收到重复分组时需要重新发送确认
rdt 2.2:无 NAK 的协议
与 rdt 2.1 功能相同,但不使用 NAK
接收方对最后正确接收的分组发送 ACK,ACK 中包含被确认分组的序号
发送方收到重复 ACK(即当前分组的 ACK 序号不是期望的),等价于收到 NAK → 重传
rdt 3.0:经具有比特差错的丢包信道的可靠数据传输
底层信道还可能丢失分组(包括数据分组和 ACK)
方法:发送方等待”合理”的时间,如果没收到 ACK 则重传
引入倒计时定时器(countdown timer):
- 每次发送分组时启动定时器
- 响应定时器中断(超时重传)
- 定时器超时仍未收到正确 ACK → 重传
由于序号机制,重复分组会被接收方检测并丢弃
e.g.: rdt 3.0 的四种场景
场景1:无丢失
1
2
3
4
5 发送方 接收方
pkt0 -------> 收到 pkt0
<------- ACK0
pkt1 -------> 收到 pkt1
<------- ACK1场景2:分组丢失
1
2
3
4
5 发送方 接收方
pkt0 ---X (丢失)
[超时]
pkt0 -------> 收到 pkt0
<------- ACK0场景3:ACK 丢失
1
2
3
4
5
6 发送方 接收方
pkt0 -------> 收到 pkt0
X------- ACK0 (丢失)
[超时]
pkt0 -------> 收到重复 pkt0,丢弃
<------- ACK0 (重新发送)场景4:超时过早
1
2
3
4
5
6
7 发送方 接收方
pkt0 --------> 收到 pkt0
[超时太早] <-------- ACK0 (还在路上)
pkt0 --------> 收到重复 pkt0,丢弃,发 ACK0
<-------- ACK0 (第一个到达)
pkt1 --------> ...
<-------- ACK0 (第二个到达,被忽略)
rdt 3.0 的性能问题
\[U_{sender} = \frac{L/R}{RTT + L/R}\]
其中 \(L/R\) 为传输延迟,\(RTT\) 为往返时间
e.g.: \(L = 8000\) bit, \(R = 1\) Gbps, \(RTT = 30\) ms
\[U_{sender} = \frac{8 \mu s}{30.008 ms} = 0.00027\]
在 1 Gbps 链路上有效吞吐量仅为 267 kbps
停等协议的利用率极低,需要流水线协议
rdt 3.0 在功能上解决了可靠传输,但停等机制会把高速链路“卡”成低速链路
例子中 1 Gbps 链路上的发送有效吞吐只有约 267 kbps,说明协议设计会直接限制底层基础设施的性能;可靠性不是唯一目标,还必须考虑效率
流水线可靠数据传输协议
流水线协议的发送方利用率:
\[U_{sender} = \frac{n \cdot L/R}{RTT + L/R}\]
其中 \(n\) 为允许的未确认分组数(窗口大小)。当 \(n\) 足够大时利用率接近 1
允许发送方在未得到确认的情况下发送多个分组
需要:
- 增加序号范围
- 发送方和/或接收方需要缓冲区
流水线的代价:
- 序号空间必须扩大,否则多个在途分组无法区分
- 发送方要缓存已发送但未确认的数据,以便重传
- 接收方是否缓存乱序分组,取决于 GBN/SR 等具体协议
- 窗口越大,链路利用率越高,但首部序号字段、缓冲区和状态管理开销也越大
流水线在直观上理解就是“一个 RTT 周期内不只让一个包在路上跑”,如果窗口大小为 3,理想情况下利用率可约提升 3 倍;但窗口不可能无限增大,最终受链路容量、缓冲区、序号空间和拥塞控制限制
回退 N 步(Go-Back-N, GBN)
发送方:
- 维护一个大小为 \(N\) 的发送窗口
- 窗口内最多有 \(N\) 个已发送但未确认的分组
- 累积确认:\(ACK(n)\) 表示序号 \(n\) 及之前的所有分组都已正确接收
- 超时事件:重传所有已发送但未确认的分组(回退 N 步)
- 只需维护一个定时器(最早的未确认分组的定时器)
接收方:
- 只需维护一个变量:期望收到的下一个分组的序号
expectedseqnum - 对按序到达的分组发送 ACK
- 对乱序分组丢弃(不缓存),重新发送最近按序接收的分组的 ACK
序号位数为 \(k\) 时,窗口大小 \(N \le 2^k - 1\)
GBN 接收方是 ACK-only:始终确认“到目前为止按序正确收到的最高序号”
乱序分组到达时,即使它本身没有损坏,也会被丢弃或至少不能交付给上层,并重新 ACK 最后一个按序分组。因此丢一个早期分组会导致后续已经到达的分组也被浪费。
选择重传(Selective Repeat, SR)
GBN 的问题:一个分组丢失会导致大量重传
SR:接收方单独确认每个正确接收的分组,发送方仅重传未收到 ACK 的分组
发送方:
- 每个分组有自己的逻辑定时器
- 超时:仅重传该分组
- 收到 \(ACK(n)\):若 \(n\) 在窗口内,标记为已接收;若 \(n\) 为窗口基序号,滑动窗口到最小未确认序号
接收方:
- 维护一个接收窗口
- 分组在窗口内 → 缓存,发送 ACK
- 若收到的分组序号等于接收窗口基序号 → 将连续已收到的分组交付上层,滑动窗口
- 分组在窗口外但在前一个窗口范围内 → 发送 ACK(防止发送方不必要的重传)
序号位数为 \(k\) 时,发送窗口 + 接收窗口 \(\le 2^k\)
通常取发送窗口 = 接收窗口 = \(2^{k-1}\)
SR 与 GBN 的对比:
| GBN | SR | |
|---|---|---|
| 确认方式 | 累积确认 | 逐个确认 |
| 接收方缓存 | 不缓存乱序分组 | 缓存乱序分组 |
| 超时重传 | 重传窗口内所有未确认分组 | 仅重传超时分组 |
| 接收方复杂度 | 简单 | 较高 |
| 窗口大小限制 | \(N \le 2^k - 1\) | \(N_s + N_r \le 2^k\) |
e.g.: GBN 与 SR 在丢包场景下的行为对比
发送方窗口大小 \(N=4\),发送分组 0,1,2,3,4,5,其中分组 2 丢失
GBN 的行为:
GBN 总共重传了 4 个分组(pkt2~pkt5)
1
2
3
4
5
6
7
8
9
10
11
12 发送方: 发送 pkt0, pkt1, pkt2, pkt3
接收方: 收到 pkt0 → ACK0
收到 pkt1 → ACK1
pkt2 丢失
收到 pkt3 → 丢弃(乱序),重发 ACK1
发送方: 收到 ACK0 → 滑动窗口,发送 pkt4
收到 ACK1 → 滑动窗口,发送 pkt5
收到重复 ACK1 → 忽略
pkt2 超时 → 重传 pkt2, pkt3, pkt4, pkt5(回退N步)
接收方: 收到 pkt2 → ACK2
收到 pkt3 → ACK3
...SR 的行为:
SR 只重传了 1 个分组(pkt2),但接收方需要缓存乱序的 pkt3
1
2
3
4
5
6
7
8
9
10 发送方: 发送 pkt0, pkt1, pkt2, pkt3
接收方: 收到 pkt0 → ACK0
收到 pkt1 → ACK1
pkt2 丢失
收到 pkt3 → 缓存,ACK3
发送方: 收到 ACK0 → 滑动窗口,发送 pkt4
收到 ACK1 → 滑动窗口,发送 pkt5
收到 ACK3 → 标记 pkt3 已确认
pkt2 超时 → 仅重传 pkt2
接收方: 收到 pkt2 → ACK2,将 pkt2,pkt3 交付上层,滑动窗口
e.g.: 为什么 SR 的窗口大小需要 \(N_s + N_r \le 2^k\)?
假设序号位数 \(k=2\)(序号空间 0,1,2,3),\(N_s = N_r = 3\)
场景1:发送方发送 pkt0, pkt1, pkt2,接收方全部收到并发送 ACK0, ACK1, ACK2。ACK 全部丢失。发送方超时重传 pkt0
场景2:发送方发送 pkt0, pkt1, pkt2,接收方全部收到,滑动窗口期望 pkt3, pkt0, pkt1。ACK 全部到达发送方,发送方发送新的 pkt3, pkt0, pkt1
在两种场景中,接收方看到的都是 pkt0 到达。但场景1中的 pkt0 是旧数据的重传,场景2中的 pkt0 是新数据——接收方无法区分!
若 \(N_s = N_r = 2\),则 \(N_s + N_r = 4 = 2^k\),窗口不会重叠,问题解决
SR 的核心两难是:接收方看不到发送方状态,只能根据到达分组的序号和自己的接收窗口判断它是新数据还是旧数据重传。如果窗口太大,序号回绕后新旧窗口重叠,接收方在某些场景下会做出完全相同的反应,导致把旧数据当新数据交付,或者把新数据当旧重传丢弃。
TCP
TCP 概述
- 点对点:一个发送方,一个接收方
- 可靠的、按序的字节流:没有”报文边界”
- 全双工:同一连接上双向数据流;MSS(最大报文段长度)
- 面向连接:三次握手初始化发送方和接收方的状态
- 流量控制:发送方不会淹没接收方
- 拥塞控制:发送方不会淹没网络
TCP 报文段结构
1 | 0 1 2 3 |
- 序号(sequence number):报文段数据部分首字节在字节流中的编号
- 确认号(acknowledgment number):期望从对方收到的下一个字节的序号(累积确认)
- 接收窗口(receive window):接收方愿意接收的字节数(用于流量控制)
- 首部长度:以 32 位字为单位(通常 20 字节,即值为 5)
- 标志位:ACK、RST、SYN、FIN 等
常见标志位含义:
| 标志位 | 含义 |
|---|---|
| ACK | 确认号字段有效 |
| SYN | 建立连接时同步初始序列号 |
| FIN | 发送方没有更多数据,准备关闭本方向连接 |
| RST | 复位连接,通常用于异常终止 |
| PSH | 提示接收方尽快把数据交给应用 |
| URG | 紧急指针字段有效 |
| ECE | ECN Echo,接收方通知发送方路径上发生拥塞标记 |
| CWR | Congestion Window Reduced,发送方已响应 ECN 并降低拥塞窗口 |
序号和确认号
序号:该报文段首字节在字节流中的编号
确认号:主机期望收到的下一个字节的编号(累积确认)
e.g.: A 向 B 发送数据
A 已发送字节 0~535,B 已全部收到
B 发送给 A 的报文段中 ACK = 536(表示期望收到第 536 个字节)
捎带(piggybacking):确认被装载在从 B 到 A 的数据报文段中
e.g.: TCP 序号和确认号的跟踪
假设 A 和 B 之间建立了 TCP 连接,A 的初始序号为 42,B 的初始序号为 79,MSS = 500 字节
第一步:A 发送 500 字节数据给 B - seq = 42, ack = 80(期望收到 B 的第 80 字节) - 数据:字节 42~541
第二步:B 收到后回复,同时发送 200 字节数据 - seq = 80, ack = 542(期望收到 A 的第 542 字节) - 数据:字节 80~279
第三步:A 收到后发送 500 字节数据 - seq = 542, ack = 280(期望收到 B 的第 280 字节) - 数据:字节 542~1041
注意:SYN 报文段消耗一个序号,所以第一个数据报文段的 seq = ISN + 1
往返时间估计与超时
RTT 估计
\(SampleRTT\):从发送报文段到收到确认经过的时间(每次只为一个未被重传的报文段测量)
指数加权移动平均(EWMA):
\[EstimatedRTT = (1-\alpha) \cdot EstimatedRTT + \alpha \cdot SampleRTT\]
典型值 \(\alpha = 0.125\)
RTT 偏差的估计:
\[DevRTT = (1-\beta) \cdot DevRTT + \beta \cdot |SampleRTT - EstimatedRTT|\]
典型值 \(\beta = 0.25\)
超时间隔
\[TimeoutInterval = EstimatedRTT + 4 \cdot DevRTT\]
TCP 可靠数据传输
TCP 在 IP 的不可靠服务之上实现可靠数据传输:
- 流水线报文段
- 累积确认
- 单个重传定时器
- 触发重传的事件:超时、收到重复 ACK
TCP 的可靠传输可以看作 GBN 和 SR 思想的混合:
- 使用累积 ACK,类似 GBN
- 接收方实际实现中通常会缓存部分乱序报文段,类似 SR
- 只维护一个重传定时器,通常对应最早未确认报文段
- 通过快速重传避免总是等待超时
TCP 发送方事件
- 从应用层收到数据:创建报文段(含序号),启动定时器(如果尚未运行),定时器对应最早的未确认报文段
- 超时:重传引起超时的报文段,重启定时器
- 收到 ACK:若确认了之前未确认的报文段,更新窗口基序号;若还有未确认的报文段,重启定时器
超时间隔加倍
每次超时后,将下一次的超时间隔设为前一次的 2 倍(指数退避)
收到新数据的 ACK 或来自上层的数据时,恢复原来的 TimeoutInterval
快速重传
如果发送方收到3 个冗余 ACK(即同一报文段的 4 个 ACK),在定时器过期之前重传丢失的报文段
原理:3 个冗余 ACK 强烈暗示该报文段后面的报文段已经到达接收方,只有该报文段丢失
e.g.: 快速重传的含义
假设接收方期望字节 100,但发送方后续字节段陆续到达。由于字节 100 开始的报文段缺失,接收方每收到一个后续乱序段,都会回复
ACK=100。发送方连续收到 3 个冗余ACK=100后,可推断“100 之前的数据还没到,但后面的段已经到了”,于是立即重传从 100 开始的段,而不必等超时
累积 ACK 的效果:如果早期 ACK 丢失,但后面更大的 ACK 到达,例如 ACK=120,它已经隐含确认了 120 之前的所有字节,发送方不需要因为 ACK=100 丢失而重传对应数据
TCP SACK
选择确认(Selective Acknowledgment, SACK):TCP 选项字段中的扩展
标准 TCP 使用累积确认,只能告诉发送方”我收到了 seq X 之前的所有数据”。如果中间有空洞,发送方不知道接收方缓存了哪些乱序报文段
SACK 允许接收方在 ACK 中报告已收到的不连续数据块(最多 3~4 个块的边界),发送方据此仅重传缺失的报文段,而非整个窗口
延迟确认
延迟确认(Delayed ACK):接收方不是每收到一个报文段就立即发送 ACK,而是等待一小段时间(通常 ≤ 500ms)
目的是等待是否有数据要发送给对方,以便捎带确认,减少纯 ACK 报文的数量
规则:
- 收到一个按序报文段,且前一个按序报文段已被确认 → 延迟 ACK,等待最多 500ms
- 收到一个按序报文段,且有一个延迟的 ACK 待发 → 立即发送累积 ACK
- 收到乱序报文段 → 立即发送冗余 ACK(指示期望的序号)
- 收到部分或完全填补 gap 的报文段 → 立即发送 ACK,确认从 gap 小端开始的连续字节
Nagle 算法
Nagle 算法:防止发送大量小报文段(如每次只发 1 字节)
规则:如果发送方有已发送但未确认的数据,则将后续的小块数据缓存起来,直到收到 ACK 或缓存数据积累到一个 MSS 再发送
在交互式应用(如 SSH)中可能导致延迟,可通过 TCP_NODELAY 选项禁用
TCP 流量控制
流量控制:防止发送方发送太快而淹没接收方的缓存
接收方在 TCP 首部的接收窗口字段中告知发送方自己还有多少空闲缓存空间:
\[rwnd = RcvBuffer - [LastByteRcvd - LastByteRead]\]
发送方维护:
\[LastByteSent - LastByteAcked \le rwnd\]
当 \(rwnd = 0\) 时,发送方仍发送只有一个字节数据的报文段(探测报文段),以便接收方回送带有新 \(rwnd\) 值的 ACK
流量控制和拥塞控制不是一回事:
- 流量控制解决接收主机处理不过来的问题,瓶颈在接收方 socket 缓冲区和应用读取速度
- 拥塞控制解决网络处理不过来的问题,瓶颈在路由器队列、链路带宽和网络路径
- 流量控制通过
rwnd限制发送方未确认数据量,拥塞控制通过cwnd限制发送方对网络的注入速率
接收方在 TCP 首部的 rwnd 字段中通告剩余缓冲区。RcvBuffer 可以通过 socket 选项设置,许多操作系统也会自动调节接收缓冲区大小。
TCP 连接管理
三次握手
- 客户发送 SYN 报文段(SYN=1, seq=client_isn)
- 服务器回复 SYNACK 报文段(SYN=1, ACK=1, seq=server_isn, ack=client_isn+1)
- 客户发送 ACK 报文段(ACK=1, seq=client_isn+1, ack=server_isn+1),可以携带数据
为什么需要三次握手(不是两次)?
- 防止已失效的连接请求报文段突然到达服务器,导致建立错误连接
- 确认双方的发送和接收能力
两次握手在理想情况下更快,但在真实网络中会受可变延迟、报文丢失后的重传、报文重排序、旧连接请求滞留等影响。服务器可能在客户端并不打算建立新连接时进入已建立状态,浪费资源或造成状态不一致。三次握手至少让双方都确认对方已收到自己的初始序号,并能协商连接参数
三次握手需要协商的状态包括:双方初始序列号、接收窗口、MSS 等选项。它也不是“完美无缺”,只是比两次握手更稳健
SYN flooding:攻击者大量发送 SYN,但不完成第三次握手,使服务器维持大量半连接状态并消耗资源
这利用了 TCP 三次握手期间服务器需要为未完成连接保留状态的特点。常见缓解方法包括 SYN cookies、缩短半连接超时、过滤异常源等
四次挥手
- 客户发送 FIN 报文段
- 服务器回复 ACK
- 服务器发送 FIN 报文段
- 客户回复 ACK,进入 TIME_WAIT 状态(等待 2MSL 后关闭)
TCP 连接是全双工的,两个方向可以独立关闭。收到对方 FIN 后,本端可以先 ACK,再等自己的应用也结束发送后再发 FIN;如果时机合适,ACK 和本端 FIN 可以合并在同一个 TCP 报文段中。双方也可能几乎同时发送 FIN
TIME_WAIT 的意义:
- 确保最后一个 ACK 能够到达服务器(如果丢失,服务器会重发 FIN)
- 让本次连接中所有报文段都从网络中消失
TCP 状态转换
客户端:CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
服务器端:CLOSED → LISTEN → SYN_RCVD → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
拥塞控制原理
拥塞的代价
- 排队延迟增大
- 路由器缓存溢出导致丢包,需要重传
- 不必要的重传(超时过早触发)浪费带宽
- 上游路由器转发了最终被丢弃的分组 → 浪费了上游的传输容量
拥塞的三个典型场景:
- 无限缓存、单瓶颈链路:不会丢包,但当输入速率接近链路容量时,排队时延会急剧增大,趋向不可接受。带宽利用率过高会以巨大延迟为代价
- 有限缓存、单瓶颈链路:缓存满时丢包,发送方需要重传。实际注入网络的流量包括原始数据和重传数据,因此有效吞吐小于链路容量
- 多跳路径、多流竞争:分组可能在后续路由器被丢弃,前面多跳已经消耗的带宽和缓冲全部浪费。路径越长,越晚丢包,浪费越大
接近 100% 利用率时,队列和时延会迅速恶化;工程上往往需要在吞吐量、时延、丢包率之间折中
拥塞控制方法
- 端到端拥塞控制:网络层不提供显式反馈,端系统通过丢包和延迟来推断拥塞(TCP 采用)
- 网络辅助拥塞控制:路由器向端系统提供显式反馈
- 单个比特指示拥塞(如 ECN)
- 直接告知发送方应该使用的发送速率
端到端方法的优点是不需要网络核心配合,部署简单;缺点是只能从丢包、重复 ACK、RTT 增大等间接信号推断拥塞。网络辅助方法可以更直接,但需要路由器、主机和协议共同支持。
TCP 拥塞控制
TCP 让每一个发送方根据感知到的网络拥塞程度来限制发送速率
发送方维护一个拥塞窗口(congestion window, cwnd)变量:
\[LastByteSent - LastByteAcked \le \min\{cwnd, rwnd\}\]
发送速率近似为 \(\frac{cwnd}{RTT}\) bytes/sec
如何感知拥塞
- 丢包事件:超时或收到 3 个冗余 ACK
- ACK 到达:一切正常,可以增大发送速率
TCP 拥塞控制算法
慢启动(Slow Start)
连接开始时,\(cwnd = 1\) MSS
每收到一个 ACK,\(cwnd\) 增加 1 MSS → 每经过一个 RTT,\(cwnd\) 翻倍(指数增长)
退出慢启动的条件:
- \(cwnd\) 达到 \(ssthresh\)(慢启动阈值)→ 转入拥塞避免
- 检测到丢包(超时)→ \(ssthresh = cwnd/2\),\(cwnd = 1\) MSS,重新慢启动
- 检测到 3 个冗余 ACK → \(ssthresh = cwnd/2\),\(cwnd = ssthresh + 3\) MSS(快速恢复)
拥塞避免(Congestion Avoidance)
当 \(cwnd \ge ssthresh\) 时进入
每经过一个 RTT,\(cwnd\) 增加 1 MSS(线性增长)
实现方式:每收到一个 ACK,\(cwnd = cwnd + \frac{MSS}{cwnd} \times MSS\)
遇到丢包事件:
- 超时:\(ssthresh = cwnd/2\),\(cwnd = 1\) MSS,回到慢启动
- 3 个冗余 ACK:\(ssthresh = cwnd/2\),\(cwnd = ssthresh + 3\) MSS,进入快速恢复
快速恢复(Fast Recovery)
TCP Reno 在收到 3 个冗余 ACK 时进入快速恢复:
- \(ssthresh = cwnd/2\)
- \(cwnd = ssthresh + 3\) MSS
- 每收到一个冗余 ACK,\(cwnd\) 增加 1 MSS
- 收到新的 ACK → \(cwnd = ssthresh\),进入拥塞避免
TCP Tahoe:不论超时还是 3 个冗余 ACK,都将 \(cwnd\) 设为 1 MSS
TCP Reno:超时 → \(cwnd = 1\) MSS;3 个冗余 ACK → 快速恢复
TCP Reno 的吞吐量
忽略慢启动阶段,TCP Reno 的平均吞吐量近似为:
\[\text{avg throughput} = \frac{3}{4} \cdot \frac{W}{RTT}\]
其中 \(W\) 为丢包时的窗口大小
或用丢包率 \(L\) 表示:
\[\text{avg throughput} \approx \frac{1.22 \cdot MSS}{RTT \sqrt{L}}\]
e.g.: TCP 拥塞窗口演化
假设初始 \(ssthresh = 8\),\(cwnd = 1\) MSS,使用 TCP Reno
轮次 cwnd (MSS) 阶段 事件 1 1 慢启动 2 2 慢启动 3 4 慢启动 4 8 慢启动 → 拥塞避免 cwnd = ssthresh 5 9 拥塞避免 6 10 拥塞避免 7 11 拥塞避免 8 12 拥塞避免 收到 3 个冗余 ACK 9 9 快速恢复 → 拥塞避免 ssthresh = 12/2 = 6, cwnd = 6+3 = 9, 收到新 ACK 后 cwnd = 6 9 6 拥塞避免 10 7 拥塞避免 11 8 拥塞避免 12 9 拥塞避免 超时 13 1 慢启动 ssthresh = 9/2 ≈ 4, cwnd = 1 14 2 慢启动 15 4 慢启动 → 拥塞避免 cwnd = ssthresh 16 5 拥塞避免 关键区别: - 3 个冗余 ACK(Reno):\(ssthresh = cwnd/2\),\(cwnd = ssthresh + 3\),进入快速恢复 - 超时(Reno):\(ssthresh = cwnd/2\),\(cwnd = 1\),重新慢启动 - 如果是 Tahoe:不论哪种丢包,都 \(cwnd = 1\),重新慢启动
AIMD
TCP 拥塞控制的核心思想:加性增、乘性减(Additive Increase, Multiplicative Decrease, AIMD)
- 加性增:每经过一个 RTT,\(cwnd\) 增加 1 MSS(拥塞避免阶段)
- 乘性减:丢包时,\(cwnd\) 减半
AIMD 导致 TCP 拥塞窗口呈现锯齿状行为
课堂补充:AIMD 是讨论 TCP 拥塞控制时的高频核心词。它不是集中式算法,而是各端系统基于本地 ACK/丢包信号异步调整,整体上趋向稳定和公平。锯齿状的是发送窗口/发送速率的试探过程;路由器输出链路只要队列中有包,实际输出通常仍按链路速率发送。
TCP CUBIC
TCP Reno 的 AIMD 在高带宽环境中恢复太慢(线性增长回到之前的峰值需要很长时间)
TCP CUBIC 的改进:
设 \(W_{max}\) 为上次丢包时的窗口大小,\(K\) 为 CUBIC 窗口增长到 \(W_{max}\) 所需的时间点
\[W(t) = C(t - K)^3 + W_{max}\]
特点:
- 在远离 \(W_{max}\) 时快速增长
- 在接近 \(W_{max}\) 时缓慢增长(谨慎探测)
- 超过 \(W_{max}\) 后再次快速增长(探索更高带宽)
TCP CUBIC 是目前 Linux 和 Windows 的默认拥塞控制算法
基于时延的拥塞控制
经典 TCP 和 CUBIC 主要通过丢包判断瓶颈链路已经拥塞。基于时延的拥塞控制希望在诱发丢包前发现队列正在增长,目标是“保持管道刚好满,但不要更满”。
基本思路:
- 记录最小 RTT:\(RTT_{min}\),近似表示无拥塞排队时的传播和处理时延
- 用 \(\frac{cwnd}{RTT_{min}}\) 估计无拥塞时可达到的吞吐
- 如果实测吞吐接近无拥塞吞吐,说明路径未明显拥塞,可以线性增加
cwnd - 如果实测吞吐明显低于无拥塞吞吐,说明队列积累、时延增大,应线性减少
cwnd
这类方法试图在维持高吞吐的同时保持低延迟。BBR 是典型例子,已在 Google 内部骨干等场景部署。
显式拥塞通知(ECN)
网络辅助拥塞控制:
- IP 首部的 ToS 字段中有 2 位 ECN 比特
- 路由器在经历拥塞时设置 ECN 比特(标记而非丢弃)
- 接收方在 TCP ACK 中设置 ECE(ECN-Echo)比特通知发送方
- 发送方收到 ECE 后减小发送速率(类似于收到 3 个冗余 ACK 的反应)
ECN 的反馈路径通常需要一个 RTT:路由器在数据包上标记拥塞,接收方收到后再通过 ACK 把 ECE 反馈给发送方。因此它比路由器直接通知发送方慢一些,但比“等到丢包再反应”更温和。
TCP 公平性
公平性目标:如果 \(K\) 个 TCP 会话共享一条瓶颈带宽为 \(R\) 的链路,每个会话的平均速率应为 \(R/K\)
在理想条件下(相同 RTT、相同路径),TCP 的 AIMD 确实趋向于公平分配
但实际中 RTT 更短的连接可以更快地增大窗口,获得更大的带宽份额
UDP 流量没有拥塞控制,不受 TCP 公平性约束
公平性的例外和问题:
- 多媒体应用常用 UDP,以恒定速率发送音视频并容忍丢包,不会自动遵守 TCP 的 AIMD 公平性
- 应用可以打开多个并行 TCP 连接来获得更多份额。例如链路上已有 9 条 TCP 连接,新应用开 1 条连接大约只能拿到 \(R/10\);如果开 11 条并行连接,可能获得接近 \(R/2\)
- RTT 不同、路径不同、拥塞控制算法不同都会破坏简单的 \(R/K\) 公平模型
长肥管道中的 TCP 吞吐
“长肥管道”(long, fat pipe)指 RTT 很大且带宽很高的路径。要跑满这类链路,需要非常大的在途数据量:
\[\text{in-flight bytes} \approx \text{bandwidth} \times RTT\]
e.g.: 10 Gbps 链路,RTT = 100 ms,MSS = 1500 字节
需要的在途数据量:
\[10^{10}\text{ bit/s} \times 0.1s = 10^9\text{ bit} = 125\text{ MB}\]
对应报文段数量:
\[\frac{125\text{ MB}}{1500\text{ B}} \approx 83333 \text{ 个 MSS}\]
这要求窗口足够大、丢包率极低。根据 Mathis 近似公式,要达到 10 Gbps 级吞吐,允许的丢包率必须非常小;因此高带宽长 RTT 场景需要适合的 TCP 变体和窗口扩展。
传输层功能的演变:QUIC
TCP 和 UDP 是长期主流传输协议,但传输层功能正在演变。一个趋势是:把原来由 TCP 提供的一些功能移动到 UDP 之上的应用层协议中,由应用更灵活地组合可靠性、拥塞控制、加密和连接管理。
QUIC(Quick UDP Internet Connections)是 HTTP/3 使用的传输基础:
- 运行在 UDP 之上,便于穿过已有 NAT 和中间盒
- 在用户态实现可靠数据传输、丢包检测和拥塞控制
- 默认集成加密和身份认证
- 单个 QUIC 连接内可复用多个应用层流
- 多个流共享拥塞控制,但每个流有独立的可靠传输状态
- 连接建立时把可靠性、拥塞控制、认证和加密状态合并,通常 1 RTT 即可建立,恢复连接时可支持 0-RTT
QUIC 与 TCP+TLS 的连接建立差异:
- TCP+TLS 通常需要先完成 TCP 三次握手,再完成 TLS 握手,握手串行增加时延
- QUIC 把传输握手和加密握手合并,减少首次请求前等待的 RTT 数
QUIC 解决 HTTP/2 over TCP 的一个关键问题:TCP 是按序字节流,一个 TCP 段丢失会阻塞同一连接上的所有 HTTP/2 流;QUIC 在 UDP 之上自己管理流,某个流的数据丢失只阻塞该流,不会阻塞其他流,减少队头阻塞(HOL blocking)
小结
传输层核心知识:
- 多路复用/分解:UDP 用二元组,TCP 用四元组
- UDP:简单、无连接、低开销,校验和提供基本差错检测
- 可靠数据传输原理:rdt 1.0 → 2.0 → 2.1 → 2.2 → 3.0,逐步解决比特差错和丢包问题
- 流水线协议:GBN(累积确认、丢弃乱序)vs SR(逐个确认、缓存乱序)
- TCP:可靠字节流、序号/确认号、超时重传与快速重传、流量控制、三次握手与四次挥手
- 拥塞控制:慢启动 → 拥塞避免 → 快速恢复;AIMD 锯齿行为;TCP CUBIC 的立方增长;基于时延的拥塞控制;ECN
- 传输层功能演变:QUIC 在 UDP 之上重新组合可靠传输、拥塞控制、加密、连接建立和多流复用