第 3 章 · 传输层:UDP 与 TCP

先在自己机器上跑一条命令:

$ ss -tln
State   Recv-Q  Send-Q   Local Address:Port     Peer Address:Port
LISTEN  0       32       192.168.122.1:53        0.0.0.0:*
LISTEN  0       4096     127.0.0.1:631           0.0.0.0:*
LISTEN  0       511      127.0.0.1:4000          0.0.0.0:*

这是传输层最好的开胃菜。53 是 DNS,631 是 Linux 打印服务(CUPS),4000 大概是我自己起的开发服务器。这些都是你机器上正在监听的端口,也就是说,你的电脑此刻就跑着一堆服务器。传输层的一切都围绕这张表展开:IP 地址把包送到机器,端口号决定交给机器上的哪个程序。

3.1 端口号:一台机器上的门牌号

端口号 16 位,0 到 65535,分三段:0–1023 是知名端口,约定俗成给系统服务(HTTP 80、HTTPS 443、DNS 53、SSH 22);1024–49151 可注册;49152–65535 是临时端口,系统随手分配给客户端用,你每次发起连接,源端口就是从这里抽的。

IP:端口 合起来唯一确定通信的一端,这一对儿也叫 socket(编程接口那层意思见第 9 章)。同一台服务器上,Web 服务占着 443,SSH 占着 22,互不干扰;反过来,两个程序不能同时 bind 同一个 IP:端口。

这里有个我踩过的小坑。第一次写 socket 程序,想当然地把服务器绑在 80 端口,一跑:

bind: Permission denied

排查半天代码,最后才反应过来:1024 以下的端口是特权端口,普通用户(非 root)无权监听,这是历史遗留的规矩(那会儿只有系统管理员能跑可信服务)。开发时用 8000 以上的端口,是每个初学者都会学到的教训。

3.2 UDP:八个字节的极简主义

UDP 首部只有 8 字节,是整个 TCP/IP 里最寒酸的首部:

 0                   15 16                  31
┌────────────────────┬────────────────────┐
│      源端口         │      目的端口       │  4 字节
├────────────────────┼────────────────────┤
│      长度           │      校验和         │  4 字节
└────────────────────┴────────────────────┘

长度字段是首部加数据的总字节数。校验和值得一提:IP 头自己有校验和,以太网帧尾还有 CRC,UDP 为什么还要再算一遍?因为那两处校验都是逐跳的,帧每过一个路由器就被剥掉重建,如果错误发生在路由器的转发软件里,逐跳校验全都能「干净地」通过。UDP 校验和提供的是端到端的保证:从发送进程到接收进程,数据没变。顺带一提,IPv4 里这个字段可以填 0 表示偷懒不算,IPv6 里强制要算。

UDP 无连接、无握手、无确认、无重传、无拥塞控制,sendto 出去就不管了。换来的是极低的开销和延迟,所以 DNS 查询、DHCP、音视频直播、游戏实时状态都住在这里。丢一个视频帧,下一帧照样播,这种场景要可靠反而碍事。

不过「不可靠」挡不住有人要可靠:QUIC(HTTP/3 的底座)就是在 UDP 之上自己实现了可靠、有序、加密。UDP 把自由留给了应用层。

3.3 TCP 首部:每个字段都是为后面的章节埋的伏笔

TCP 首部至少 20 字节,固定部分如下:

 0                   15 16                  31
┌────────────────┬────────────────┬────────────────────┐
│    源端口       │    目的端口     │                    │
├────────────────┴────────────────┤     序号 seq        │ 32 位
│         确认号 ack              │                    │ 32 位
├──────┬───┬─────────────────────┼────────────────────┤
│数据偏移│保留│ URG ACK PSH RST SYN FIN │   窗口大小    │
├──────┴───┴─────────────────────┼────────────────────┤
│      校验和                     │    紧急指针         │
└─────────────────────────────────┴────────────────────┘
│              选项(可变长)+ 填充                     │

字段不用死背,记住每个字段为哪章服务:

  • 序号 seq 和确认号 ack 按字节编号,是可靠与有序的全部基础,第 4 章的主角。
  • 标志位里,SYN、FIN、ACK 三个管连接的建立与拆除(第 4 章);RST 管异常复位,你访问一个服务器上没开的端口,对端内核回的就是 RST,curl 翻译成一句 Connection refused,这个报错你早晚见过。
  • 窗口大小是接收方在喊话「我还能收多少字节」,流量控制的依据(第 4 章)。
  • 选项里最常见的是 MSS(最大段大小),以太网上双方通常协商 1460:1500 的 MTU 减去 IP 和 TCP 各 20 字节头(第 1 章算过这笔账)。URG 和紧急指针在现实中基本绝迹,考试认得即可。

3.4 字节流与报文:新手最容易翻车的区别

TCP 是字节流,UDP 是数据报,这句话每个教材都写,但直到自己写 socket 程序才会撞上它的真实含义(第 9 章有完整实验)。这里先把现象说清:发送方连做两次 send,各发 5 字节;接收方 UDP 的 recvfrom 要么收到 5 字节,要么收到 5 字节,两次 send 对两次 recv,边界分毫不差。换成 TCP,接收方一次 recv 可能一口气读到 10 字节,也可能读到 5 字节,也可能读到 7 字节,TCP 只保证字节不丢不重不乱序,完全不承诺「你 send 几次我就 recv 几次」。

这个差别有一堆衍生结论:TCP 上发消息要自己定边界(长度前缀或分隔符),否则就是「粘包」问题;UDP 报文太大超出接收缓冲区时会被直接截断,多余字节扔掉,不会分两次给。第 9 章会用真实的程序输出把这件事钉死。

3.5 怎么选

维度TCPUDP
连接面向连接(三次握手)无连接
可靠性确认 + 重传,保证到达尽力而为,可能丢、乱序、重复
顺序字节流有序不保证
边界无(字节流)保留报文边界
首部开销至少 20 字节8 字节
流量/拥塞控制有无
典型应用HTTP、SSH、邮件、 FTPDNS、DHCP、直播、游戏、QUIC

一句话给判断标准:丢不得的用 TCP,丢得起、等不起的用 UDP。网页、登录、文件传输出不得;直播画面、游戏位置同步、DNS 查询等不起,重传一个过时的视频帧毫无意义。

练习

  1. 用 ss -tln(或 netstat -tlnp)列出你机器的监听端口,挑一个不认识的,查 /etc/services 或搜一下它是什么服务。
  2. 手算:UDP 数据报携带 100 字节数据,UDP 首部长度和长度字段各是多少?算上 IP 头,这个 IP 包总长多少?
  3. 思考:QUIC 为什么选择架在 UDP 上,而不是向 IANA 申请一个新的 IP 协议号?(提示:想想互联网中间的 NAT 和防火墙设备对待「陌生协议号」的态度)
  4. 思考:你 curl http://某服务器:9999,若该端口没有服务在听,你会收到 TCP 的什么标志位?这个交互发生在三次握手完成之前还是之后?