一、传输层做什么
1.1 服务范围
网络层: 主机 ←—————————————→ 主机 (主机到主机的交付)
传输层: 进程 ←—————————————→ 进程 (进程到进程的交付)
传输层建在网络层之上,把「主机到主机」增强为「进程到进程」。
1.2 家庭信使类比(KR 经典)
东海岸的一家有 12 个孩子,西海岸的一家有 12 个孩子,两家的孩子每周互相写信。
应用层报文 = 信纸
进程 = 孩子
主机 = 房子
传输层协议 = Ann 和 Bill(各家负责收发信的人)
网络层协议 = 邮政服务
- Ann 把弟妹们的信收齐,装进一个大信封交给邮局 → 多路复用
- Bill 收到大信封,拆开按收件人分发给各个弟妹 → 多路分解
- 邮局(网络层)只管把信封从这栋房子送到那栋房子,不认识里面的孩子
📌 这个类比还能解释更多:
- 如果 Ann 记录了哪封信已经送达、没送达的重发 → 这是 TCP
- 如果 Ann 只是转交,丢了不管 → 这是 UDP
- 邮局本身不提供任何可靠性 → 这是 IP 的尽最大努力交付
1.3 传输层可以提供、和不可能提供的
| 能提供 | 不能提供 |
|---|---|
| 可靠数据传输(TCP) | 带宽保证 |
| 有序交付(TCP) | 时延保证 |
| 流量控制(TCP) | |
| 拥塞控制(TCP) | |
| 差错检测(TCP、UDP) |
⭐ 为什么不能提供带宽和时延保证?因为传输层只在端系统上运行,它无法控制网络内部的排队和路由。你不能靠端上的软件凭空创造出网络里不存在的容量。这是端到端原则的另一面:端能做的事有限,端做不到的事,网络不给,就没有。
二、多路复用与多路分解
多路复用(multiplexing):发送端从多个 socket 收集数据,加上头部,交给网络层。 多路分解(demultiplexing):接收端根据头部信息,把报文段交给正确的 socket。
关键问题是:接收端根据什么来分?
2.1 通用头部字段
每个传输层报文段都有:
┌────────────────┬────────────────┐
│ 源端口号 (16) │ 目的端口号 (16) │
├────────────────┴────────────────┤
│ 其他头部字段 │
├─────────────────────────────────┤
│ 应用数据 │
└─────────────────────────────────┘
2.2 无连接的多路分解(UDP)⭐
UDP socket 由二元组标识:
(目的 IP 地址, 目的端口号)
规则:两个源地址完全不同的数据报,只要目的 IP 和目的端口相同,就会被交给同一个 socket。
主机 A (10.0.0.1:9157) ──┐
├──→ 服务器 B (198.51.100.2:6428)
主机 C (10.0.0.5:5775) ──┘ ↓
同一个 UDP socket
📌 源端口号在 UDP 里的用途:它是回信地址。服务器要回复时,把它当作目的端口。
2.3 面向连接的多路分解(TCP)⭐
TCP socket 由四元组标识:
(源 IP, 源端口, 目的 IP, 目的端口)
四个值中任何一个不同,就是不同的 socket。
主机 A (10.0.0.1:9157) ──→ 服务器 B:80 → socket #1
主机 A (10.0.0.1:9158) ──→ 服务器 B:80 → socket #2 ← 源端口不同
主机 C (10.0.0.5:9157) ──→ 服务器 B:80 → socket #3 ← 源 IP 不同
这正是第 9 讲例题里的结论。Web 服务器能在一个 80 端口上服务几十万连接,靠的就是这个四元组。
2.4 对比表(考试必背)
| UDP | TCP | |
|---|---|---|
| socket 标识 | 二元组(目的IP, 目的端口) | 四元组 |
| 一个 socket 能服务多少客户 | 任意多个 | 恰好一个连接 |
| 服务器需要几个 socket | 1 个 | 1 欢迎 + N 连接 |
⚠️ 常见错误:以为 UDP 也用四元组。不是的——UDP 是无连接的,内核里没有「连接」这个概念可供匹配。(例外:应用调用了 connect() 的 UDP socket,会被内核过滤源地址,但那是应用层的便利功能,不改变协议本身。)
三、UDP
3.1 UDP 提供什么
几乎什么都不提供。它在 IP 之上只加了两样东西:
- 多路复用/分解(端口号)
- 差错检测(校验和)
仅此而已。KR 教材的说法很准确:UDP 是「除了复用/分解和轻量差错检测之外,什么都不加」的传输协议。
3.2 为什么还要有 UDP
四个理由,要能说全:
1️⃣ 对何时发什么数据有精确的应用级控制
TCP 会因为拥塞控制而限速、因为可靠传输而重传。对实时应用来说,迟到的数据等于无用的数据——宁可丢掉。
2️⃣ 无需建立连接
TCP 要三次握手(1 个 RTT)。DNS 用 UDP,正是因为一次查询本来就只有一来一回,为它做握手是纯粹的浪费。
3️⃣ 无连接状态
TCP 要维护接收/发送缓冲区、拥塞参数、序号、确认号。一台服务器用 UDP 能支持的活跃客户端数量,远多于用 TCP。
4️⃣ 头部开销小
UDP 头部 8 字节,TCP 头部 20 字节(还常有选项)。对小报文,这个差异不可忽略。
3.3 UDP 报文段结构
0 16 31
┌───────────────────┬───────────────────┐
│ 源端口号 │ 目的端口号 │ 4 字节
├───────────────────┼───────────────────┤
│ 长度(字节) │ 校验和 │ 4 字节
├───────────────────┴───────────────────┤
│ │
│ 应用数据 │
│ │
└────────────────────────────────────────┘
- 长度:头部 + 数据的总字节数(不是只有数据!)。最小值 8(无数据)
- 校验和:见下节。IPv4 中可选(填 0 表示不校验),IPv6 中强制
3.4 用 UDP 的应用
| 应用 | 为什么用 UDP |
|---|---|
| DNS | 一问一答,握手是浪费;丢了重问就行 |
| DHCP | 还没有 IP 地址,谈不上建立连接 |
| SNMP | 网络已经出问题时还要能工作,越简单越好 |
| RTP / WebRTC | 实时音视频,宁可丢帧不可卡顿 |
| QUIC / HTTP3 | 在用户态自己实现可靠性与拥塞控制(第 6 讲) |
| 游戏 | 迟到的位置信息不如丢弃 |
| NTP | 时间同步,重传会引入误差 |
⚠️ 一个重要警告:UDP 没有拥塞控制。如果所有人都用不受控的 UDP 高速发送,网络会进入拥塞崩溃(congestion collapse,第 15 讲)。因此负责任的 UDP 应用必须自己实现某种速率控制——QUIC 做了,WebRTC 也做了(Google Congestion Control)。
四、UDP 校验和
4.1 目标
检测报文段在传输中是否发生了比特翻转。
来源可能是:链路噪声、路由器内存错误、软件 bug。
⚠️ 注意端到端原则又出现了:链路层已经有 CRC,为什么传输层还要校验?因为链路层的 CRC 只覆盖一段链路,而分组在路由器内存中停留时发生的错误,没有任何链路层检查能覆盖。只有端到端的校验能覆盖全程。
4.2 计算方法
发送端:
- 把报文段(含伪首部)看成一串 16 位整数
- 求和(进位要回卷到最低位,这叫「反码和」)
- 结果取反,放进校验和字段
接收端:
- 把所有 16 位字(包括校验和字段)加起来
- 如果结果全是 1(0xFFFF),说明没检出错误;否则有错
4.3 手工计算示例
计算两个 16 位字的校验和:
第一个字: 0110011001100000
第二个字: 0101010101010101
─────────────────
和: 1011101110110101 ← 无进位
假设再加一个字: 1000111100001100
1011101110110101
+ 1000111100001100
─────────────────
10100101011000001 ← ⚠️ 产生了 17 位,最高位溢出
回卷(wraparound):把溢出的 1 加到最低位
0100101011000001
+ 1
─────────────────
0100101011000010 ← 反码和
取反得校验和:
1011010100111101 ← 放进校验和字段
验证(接收端):
0100101011000010 (数据的反码和)
+ 1011010100111101 (校验和)
─────────────────
1111111111111111 ← 全 1 ✅ 无错误
4.4 它检得出什么、检不出什么
✅ 检得出:任何单个比特的翻转,绝大多数随机差错。
❌ 检不出:
- 偶数个比特同时翻转,且互相抵消。例如两个字中同一位置分别 0→1 和 1→0,和不变
- 因此漏检概率约为 2⁻¹⁶ ≈ 1/65536
📌 为什么不用更强的 CRC?因为 UDP/TCP 校验和是在软件里、对每个字节计算的,必须极快。CRC 更强但更慢,被放在有硬件加速的链路层。这是一个明确的性能与强度权衡。
⚠️ 现实警告:在高速链路上,2⁻¹⁶ 的漏检率意味着每传输几十 GB 就可能有一个未被检出的错误。对数据完整性要求高的应用(文件同步、数据库复制),必须自己再做一层强校验(SHA-256)——这又是端到端原则。
4.5 伪首部
TCP/UDP 校验和实际上还覆盖了一个伪首部(pseudo-header),包含:
源 IP 地址 (32) | 目的 IP 地址 (32) | 0 (8) | 协议号 (8) | 长度 (16)
**为什么?**为了检测「报文段被送错了主机」这类错误——如果 IP 地址在传输中损坏,校验和会失败。
⚠️ 但这带来了一个分层污染:传输层的校验和依赖了网络层的地址。这是分层原则的一个著名破例,也正是 NAT 必须重算 TCP/UDP 校验和(而不能只改 IP 头)的原因(第 18 讲)。
五、例题(Worked Example)
题目:某 Web 服务器 IP 为 198.51.100.7,监听 TCP 80 端口和 UDP 53 端口(同时跑 DNS)。以下四个报文段先后到达:
① TCP, 源 (203.0.113.5, 40001), 目的 (198.51.100.7, 80)
② TCP, 源 (203.0.113.5, 40002), 目的 (198.51.100.7, 80)
③ UDP, 源 (203.0.113.9, 33333), 目的 (198.51.100.7, 53)
④ UDP, 源 (198.18.0.4, 44444), 目的 (198.51.100.7, 53)
(a) 这些报文段分别被交给几个不同的 socket? (b) 若 ① 和 ② 来自同一台机器,服务器还能区分它们吗? (c) 若服务器的 DNS 服务想知道该把响应发回给谁,靠什么?
解答:
(a) 三个 socket:
① → TCP socket A,四元组 (203.0.113.5, 40001, 198.51.100.7, 80)
② → TCP socket B,四元组 (203.0.113.5, 40002, 198.51.100.7, 80)
③ ④ → 同一个 UDP socket,二元组 (198.51.100.7, 53)
⭐ 关键点:③ 和 ④ 源地址完全不同,但因为 UDP 只看目的二元组,它们共享同一个 socket。
(b) 能。虽然源 IP 相同,但源端口不同(40001 vs 40002),四元组因而不同。这正是浏览器能对同一站点并行开多条连接的原因(第 6 讲)。
(c) 靠 recvfrom() 返回的源地址与源端口(也就是 UDP 头部里的源端口字段 + IP 头部的源 IP)。应用需要自己保存这个地址,在发送响应时显式指定——这就是第 9 讲 UDP 服务器代码里 sendto(response, client_address) 的由来。
📌 一个推论:UDP 服务器的「会话状态」必须由应用自己维护,因为传输层不帮你记住任何东西。这正是 QUIC 要在 UDP 之上重建连接概念的原因。
六、随堂自测
- 传输层把网络层的什么服务增强成了什么服务?
- 为什么传输层不可能提供带宽和时延保证?
- UDP socket 用几元组标识?TCP 呢?给出理由而不是死记。
- 一台 UDP 服务器同时被 1000 个客户端访问,需要几个 socket?TCP 呢?
- UDP 长度字段包含头部吗?最小值是多少?
- 计算下面两个 16 位字的反码和并给出校验和:
1111000011110000和1111000011110001。 - UDP 校验和检不出什么样的错误?漏检概率量级是多少?
- 为什么说伪首部破坏了分层?它导致了什么实际后果?
七、本讲要点回顾
- 传输层把网络层的主机到主机增强为进程到进程。
- UDP 用二元组分解,TCP 用四元组分解。
- UDP 只提供两样东西:多路复用/分解 + 差错检测。
- 用 UDP 的四个理由:应用级控制、无握手、无连接状态、头部小。
- UDP 头部 8 字节:源端口、目的端口、长度、校验和。
- 反码校验和:求和(进位回卷)→ 取反;接收端验证为全 1。
- 它检不出偶数个互相抵消的比特翻转,漏检率约 2⁻¹⁶。
- UDP 没有拥塞控制,用它的应用必须自己负责,否则会导致拥塞崩溃。
八、自测答案
1. 把网络层的主机到主机的尽最大努力交付,增强为进程到进程的交付(UDP),并可进一步增强为可靠、有序、有流量控制与拥塞控制的字节流(TCP)。
2. 因为传输层协议只在端系统上运行,它对网络内部的排队、路由和其他流的行为没有任何控制权。带宽和时延由路径上的链路容量与竞争流量决定,端上的软件无法凭空改变它们。要提供这类保证,必须由网络内部(资源预留、准入控制)来实现。
3. UDP 用二元组(目的IP, 目的端口),因为 UDP 是无连接的——内核不维护任何「这是哪条连接」的状态,只能按目的地分发。TCP 用四元组,因为 TCP 需要为每条连接维护独立的序号、窗口和缓冲区状态,必须能把不同连接区分开。
4. UDP:1 个。TCP:1001 个(1 个欢迎套接字 + 1000 个连接套接字)。
5. **包含。**UDP 长度字段是「头部 + 数据」的总字节数,最小值为 8(只有头部、没有数据时)。
6.
1111000011110000
+ 1111000011110001
──────────────────
11110000111100001 ← 17 位,有溢出
回卷:1110000111100001 + 1 = 1110000111100010
取反(校验和):0001111000011101
验证:1110000111100010 + 0001111000011101 = 1111111111111111 ✅
7. 检不出偶数个比特翻转且效果互相抵消的情况——例如两个不同的 16 位字在同一位置分别发生 0→1 和 1→0,总和不变。漏检概率量级约为 2⁻¹⁶ ≈ 1.5×10⁻⁵。
8. 伪首部让传输层的校验和覆盖了网络层的 IP 地址,即传输层的正确性依赖于下层的具体字段,违反了「每层只处理自己的头部」的分层原则。实际后果:任何改写 IP 地址或端口的设备(NAT、负载均衡器)都必须重新计算 TCP/UDP 校验和,否则报文会被接收端丢弃。这显著增加了 NAT 的实现复杂度,也是 NAT 无法对某些加密协议(如 IPsec AH)透明工作的原因之一。