先在自己机器上跑一条命令:
$ 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 地址把包送到机器,端口号决定交给机器上的哪个程序。
端口号 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 以上的端口,是每个初学者都会学到的教训。
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 把自由留给了应用层。
TCP 首部至少 20 字节,固定部分如下:
0 15 16 31
┌────────────────┬────────────────┬────────────────────┐
│ 源端口 │ 目的端口 │ │
├────────────────┴────────────────┤ 序号 seq │ 32 位
│ 确认号 ack │ │ 32 位
├──────┬───┬─────────────────────┼────────────────────┤
│数据偏移│保留│ URG ACK PSH RST SYN FIN │ 窗口大小 │
├──────┴───┴─────────────────────┼────────────────────┤
│ 校验和 │ 紧急指针 │
└─────────────────────────────────┴────────────────────┘
│ 选项(可变长)+ 填充 │
字段不用死背,记住每个字段为哪章服务:
Connection refused,这个报错你早晚见过。TCP 是字节流,UDP 是数据报,这句话每个教材都写,但直到自己写 socket 程序才会撞上它的真实含义(第 9 章有完整实验)。这里先把现象说清:发送方连做两次 send,各发 5 字节;接收方 UDP 的 recvfrom 要么收到 5 字节,要么收到 5 字节,两次 send 对两次 recv,边界分毫不差。换成 TCP,接收方一次 recv 可能一口气读到 10 字节,也可能读到 5 字节,也可能读到 7 字节,TCP 只保证字节不丢不重不乱序,完全不承诺「你 send 几次我就 recv 几次」。
这个差别有一堆衍生结论:TCP 上发消息要自己定边界(长度前缀或分隔符),否则就是「粘包」问题;UDP 报文太大超出接收缓冲区时会被直接截断,多余字节扔掉,不会分两次给。第 9 章会用真实的程序输出把这件事钉死。
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 确认 + 重传,保证到达 | 尽力而为,可能丢、乱序、重复 |
| 顺序 | 字节流有序 | 不保证 |
| 边界 | 无(字节流) | 保留报文边界 |
| 首部开销 | 至少 20 字节 | 8 字节 |
| 流量/拥塞控制 | 有 | 无 |
| 典型应用 | HTTP、SSH、邮件、 FTP | DNS、DHCP、直播、游戏、QUIC |
一句话给判断标准:丢不得的用 TCP,丢得起、等不起的用 UDP。网页、登录、文件传输出不得;直播画面、游戏位置同步、DNS 查询等不起,重传一个过时的视频帧毫无意义。
ss -tln(或 netstat -tlnp)列出你机器的监听端口,挑一个不认识的,查 /etc/services 或搜一下它是什么服务。curl http://某服务器:9999,若该端口没有服务在听,你会收到 TCP 的什么标志位?这个交互发生在三次握手完成之前还是之后?