PHASE 0 · 网络基础重建

0.4传输层

端口、TCP 握手与流控,以及 UDP

0.3 把数据包送到了正确的机器。可一台机器上同时跑着浏览器、SSH、数据库、十几个后台服务——这个包到底交给谁?这就是传输层的第一件事:用端口给程序编号。这一章的主题:数据到了机器,交给哪个程序;以及,怎么把它可靠地传过去。

一句话定义

传输层(Transport Layer):在"机器到机器"之上解决"进程到进程"——用端口区分程序,用 TCP / UDP 决定怎么传。

1端口:数据交给哪个程序

端口(Port)是一个 16 位编号(0–65535)。IP 把包送到机器,端口把它送到机器里具体的某个程序或连接。一个 TCP 连接由四元组(four-tuple)唯一确定:源IP : 源端口 ↔ 目的IP : 目的端口

0–1023 是知名端口(well-known):80 HTTP、443 HTTPS、22 SSH、53 DNS……0.1 里 curl 连的是 example.com 的 443 端口,那个 443 就是这里说的端口。

类比:楼层地址 + 房间号,或者一次哈希查找

IP 像写字楼地址,端口像房间号:楼把信送到,房间号决定交给谁。对内核更精确的说法是——它拿四元组当 key,在连接表里查出该把数据交给哪个 socket,就是一次哈希查找。这个动作叫多路分解(demultiplexing)

看本机在监听哪些端口(哪些程序在等连接),以及现有连接的四元组:

bash
ss -tlnp           # Linux:监听中的 TCP 端口(-t TCP  -l 监听  -n 数字  -p 进程)
ss -tn             # Linux:已建立的连接,每行就是一个四元组
lsof -iTCP -P -n   # macOS

2TCP 三次握手

TCP(Transmission Control Protocol)是面向连接、可靠、有序的字节流。传数据前,双方先用三次握手建立连接——目的是同步彼此的初始序列号(ISN),并互相确认"我能发、你能收":

客户端 (Client)服务端 (Server)
1
SYN →  seq = x 我想建连,我的起始序号是 x
2
← SYN + ACK  seq = y, ack = x+1 收到你的 SYN;这是我的起始序号 y,并确认你的 x
3
ACK →  ack = y+1 确认你的 y;连接建立,开始传数据

为什么是三次而不是两次?服务器发完 SYN+ACK 后,需要客户端的那个 ACK 来确认"客户端确实收得到我"。少了第三步,服务器无法确认反向通路通不通。这正是 0.1 里 curl 的 connect() 触发的过程。

建连三次,断连四次

关闭连接比建立多一步:四次挥手(FIN → ACK → FIN → ACK),因为两个方向要各自关闭(我说完了,但你可能还没说完)。细节这里不展开,记住"三次建、四次断"即可。

抓一次握手,亲眼看这三个包:

bash
sudo tcpdump -i any -n 'tcp port 443'    # macOS 换成 -i en0
curl https://example.com

你会看到 Flags [S](SYN)→ [S.](SYN-ACK)→ [.](ACK),正是三次握手;之后才轮到 TLS 握手和应用数据。回到 0.1 那个实验——现在这些 flag 你能读懂了。

补 0.3 的尾巴:TTL 与 traceroute

还记得 0.3 那个 ttl=63 吗?TTL 在 IP 头里、每过一跳减 1,归零就被丢弃,用来防止包在环路里永远打转。traceroute 正是利用它:故意发 TTL=1、2、3… 的包,逼沿途每一跳依次"超时回报",从而一跳跳探出整条路径。跑 traceroute example.com(macOS 自带)或 tracepath example.com(Linux)试试。

3可靠传输:序号、ACK、重传

回忆 0.3:IP 是尽力而为(best-effort)——可能丢包、可能乱序、可能重复。那 TCP 的"可靠"从哪来?它在 IP 之上补了三件套:

  • 序列号(sequence number):给字节流里每个字节编号,接收方据此重排乱序、丢弃重复;
  • 确认(ACK):接收方回告"我已经收到到第几号了";
  • 重传(retransmission):发送方在超时内没等到 ACK,就把那段重发一次。
类比:在不可靠信道上构建可靠传输

这套"编号 + 确认 + 超时重发",和你在分布式系统里见到的 at-least-once 投递 + 按序号幂等去重是同一种思路。区别只是 TCP 把它做进了内核,对应用透明。

4流控与拥塞控制:别冲垮对方,也别压垮网络

能传了,还得控制速度。TCP 装了两个独立的"刹车":

  • 流量控制(flow control):接收方用窗口大小(window size)告诉发送方"我缓冲区还能收多少",发送方据此限速。靠滑动窗口(sliding window)实现。它保护的是慢的接收方
  • 拥塞控制(congestion control):防止把网络本身塞爆。从小窗口慢启动(slow start)探起,一遇丢包就退让;常见算法有 CUBIC(Linux 默认)、BBR(Google)。它保护的是网络

两个刹车同时生效,取更严格的那个决定实际发送速率。

类比:流量控制就是 TCP 的背压(backpressure)

接收方处理不过来,就缩小窗口让发送方慢下来——和你在 Kotlin Flow / 响应式流里见到的背压是同一个思想,只是 TCP 把它做在了传输层。下游顶不住,信号自动往上游传。

5UDP:要快不要可靠

UDP(User Datagram Protocol)走了相反的路:只在 IP 上加个端口就发,不连接、不确认、不排序、不限速。丢了就丢了,换来的是几乎为零的开销和最低的延迟。

TCPUDP
连接面向连接(先握手)无连接,直接发
可靠性可靠(ACK + 重传)尽力而为,可能丢
顺序保证有序不保证
开销 / 延迟较高极低
典型场景网页、SSH、文件传输DNS、音视频、游戏、QUIC

UDP 用在哪:DNS(0.5 马上就用)、实时音视频通话、在线游戏,以及 QUIC / HTTP3——后者干脆在 UDP 之上由应用自己重建可靠性,绕开内核 TCP 的历史包袱。

类比:fire-and-forget vs 带确认的投递

UDP 是"发完即忘(fire-and-forget)",TCP 是"带确认的可靠投递"。要省心就用 TCP;要自己精细掌控每个包(像 QUIC 那样)就用 UDP 自己搭一套。

路由器关联 router-link

你家路由器做端口转发(把外网某端口的流量转进内网某台机器),本质是 DNAT——按目的端口改写目的地址。它能把回包正确送回内网,靠的是 conntrack 记下的连接四元组。OpenWRT 的 firewall4 里,一条 port forward 规则必须指定 TCP 还是 UDP——正因为传输层有这两种独立协议;而且 TCP 有明确的握手 / 挥手状态可跟踪,UDP 没有连接状态,只能靠超时老化来"猜"连接结束。这条线 0.6 讲 NAT 和 conntrack 时会完整展开。

本章小结

  • 端口(16 位)区分机器上的程序;一个 TCP 连接由四元组唯一确定,内核靠它把包分给正确的 socket(多路分解)。
  • TCP:面向连接(三次握手)、可靠(序号 + ACK + 重传)、有序;在尽力而为的 IP 上补出可靠性。
  • 流量控制(窗口)保护接收方,拥塞控制保护网络,共同限速;流控 = 传输层的背压
  • UDP:无连接、不可靠、低延迟;DNS、音视频、QUIC 在用。
  • 路由器的端口转发按"协议 + 端口"工作,回包靠 conntrack 的四元组——0.6 展开。

动手练习

  1. ss -tlnp(或 lsof -iTCP -P -n),挑一个监听端口,说出它是哪个程序、为什么用这个端口。
  2. 抓一次 TCP 握手:sudo tcpdump -n 'tcp port 443',然后 curl 一个 https 站点,把 [S] / [S.] / [.] 三个包找出来。
  3. 思考题:DNS 查询通常用 UDP(53 端口)。为什么 DNS 适合 UDP 而非 TCP?(提示:一次查询就一问一答、包很小,握手那三个来回的代价划不划算?)
  4. 进阶:跑 traceroute example.com(或 tracepath),数一数到目标经过几跳;结合 0.3 的 ttl=63,解释 traceroute 是怎么用 TTL 一跳跳探路的。