Skip to content

计算机网络全景图:从一根电缆到一次网页加载

网络这门知识有个奇怪的现象:几乎每个程序员都学过,但很少有人觉得自己"懂"。

原因不在深度,在碎片化。你可能背过三次握手,也配过 Nginx,还查过 DNS,甚至排过一次 MTU 问题——但这些知识彼此不连通。于是遇到真实故障时,你不知道该从哪一层开始怀疑:是 DNS 解析错了?路由不通?TCP 建不上连接?TLS 证书有问题?还是应用层的 Host 头写错了?

这篇文章的目标不是把任何一层讲透——那需要一本书。它要做的是给你一张完整的地图:每一层负责什么、有哪些关键协议、它们在真实链路上如何咬合,以及排障时该怎么按层切分。

广度优先,深度适中。看完之后,你应该能对着任何一个网络问题说出"这大概是第几层的事"。

这篇文章怎么读

按顺序读一遍会得到完整图景,但也可以当手册跳读:

  • 第一节是全文的骨架(分层与封装),跳过它后面会散架;
  • 第二到五节是自下而上的四层巡礼,每节都是"这层解决什么问题 + 关键协议 + 常见坑";
  • 第六节把前面全部串成一条真实链路,是我认为最值得读的一节;
  • 第七到九节是现实世界的补充:云和 CDN 时代的网络、性能模型、排障工具箱。

文中所有 RFC 编号、端口、默认值都是可核实的事实。凡是"通常/多数实现"这类措辞,说明具体行为依赖操作系统或厂商实现。

一、分层:网络最重要的那一个概念

如果只能记住一件事,就记这个。

为什么要分层

想象你要设计一个全球通信系统,需要同时解决:怎么在铜线里表示 0 和 1、怎么让数据穿过几十个不同厂商的设备、怎么在丢包的链路上保证可靠、怎么让浏览器和服务器互相理解。

这些问题的变化速度完全不同:物理介质从铜线换到光纤换到 WiFi,但"IP 地址"这个概念三十年没变;应用协议从 Gopher 换到 HTTP 换到 gRPC,但 TCP 一直在下面。

分层就是按变化速度切分:让每层只关心自己的事,通过稳定的接口和上下层打交道。于是换掉 WiFi 不影响 HTTP,发明 HTTP/3 不需要改路由器。

这带来了网络世界最重要的一个性质:沙漏形状

   应用层    HTTP  DNS  SMTP  SSH  gRPC  WebRTC  MQTT  ...   ← 五花八门
             \        \      |      /        /
   传输层         TCP        UDP        (QUIC 在 UDP 上)
                    \                /
   网络层    ═══════════  IP  ═══════════      ← 细腰:全世界只有这一个
                    /                \
   链路层      以太网  WiFi  PPP  5G  卫星  ...  ← 五花八门
   物理层      铜线  光纤  电磁波  ...

IP 是那个细腰。 上面协议无穷多、下面介质无穷多,中间只有一个 IP。这就是所谓"IP over everything, everything over IP"——互联网能长成今天这样,靠的就是这个腰足够细、足够简单、足够不做承诺。

两套模型:OSI 与 TCP/IP

OSI 七层TCP/IP 实用五层干什么代表协议
7 应用层应用层具体业务语义HTTP、DNS、SMTP、SSH
6 表示层(并入应用层)编码、加密、压缩TLS 常被算在这一层半
5 会话层(并入应用层)会话管理现实中很少单独实现
4 传输层传输层端到端可靠性、端口复用TCP、UDP、QUIC
3 网络层网络层全局寻址与路由IP、ICMP、路由协议
2 数据链路层链路层相邻节点间传帧、本地寻址以太网、WiFi、ARP
1 物理层物理层比特变信号双绞线、光纤、无线电

OSI 是学术界的参考模型(七层齐全但工程上没人完整实现),TCP/IP 是工业界实际跑的东西。面试背 OSI,干活用 TCP/IP。

TLS 的位置是个经典争议:它跑在 TCP 之上、为应用层提供加密,既不属于传输层也不属于应用层,通常被称为"第 4.5 层"。QUIC 更混乱——它在 UDP 上重新实现了传输层功能并内置 TLS,属于把 4、4.5 层揉在一起。

封装:数据下楼时穿了几层衣服

这是分层在字节层面的体现。你发一个 HTTP 请求,它下楼时每层都往外包一圈头部:

应用层     ┌──────────────────────────────────────┐
           │            HTTP 请求                  │
           └──────────────────────────────────────┘
                            │ 交给 TCP
传输层     ┌────────┬─────────────────────────────┐
           │TCP 头  │       HTTP 请求              │   ← 段(Segment)
           │20 字节 │                             │      加了:源/目的端口、序号
           └────────┴─────────────────────────────┘
                            │ 交给 IP
网络层     ┌───────┬────────┬────────────────────┐
           │IP 头  │TCP 头  │    HTTP 请求        │   ← 包(Packet)
           │20 字节│        │                    │      加了:源/目的 IP、TTL
           └───────┴────────┴────────────────────┘
                            │ 交给以太网
链路层     ┌────────┬───────┬───────┬───────┬────┐
           │以太网头│IP 头  │TCP 头 │ HTTP  │FCS │   ← 帧(Frame)
           │14 字节 │       │       │       │4B  │      加了:源/目的 MAC、校验
           └────────┴───────┴───────┴───────┴────┘

物理层                比特流 → 电信号 / 光信号 / 电磁波

对端收到后逐层拆包:网卡拆以太网头、内核拆 IP 头、TCP 拆 TCP 头、最后把 HTTP 请求交给应用。

这张图能解释一个高频问题:数据包最大能多大

以太网的载荷上限(MTU)通常是 1500 字节。这 1500 要装 IP 头(20)+ TCP 头(20)+ 数据,所以 TCP 一次最多带 1460 字节数据(这就是 MSS)。

在此基础上再叠加任何隧道(VPN、VXLAN、IPsec、GRE)都会吃掉更多空间——这是"用了 VPN 之后有些网站打不开"的经典成因,第三节详细讲。

二、物理层与链路层:如何把比特送给"隔壁"

这两层的任务范围很小:只负责把数据送到直连的下一个设备。跨越整个互联网是网络层的事。

物理层:比特怎么变成信号

介质典型速率特点
双绞线(Cat5e/6/6a)1–10 Gbps便宜,距离限 100m
多模光纤10–100 Gbps短距离(数百米–2km),数据中心内部主力
单模光纤100 Gbps–1.6 Tbps长距离,跨城跨洋
WiFi(802.11ax/be)理论 1–40 Gbps共享介质,实际远低于理论值
5G理论 1–10 Gbps移动性好,延迟抖动大
卫星(LEO)100–300 Mbps覆盖偏远,延迟 20–50ms(GEO 高达 600ms)

这一层要解决的核心问题是编码与同步:接收方怎么知道一个比特开始和结束?用什么波形表示 1?(曼彻斯特编码、4B/5B、PAM4 等等。)日常开发几乎不会碰到,但有一个概念要记住:

光速是延迟的物理下限

光在光纤里因为折射率(约 1.47)只能跑到真空光速的 68%,约 20 万公里/秒,即 5 微秒/公里

所以:

  • 北京到上海直线 1100 km → 单程 5.5ms,往返 11ms(实际因绕路和设备,30–40ms)
  • 北京到旧金山约 10000 km → 单程 50ms,往返 100ms(实际 150–200ms)

这个下限无法优化。 没有任何软件技巧能让跨洋 RTT 降到 100ms 以下。理解它,你才明白 CDN 存在的根本原因不是带宽,是距离

链路层:本地寻址与成帧

以太网帧结构:

┌────────┬────────┬──────┬─────────────────────┬─────┐
│目的 MAC │源 MAC  │类型  │       载荷           │ FCS │
│6 字节  │6 字节  │2字节 │  46–1500 字节        │4字节│
└────────┴────────┴──────┴─────────────────────┴─────┘

              0x0800 = IPv4
              0x86DD = IPv6
              0x0806 = ARP
              0x8100 = VLAN 标签

MAC 地址是 48 位、全球(理论上)唯一、烧在网卡里的硬件地址。它和 IP 的分工是:

MAC 地址IP 地址
范围只在本地链路有意义全球可路由
分配厂商烧死(可软改)网络管理员/DHCP 分配
类比门牌号完整邮寄地址
跨路由器每跳都会被改写全程不变(除 NAT)

最后一行是理解网络的关键:一个数据包穿过 10 个路由器,源/目的 IP 全程不变,但源/目的 MAC 被改写了 10 次。 每跳都是"交给下一个邻居",MAC 只表达"下一跳是谁"。

交换机做什么

交换机工作在链路层,核心能力是自学习 MAC 地址表

1. 收到一帧,记下「源 MAC ← 来自哪个端口」,写入 MAC 表
2. 查目的 MAC:
     · 表里有 → 只从对应端口转发出去(单播,其他端口听不到)
     · 表里没有 → 泛洪到所有其他端口(等对方回复时就学会了)
     · 是广播地址 ff:ff:ff:ff:ff:ff → 泛洪

由此产生几个概念:

  • 冲突域已经消失了。早期同轴电缆/集线器是共享介质,需要 CSMA/CD(发前先听、冲突了退避重发)。现代交换机是全双工点对点,CSMA/CD 在有线以太网上已经是历史。但在 WiFi 上仍有类似机制(CSMA/CA,发前先避让,因为无线无法边发边听)。
  • 广播域由 VLAN 划分。VLAN 用 802.1Q 标签(帧里插 4 字节)把一台物理交换机切成多个逻辑网段,广播不跨 VLAN。跨 VLAN 通信必须经过三层设备。
  • 环路是致命的。链路层没有 TTL,一个广播帧在环路里会无限复制,几秒内打死整个网络(广播风暴)。所以有 STP / RSTP / MSTP 协议自动阻断冗余链路,现代数据中心更多用 链路聚合(LACP)ECMP

ARP:把 IP 翻译成 MAC

现在有个衔接问题:应用层只知道 IP,但发帧需要 MAC。谁来翻译?

ARP(地址解析协议,只在 IPv4 里):

主机 A(192.168.1.10)要给 192.168.1.20 发包,但不知道它的 MAC。

A → 广播:  "谁是 192.168.1.20?请告诉 192.168.1.10"
            (目的 MAC = ff:ff:ff:ff:ff:ff,整个网段都收到)

B → 单播:  "我是 192.168.1.20,我的 MAC 是 aa:bb:cc:dd:ee:ff"

A 把这条记录存进 ARP 缓存(通常几分钟过期),后续直接用。
bash
ip neigh          # Linux 看 ARP 缓存
arp -a            # macOS / Windows

ARP 完全没有认证

任何人都可以宣称"我是 192.168.1.1"。这就是 ARP 欺骗/中间人攻击的原理——攻击者把自己伪装成网关,同一局域网内所有流量都经过他。

这也是为什么"公共 WiFi 不安全":不是 WiFi 加密不行,而是同一个局域网内的人可以骗到你的流量。防御手段只有一个真正有效:端到端加密(HTTPS、VPN)。链路层的信任问题只能在上层解决。

IPv6 用 NDP(邻居发现协议)取代 ARP,同样存在信任问题,需要 SEND 或交换机侧的 RA Guard 防护。

三、网络层:让数据穿过整个星球

链路层只能送到隔壁。要跨越几十跳、上万公里,需要网络层。

它只做两件事:全局寻址(IP 地址)和逐跳转发(路由)。而且它是"不可靠、无连接、尽力而为"的——IP 不保证送达、不保证顺序、不保证不重复。所有可靠性都推给上层。这是刻意的简化,让细腰足够细。

IPv4 地址与 CIDR

32 位地址,写成点分十进制:192.168.1.10

CIDR 表示法 192.168.1.0/24 的含义是:前 24 位是网络号,后 8 位是主机号。

192.168.1.0/24
  ├─ 网络地址:  192.168.1.0     (不能分配给主机)
  ├─ 可用地址:  192.168.1.1  –  192.168.1.254   (254 个)
  └─ 广播地址:  192.168.1.255   (不能分配给主机)

常见前缀长度对照:
  /8   →  16,777,214 个可用地址   (如 10.0.0.0/8)
  /16  →  65,534
  /24  →  254
  /30  →  2      (点对点链路常用)
  /31  →  2      (RFC 3021,点对点链路更省)
  /32  →  1      (单个主机,如回环、Anycast)

私有地址段(RFC 1918,不可在公网路由):

网段大小常见用途
10.0.0.0/81600 万企业内网、K8s Pod 网段
172.16.0.0/12100 万Docker 默认桥接(172.17.0.0/16)
192.168.0.0/166.5 万家用路由器
100.64.0.0/10400 万运营商级 NAT(CGNAT),RFC 6598
169.254.0.0/16链路本地(DHCP 失败时自动配置)
127.0.0.0/8回环

看到自己的公网 IP 是 100.x.x.x,说明你在运营商的大 NAT 后面——这时任何端口映射都做不了。

NAT:把 IPv4 的寿命续了二十年

IPv4 只有 43 亿个地址,早在 2011 年就分配完了。NAT(网络地址转换)让一个公网 IP 后面藏成千上万台设备:

内网                          NAT 网关                        公网
192.168.1.10:54321  ───┐
                       ├──→  改写为 203.0.113.5:10001  ──→  8.8.8.8:443
192.168.1.11:54321  ───┘      (同时在表里记一条映射)

映射表(NAPT,也叫 PAT):
┌──────────────────────┬──────────────────────┬──────────────┐
│ 内网 IP:端口          │ 外网 IP:端口          │ 目标          │
├──────────────────────┼──────────────────────┼──────────────┤
│ 192.168.1.10:54321   │ 203.0.113.5:10001    │ 8.8.8.8:443  │
│ 192.168.1.11:54321   │ 203.0.113.5:10002    │ 8.8.8.8:443  │
└──────────────────────┴──────────────────────┴──────────────┘

回包按外网端口反查表,改回内网地址。关键在于 NAT 必须维护状态——这打破了 IP "无状态转发"的设计,代价一路传导到今天:

  1. 外部无法主动连接内网设备。所以 P2P、视频通话、游戏联机都需要额外的穿透机制(第七节讲 STUN/TURN/ICE)。
  2. NAT 表项会过期。TCP 长连接空闲太久,映射被回收,连接"莫名断开"。这就是各种客户端要发心跳包的原因(通常 30–60 秒一次)。
  3. NAT 是有状态的单点。它必须记住所有连接,成为容量瓶颈和故障点。
  4. 它破坏了端到端原则。任何在载荷里写了 IP 的协议(FTP 主动模式、SIP)都需要 NAT 设备做协议感知的特殊处理(ALG)。

ICMP:网络层的诊断通道

ICMP 不传输用户数据,只传控制消息。两个最著名的用途:

ping —— 发 ICMP Echo Request,等 Echo Reply。测的是"通不通"和 RTT。

traceroute —— 巧妙利用了 TTL:

IP 头里有个 TTL(生存时间),每经过一个路由器减 1,减到 0 就丢弃并回一个
ICMP Time Exceeded 给源地址。traceroute 就利用这一点:

发 TTL=1 的包 → 第 1 跳路由器丢弃 + 回消息 → 知道第 1 跳是谁
发 TTL=2 的包 → 第 2 跳路由器丢弃 + 回消息 → 知道第 2 跳是谁
发 TTL=3 ... 直到到达目标(回 Port Unreachable 或 Echo Reply)

traceroute 结果里的 * * * 不一定代表故障

很多路由器出于安全或性能考虑不回 ICMP,或对 ICMP 限速,于是显示为超时。只要后面的跳还有回应,说明包是通的,这一跳只是"沉默"。

另外,中间跳显示高延迟也常常是假象——路由器处理 ICMP 是最低优先级的,转发正常流量时它可能很快。只有最后一跳的延迟才可信。 想看更准确的丢包分布,用 mtr(持续采样并统计)。

TTL 的另一个价值:它是防环的兜底。链路层没有 TTL 所以怕环路,网络层有 TTL,环路最多让包多跑 64 或 255 跳就被丢掉。

分片与 MTU:一个高频故障源

包比链路 MTU 大怎么办?IPv4 允许路由器分片,接收端重组。但分片很糟:任何一个片丢了整个包都要重传,且重组消耗内存。

所以现代做法是路径 MTU 发现(PMTUD)

1. 发包时设置 IP 头的 DF(Don't Fragment)标志位
2. 途中某台路由器发现包太大又不能分片
     → 丢弃,并回 ICMP "Fragmentation Needed",附上它的 MTU
3. 发送方收到后调小 MSS,重发

PMTUD 黑洞:为什么"能 ping 通但网页打不开"

PMTUD 完全依赖 ICMP。而很多防火墙一刀切封了所有 ICMP,于是:

  1. 大包被中途丢弃;
  2. ICMP 通知被防火墙拦掉,发送方永远收不到;
  3. 发送方一直重发同样大小的包,一直被丢。

症状极具误导性:小包(ping、TCP 握手、短请求)一切正常,大包(POST 大 body、下载文件、TLS 证书链稍长)全部卡死。 表现为"网页打开一半就转圈"。

排查方法:

bash
# 逐步加大包体,找出实际可通过的 MTU(-M do 相当于设置 DF)
ping -M do -s 1472 example.com     # 1472 + 28 = 1500
ping -M do -s 1400 example.com

临时缓解手段是在网关上钳制 MSS:iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

IPv6 更严格:路由器完全不允许分片,只有源端可以。所以 IPv6 网络里 ICMPv6 是必须放通的,封了它网络就不能正常工作。

路由:包怎么找到下一跳

每台路由器(包括你的电脑)都有一张路由表:

bash
$ ip route
default via 192.168.1.1 dev eth0             # 默认路由(0.0.0.0/0)
192.168.1.0/24 dev eth0 proto kernel scope link
10.244.0.0/16 via 10.0.0.5 dev eth1          # 去 K8s Pod 网段走这里

查表规则是最长前缀匹配:目的地址同时匹配多条时,选前缀最长(最具体)的那条。0.0.0.0/0 前缀长度为 0,所以它是最后的兜底——这就是"默认网关"。

路由表从哪来?三个来源:

来源说明
直连配了 IP 就自动有本网段的路由
静态人工配置,简单可控但不会自动收敛
动态路由协议路由器之间互相通告,自动算出最优路径

动态路由协议的分工:

                     ┌──────────────────────────────────────┐
                     │        AS 65001(某运营商)           │
                     │   内部用 OSPF / IS-IS 算最短路径      │
                     └────────────────┬─────────────────────┘
                                      │ BGP(AS 之间)
                     ┌────────────────┴─────────────────────┐
                     │        AS 65002(某云厂商)           │
                     │   内部用 OSPF / IS-IS                │
                     └──────────────────────────────────────┘
协议类型用在哪关键点
RIP距离向量已基本淘汰按跳数选路,上限 15 跳,收敛慢
OSPF链路状态企业内网、数据中心每台设备掌握全网拓扑,跑 Dijkstra
IS-IS链路状态大型运营商骨干与 OSPF 同类,更适合超大规模
BGP路径向量AS 之间,整个互联网的路由靠它选路依据是策略(商业关系)而非距离

BGP 是互联网真正的粘合剂,也是它最脆弱的地方:BGP 基于"相信邻居宣告的路由"。历史上多次出现某个 AS 误宣告(或恶意宣告)了不属于自己的网段,导致大片流量被吸走——BGP 劫持。2018 年就有一起把大量流量导向异常路径的事件。RPKI 正在推进路由源验证,但部署率仍不完整。

IPv6:不只是地址变长

128 位地址,写成 8 组 4 位十六进制:

2001:0db8:0000:0000:0000:8a2e:0370:7334
2001:db8::8a2e:370:7334          ← 简写:去前导零,最长的全零段用 :: 替代(只能一次)

::1                              ← 回环,等于 IPv4 的 127.0.0.1
fe80::/10                        ← 链路本地,每个接口自动有一个
2000::/3                         ← 全球单播地址(当前实际分配范围)
ff00::/8                         ← 组播(IPv6 没有广播,全用组播替代)

变化不止长度:

IPv4IPv6
头部20–60 字节,变长,有校验和40 字节固定,无校验和(交给上层)
地址配置DHCPSLAAC(无状态自动配置)或 DHCPv6
邻居发现ARPNDP(基于 ICMPv6)
分片路由器可分片只有源端可分片
NAT必需理论上不需要(地址够用)
组播/广播都有只有组播

SLAAC 是很优雅的设计:主机上线后发一个 Router Solicitation,路由器回 Router Advertisement 告知网络前缀,主机把前缀和自己的接口标识拼起来就是完整地址——不需要 DHCP 服务器

过渡技术里,实践上最主流的是双栈(同时跑 v4 和 v6,应用优先试 v6)。这带来一个副作用:浏览器采用 Happy Eyeballs(RFC 8305)策略同时发起 v4 和 v6 连接,取先成功的——因为很多网络的 IPv6 是"配了但不通",不这么做用户会体验到几秒卡顿。

DHCP:设备怎么自动拿到 IP

四步(DORA):

客户端 → 广播 DISCOVER    "有 DHCP 服务器吗?"
服务器 → 回   OFFER       "给你 192.168.1.100,网关 .1,DNS 是 ..."
客户端 → 广播 REQUEST     "我要这个"(广播是为了告知其他服务器它已选定)
服务器 → 回   ACK         "确认,租期 24 小时"

DHCP 一次性下发的不只是 IP,还有子网掩码、默认网关、DNS 服务器、NTP 服务器——这就是为什么连上 WiFi 什么都不用配就能上网。也因此,恶意的 DHCP 服务器可以把 DNS 指向自己(DHCP 欺骗)。

四、传输层:从"主机到主机"到"进程到进程"

IP 只能把包送到某台主机。但一台主机上跑着几十个程序,给谁?这是传输层的第一个任务。

端口与四元组

一条 TCP 连接由四元组唯一标识:

  源 IP        源端口     目的 IP        目的端口
  1.2.3.4  :  54321  →  93.184.216.34 :  443

所以同一台服务器的 443 端口能同时服务几万个客户端——四元组不同就是不同连接。也因此,单机的连接数上限不是 65535:65535 是"某个源 IP 到某个目的 IP:端口"的可用源端口数,不是总连接数。

常见端口速查:

端口协议端口协议
20/21FTP443HTTPS / HTTP/3(UDP)
22SSH587SMTP(提交,带认证)
25SMTP993IMAPS
53DNS(UDP+TCP)3306MySQL
67/68DHCP5432PostgreSQL
80HTTP6379Redis
123NTP8080HTTP 备用

UDP:几乎什么都不做

┌──────────┬──────────┬────────┬────────┐
│ 源端口   │ 目的端口 │ 长度   │ 校验和 │  ← 一共只有 8 字节
└──────────┴──────────┴────────┴────────┘

UDP 只提供两件事:端口复用和**(可选的)校验和**。不保证送达、不保证顺序、不做流控、不做拥塞控制。

这听起来像缺陷,实际是特性。UDP 的价值是它不替你做决定

  • DNS:一次请求一次响应,用 TCP 建连的成本远超重发一次的成本;
  • 实时音视频:迟到的数据包没有价值,重传毫无意义,宁可丢;
  • QUIC / HTTP/3:需要在用户态自己实现更好的传输逻辑,UDP 提供了最干净的画布;
  • 游戏状态同步:只关心最新状态,旧包直接丢。

TCP:把不可靠变可靠

 0                   1                   2                   3
┌───────────────────────────────┬───────────────────────────────┐
│           源端口              │           目的端口             │
├───────────────────────────────┴───────────────────────────────┤
│                            序号 (Seq)                         │
├───────────────────────────────────────────────────────────────┤
│                         确认号 (Ack)                          │
├────┬──────┬───────────────────┬───────────────────────────────┤
│头长│ 标志 │ URG ACK PSH RST SYN FIN │        窗口大小          │
├────┴──────┴───────────────────┴───────────────────────────────┤
│           校验和              │           紧急指针            │
├───────────────────────────────┴───────────────────────────────┤
│                      选项(MSS、SACK、窗口扩大、时间戳…)      │
└───────────────────────────────────────────────────────────────┘

TCP 用这些字段实现了五件事:连接管理、可靠传输、有序交付、流量控制、拥塞控制

三次握手

客户端                                         服务端
  │                                              │
  │──── SYN, seq=x ─────────────────────────────→│  "我要连,我的初始序号是 x"
  │                                              │  (连接进入半连接队列)
  │←─── SYN+ACK, seq=y, ack=x+1 ─────────────────│  "同意,我的序号是 y,
  │                                              │   我确认收到了你的 x"
  │──── ACK, ack=y+1 ───────────────────────────→│  "我确认收到了你的 y"
  │                                              │  (进入全连接队列,等 accept)
  │═══════════ 连接建立,可以发数据 ══════════════│

为什么是三次不是两次? 因为双方都需要确认"对方能收到我的包",且都需要同步初始序号。两次的话服务端无法确认自己的 SYN+ACK 被收到,可能为一个早已失效的重复 SYN 建立连接、白占资源。

为什么不是四次? 服务端的 ACK 和 SYN 可以合并成一个包,没必要分开。

半连接队列与 SYN Flood

握手期间,服务端会为"收到 SYN 但还没收到最后 ACK"的连接分配资源,放入半连接队列(SYN queue)。

攻击者疯狂发 SYN 但从不回 ACK,就能把这个队列打满,正常用户连不上——这就是 SYN Flood

防御手段 SYN Cookie:服务端不再保存半连接状态,而是把状态编码进自己的初始序号(用源/目的地址、端口、时间戳算个哈希)。客户端回 ACK 时带回 ack=y+1,服务端反推验证即可,不占内存

bash
sysctl -w net.ipv4.tcp_syncookies=1     # 队列满时自动启用
sysctl -w net.ipv4.tcp_max_syn_backlog=8192

两个队列容易混淆:半连接队列(SYN queue,握手中)由 tcp_max_syn_backlog 控制;全连接队列(accept queue,已完成握手等应用 accept)由 listen() 的 backlog 和 net.core.somaxconn 的较小值控制。后者溢出会导致连接被静默丢弃或 RST,症状是"服务器负载不高但客户端连接超时"。

四次挥手与 TIME_WAIT

主动关闭方                                   被动关闭方
  │──── FIN ────────────────────────────────────→│  "我没数据要发了"
  │←─── ACK ─────────────────────────────────────│  "知道了"
  │                                              │  (此时它还可以继续发数据!
  │←─── FIN ─────────────────────────────────────│    这叫半关闭。发完再 FIN)
  │──── ACK ────────────────────────────────────→│
  │                                              │
  │  进入 TIME_WAIT,等 2×MSL(Linux 固定 60s)   │
  │  然后才真正释放                                │

为什么四次? 因为 TCP 是全双工,两个方向要分别关闭。收到对方 FIN 只说明"它不发了",自己可能还有数据要发。

为什么要 TIME_WAIT 等 2MSL? 两个理由:

  1. 保证最后那个 ACK 能到。如果 ACK 丢了,对方会重发 FIN,此时还在 TIME_WAIT 的一方能再回一次 ACK。如果直接关了,对方会收到 RST,认为异常关闭。
  2. 让本次连接的迷途数据包在网络中彻底消失。否则四元组被立刻复用时,上一条连接的延迟包可能被误认为属于新连接。

TIME_WAIT 的实际困扰:高并发短连接的客户端(比如 Nginx 回源)会积累大量 TIME_WAIT,耗尽本地端口。缓解手段:

bash
sysctl -w net.ipv4.tcp_tw_reuse=1        # 允许复用 TIME_WAIT 端口(作为客户端时,安全)
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# 注意:tcp_tw_recycle 在 NAT 环境下会导致连接失败,Linux 4.12 起已被移除,别再用

根本解法不是调参数,是用长连接。 TIME_WAIT 多说明你在频繁建连接。

可靠传输:序号、确认与重传

每个字节都有序号。接收方用 ACK 告知"我已连续收到到 N 为止的所有字节"。

发送方的两种重传触发:

① 超时重传(RTO)
   基于 RTT 动态估算超时时间(RFC 6298)。超时后不仅重传,还要
   把 RTO 翻倍(指数退避)——因为超时通常意味着网络真的拥塞了。

② 快速重传
   收到 3 个重复 ACK("我还在等 N,我还在等 N,我还在等 N")
   → 判断 N 号段丢了,立刻重传,不等超时。

SACK(选择性确认,RFC 2018)
   没有 SACK 时,接收方只能说"我等 N",发送方不知道 N 之后的收到了没有,
   可能重传一大片。有了 SACK,接收方能说"我等 N,但我已经收到了 N+100 到 N+500",
   发送方只补那一个洞。现代实现默认开启。

流量控制 vs 拥塞控制

这两个经常被混为一谈,但解决的是不同问题:

流量控制拥塞控制
防止接收方被压垮网络被压垮
依据接收方通告的窗口(rwnd发送方根据丢包/延迟推测(cwnd
信息来源明确告知(TCP 头里的窗口字段)靠猜——网络不会告诉你它堵了
机制滑动窗口、零窗口探测慢启动、拥塞避免、快恢复

实际发送量 = min(rwnd, cwnd)

拥塞控制的经典四阶段(Reno):

cwnd
 │                                    ╱╲            ← 丢包,减半
 │                            ╱╲    ╱   ╲
 │                    ╱╲    ╱   ╲ ╱     ╲
 │            ╱╲    ╱   ╲ ╱      ╳       ╲  ← 拥塞避免:每 RTT +1(线性)
 │        ╱                                  
 │     ╱  ← 慢启动:每 RTT 翻倍(指数)
 │  ╱
 └────────────────────────────────────────────── 时间
    ↑                    ↑
  开始              到达 ssthresh,转为线性增长

"慢启动"这名字有误导性——它起点低但增长极快(指数)。真正的问题是起点:初始 cwnd 通常是 10 个 MSS(约 14KB),所以一条新连接的前几个 RTT 根本用不满带宽。这解释了一个重要现象:

为什么短连接的性能和带宽几乎无关

假设 RTT 100ms,初始 cwnd 10 个包。要发一个 1MB 的文件:

  • 第 1 个 RTT:发 10 包(约 14KB)
  • 第 2 个 RTT:发 20 包
  • 第 3 个 RTT:发 40 包……

需要约 6–7 个 RTT 才能把 1MB 发完,也就是 600–700ms,而这期间的平均吞吐远低于链路带宽。升级 100M 到 1000M 宽带对这个场景几乎没有改善。

结论:对短连接为主的 Web 应用,减少 RTT 数量(复用连接、合并请求、CDN 就近接入)比增加带宽有效得多。 这也是 HTTP/2 多路复用和 TLS 1.3 减少握手 RTT 的价值所在。

主流拥塞控制算法:

算法判断依据特点
Reno / NewReno丢包经典教科书算法
CUBIC丢包Linux 默认,用三次函数增长,高带宽长距离下比 Reno 好得多
BBR带宽 × 延迟建模Google 2016 提出,不以丢包为信号,在有随机丢包的链路(跨国、无线)上优势显著

BBR 的思路转变很关键:丢包不等于拥塞。无线链路和长距离跨国链路存在与拥塞无关的随机丢包,基于丢包的算法会误判、疯狂降速。BBR 直接测量瓶颈带宽和最小 RTT 来决定发送速率。

bash
sysctl net.ipv4.tcp_available_congestion_control
sysctl -w net.ipv4.tcp_congestion_control=bbr    # 需要内核 4.9+

两个小机制,一对经典冲突

  • Nagle 算法:小数据包先攒着,等前一个包的 ACK 回来再一起发,减少小包数量。
  • 延迟 ACK:收到数据不立刻回 ACK,等一下(通常最多 40ms),看能不能捎带在响应数据里一起发。

两者单独都合理,碰上就是灾难:发送方在等 ACK 才发下一个小包,接收方在等数据才回 ACK,双方互等,直到延迟 ACK 超时。表现为固定 40ms 的诡异延迟

解法就是 TCP_NODELAY(关掉 Nagle)。这就是为什么几乎所有 RPC 框架、数据库客户端、Nginx 都默认设置这个选项。

QUIC:在 UDP 上重造传输层

TCP 有两个改不动的问题:

  1. 它在内核里。改进要等操作系统更新,全网普及要十年。
  2. 握手 RTT 太多。TCP 三次握手 1 个 RTT,TLS 1.2 再要 2 个,一共 3 个 RTT 才能发第一个字节。
  3. TCP 层的队头阻塞。HTTP/2 在一条 TCP 上跑多个 stream,任何一个包丢了,整条连接的所有 stream 都要等它重传——因为 TCP 必须按序交付。

QUIC(RFC 9000,2021)的做法是:把传输层搬到用户态,跑在 UDP 上。

        传统                                QUIC
┌──────────────────┐              ┌──────────────────┐
│      HTTP/2      │              │      HTTP/3      │
├──────────────────┤              ├──────────────────┤
│      TLS 1.2/3   │              │  QUIC            │
├──────────────────┤              │  (流控、可靠性、 │  ← 用户态
│      TCP         │  ← 内核态     │    拥塞控制、    │
├──────────────────┤              │    内置 TLS 1.3)│
│      IP          │              ├──────────────────┤
└──────────────────┘              │      UDP         │  ← 内核态只剩这个
                                  ├──────────────────┤
                                  │      IP          │
                                  └──────────────────┘

带来的四个改进:

能力说明
1-RTT / 0-RTT 建连传输握手与 TLS 握手合并;复用时可 0-RTT
无队头阻塞每个 stream 独立重传,一个 stream 丢包不影响其他
连接迁移用 Connection ID 而非四元组标识连接,手机从 WiFi 切 4G 不断连
可快速演进在应用层,浏览器和服务器升级即可,不等内核

代价:CPU 开销比 TCP 高(用户态处理 + 每包加密);部分企业防火墙封 UDP 443 导致回落;实现复杂度大得多。

五、应用层:真正做事的那一层

下面四层是管道,应用层才是内容。

DNS:互联网的电话簿

没有 DNS,你得记住 93.184.216.34。DNS 把域名翻译成 IP,而且它本身是一个分布式的层级数据库

                     . (根,全球 13 组根服务器地址,用 Anycast 部署了上千个实例)

      ┌──────────────┼──────────────┐
     com.           org.          cn.              ← 顶级域(TLD)
      │                             │
  example.com.              ┌───────┴───────┐
      │                   com.cn.        edu.cn.   ← 二级
  ┌───┴────┐
 www   api                                         ← 主机记录

一次完整解析(递归 + 迭代):

你的电脑 ──① 我要 www.example.com──→ 本地 DNS(运营商 / 8.8.8.8 / 114.114.114.114)

                                        │ 缓存里有?有就直接返回(大部分请求止于此)
                                        │ 没有 → 开始迭代查询:

                                        ├─② 问根:".com 的服务器是谁?"
                                        │   ← 根答:"去问 a.gtld-servers.net"

                                        ├─③ 问 .com:" example.com 的服务器是谁?"
                                        │   ← 答:"去问 ns1.example.com"

                                        └─④ 问 ns1.example.com:"www 的 A 记录?"
                                            ← 答:"93.184.216.34,TTL 300"

你的电脑 ←──────── 返回结果 ─────────────┘

注意分工:你的电脑只发一次请求(递归查询),本地 DNS 替你跑完整个迭代过程。这就是为什么换个 DNS 服务器会影响解析速度和结果。

常见记录类型:

类型作用示例
A域名 → IPv4example.com → 93.184.216.34
AAAA域名 → IPv6example.com → 2606:2800:220:1::
CNAME域名 → 另一个域名www → example.com根域名不能用 CNAME
MX邮件服务器带优先级
TXT任意文本SPF、DKIM、域名所有权验证
NS该域由哪些 DNS 服务器负责委派用
SOA区域的权威信息序列号、刷新间隔
PTRIP → 域名(反向解析)邮件服务器信誉检查用
CAA谁可以给这个域名签证书防止误签发
SRV服务发现(协议+端口)SIP、XMPP、K8s 内部

DNS 的三个坑

1. TTL 决定了变更的生效速度。 改了解析记录,全球生效时间取决于 TTL(以及各级缓存是否守规矩——有些运营商 DNS 会无视 TTL 缓存更久)。计划迁移前先把 TTL 调小到 60 秒,等旧 TTL 过期后再改记录。

2. 根域名不能用 CNAME。 example.com(裸域)必须是 A/AAAA 记录,因为它同时要有 SOA 和 NS 记录,而 CNAME 不能与其他记录共存。想让裸域指向 CDN,需要厂商提供的 ALIAS / ANAME / CNAME Flattening(非标准扩展)。

3. DNS 默认是明文的。 你查了什么域名,路径上所有人都看得见,也可以篡改(DNS 劫持、投毒)。这就是 DoH(DNS over HTTPS,443)DoT(DNS over TLS,853) 出现的原因。代价是运维可见性降低——企业内网的域名管控会失效。

HTTP:从文本协议到二进制多路复用

演进史本身就是网络优化史:

版本年份关键变化解决了什么
0.91991只有 GET,只能返回 HTML
1.01996头部、状态码、Content-Type能传任何类型的文件
1.11997持久连接、Host 头、分块编码、缓存控制不用每个请求建一次连接;一个 IP 能放多个站点
22015二进制分帧、多路复用、HPACK 头压缩、服务端推送HTTP 层的队头阻塞、头部冗余
32022基于 QUIC/UDPTCP 层的队头阻塞、握手 RTT、连接迁移

几个关键点:

HTTP/1.1 的 Host 头是虚拟主机的基础。 有了它,一个 IP 上才能通过域名区分多个网站——这是共享主机和现代反向代理的前提。

HTTP/1.1 的流水线(pipelining)实际上失败了。 它允许不等响应就发下一个请求,但要求响应必须按序返回,一个慢请求会堵住后面所有的——应用层队头阻塞。浏览器最终都默认禁用了它,改用"开 6 条 TCP 连接"这种土办法。

HTTP/2 的多路复用是真正的解法:一条连接上跑多个独立的 stream,各自有 ID,可以交错传输。于是浏览器只需要 1 条连接。副作用是 HTTP/1.1 时代的优化技巧(域名分片、雪碧图、内联小资源)统统变成负优化

HTTP/2 的服务端推送已被主流浏览器移除——它想解决的问题(提前推送 CSS/JS)在实践中命中率太低、经常推送客户端已缓存的资源,被 103 Early Hints 取代。

状态码的分类逻辑:

1xx  信息性     100 Continue(大 body 前先问一下)
                101 Switching Protocols(WebSocket 升级)
                103 Early Hints(提前告知要预加载什么)

2xx  成功       200 OK    201 Created    204 No Content
                206 Partial Content(断点续传 / 视频拖动进度条)

3xx  重定向     301 永久(会被浏览器和搜索引擎缓存,慎用)
                302 临时    304 Not Modified(命中协商缓存,不带 body)
                307/308 保持请求方法不变的临时/永久重定向

4xx  客户端错   400 语法错  401 未认证(Unauthorized 实为未认证)
                403 已认证但无权限    404 不存在
                405 方法不允许        409 冲突
                413 body 太大         422 语义错误
                429 请求太频繁(限流)

5xx  服务端错   500 内部错误    502 网关拿到非法响应
                503 暂时不可用(过载/维护)    504 网关超时

401 vs 403 是最常被写错的一对:401 = 你是谁我不知道(去登录);403 = 我知道你是谁,但你不能干这个。

缓存这块值得单独记:

http
Cache-Control: max-age=31536000, immutable     # 强缓存:一年内根本不问服务器
Cache-Control: no-cache                        # 每次都要问(不是"不缓存"!)
Cache-Control: no-store                        # 真正的"不缓存"
Cache-Control: private / public                 # 能否被 CDN 等中间节点缓存

ETag: "abc123"          →  下次带 If-None-Match: "abc123"    → 命中则 304
Last-Modified: <date>   →  下次带 If-Modified-Since: <date>  → 命中则 304

no-cache 的字面意思骗了很多人:它表示"可以存,但用之前必须向服务器确认"(协商缓存)。真正禁止存储的是 no-store

HTTPS 与 TLS

HTTPS = HTTP over TLS。它解决三个问题:加密(别人看不见)、完整性(别人改不了)、身份认证(对方真是那个网站)。

第三个最重要也最容易被忽略:加密不难,难的是知道你在跟谁加密

TLS 1.3 握手(1-RTT):

客户端                                              服务端
  │                                                   │
  │── ClientHello ───────────────────────────────────→│
  │    · 支持的密码套件                                │
  │    · 密钥交换参数(直接带上 DH 公钥,这是 1.3 提速的关键)│
  │    · SNI:我要访问 example.com  ← 明文!           │
  │    · ALPN:我支持 h2, http/1.1                    │
  │                                                   │
  │←─ ServerHello + 证书 + Finished ──────────────────│
  │    · 选定的套件、服务端 DH 公钥                     │
  │    · 证书链(用于验证身份)                         │
  │                                                   │
  │── Finished ──────────────────────────────────────→│
  │═════════════ 加密通信开始 ═════════════════════════│

对比 TLS 1.2 需要 2 个 RTT,1.3 砍到 1 个(复用时还能 0-RTT)。同时 1.3 删掉了所有已知不安全的算法(RC4、MD5、SHA-1、静态 RSA 密钥交换、压缩),用"减少选择"来提升安全性——这是很值得学的设计思路。

两个附带的重要机制:

  • SNI(Server Name Indication):客户端在握手时明文告知要访问的域名,服务器才知道该发哪张证书。这是一个 IP 托管多个 HTTPS 站点的前提,也是明文泄露访问目标的地方(ECH/Encrypted Client Hello 正在解决,部署尚不普及)。
  • ALPN(Application-Layer Protocol Negotiation):在握手时就协商好用 HTTP/1.1 还是 HTTP/2,省掉一次额外协商。

证书信任链:

根 CA 证书(预置在操作系统/浏览器里,自签名)
    │ 签发
中间 CA 证书(实际干活的,可吊销、可隔离风险)
    │ 签发
你的网站证书(example.com)

浏览器验证:
  ① 证书链能否一路验到本机信任的根?
  ② 域名对得上吗(CN / SAN)?
  ③ 在有效期内吗?
  ④ 被吊销了吗(CRL / OCSP / OCSP Stapling)?
  ⑤ 签名算法够强吗?

为什么"自签名证书"会报警告

自签名证书的加密强度和 CA 签发的完全一样。区别只在于:没有第三方证明这个公钥属于这个域名

也就是说,浏览器报警报的不是"不安全",而是"我无法确认对方是谁"——而这恰恰是中间人攻击的入口。理解这一点,你就明白企业内网让员工装根证书意味着什么:装了之后,公司可以合法地解密你所有的 HTTPS 流量。

其他值得知道的应用层协议

协议层级/端口用途关键点
WebSocket80/443全双工长连接用 HTTP Upgrade 握手,之后变成独立的帧协议
gRPC通常 443RPC跑在 HTTP/2 上,Protobuf 编码,支持流式
SSE80/443服务端单向推送纯 HTTP,比 WebSocket 简单,自动重连(大模型流式输出常用)
MQTT1883/8883物联网消息极轻量,为不稳定网络和低功耗设备设计
SSH22远程登录、隧道端口转发能力极强,常被当作临时 VPN
SMTP / IMAP / POP325,587 / 993 / 995邮件发送 / 收取发信靠 SMTP,收信靠 IMAP;SPF+DKIM+DMARC 防伪造
FTP20/21文件传输控制与数据分离,主动/被动模式与 NAT 冲突严重,已被 SFTP 取代
NTP123时间同步分层(stratum)结构;时间不准会导致 TLS 证书校验失败
RTP / RTCP动态 UDP实时音视频跑在 UDP 上,配合 WebRTC 使用
WebRTC动态 UDP浏览器 P2P 音视频一整套组合:ICE + STUN/TURN + DTLS + SRTP

六、串起来:一次网页加载的完整旅程

前五节是零件,这一节是装配图。假设你在浏览器输入 https://example.com 然后回车。

════════ 阶段 0:连接都还没开始 ════════

  ① 浏览器检查自己的 HSTS 列表
     └─ 如果 example.com 在列表里,输入 http:// 也会被强制改成 https://
        (这一步在任何网络请求之前,纯本地)

  ② 检查各级缓存:内存缓存 → 磁盘缓存 → Service Worker
     └─ 命中强缓存(max-age 未过期)→ 直接渲染,全程零网络请求

════════ 阶段 1:DNS 解析 ════════

  ③ 浏览器 DNS 缓存 → 操作系统缓存 → hosts 文件 → 本地 DNS 服务器
     └─ 全都没有 → 本地 DNS 迭代查询:根 → .com → example.com 的权威 NS
     └─ 拿到 A 记录:93.184.216.34(可能有多个,双栈时还有 AAAA)
     └─ 耗时:命中缓存 0ms,完整查询 20–200ms

════════ 阶段 2:找到出门的路 ════════

  ④ 内核查路由表:93.184.216.34 不在本网段 → 走默认网关 192.168.1.1

  ⑤ ARP:不知道 192.168.1.1 的 MAC → 广播询问 → 拿到网关 MAC
     └─ 现在才能封装出完整的以太网帧

════════ 阶段 3:TCP 建连(约 1 个 RTT)════════

  ⑥ SYN → SYN+ACK → ACK
     └─ 这个包的旅程:
          你的网卡 → 家用路由器(NAT:改写源 IP 和端口)
          → 运营商接入网 → 城域网 → 骨干网
          → 可能跨越几个 AS(BGP 决定走哪条)
          → 目标机房的边界路由器 → 负载均衡 → 服务器
        每跳都在改 MAC、减 TTL,但源/目的 IP 只在 NAT 处变过

════════ 阶段 4:TLS 握手(TLS 1.3 约 1 个 RTT)════════

  ⑦ ClientHello(带 SNI + ALPN)→ ServerHello + 证书 → Finished
  ⑧ 浏览器验证证书链、域名、有效期、吊销状态
  ⑨ ALPN 协商结果决定接下来说 HTTP/2 还是 HTTP/1.1

════════ 阶段 5:HTTP 请求与响应(约 1 个 RTT)════════

  ⑩ 发出 GET /:Host、User-Agent、Accept、Cookie、If-None-Match …
  ⑪ 服务端侧的旅程(这是另一整篇文章):
        CDN 边缘节点(命中就到此为止)
        → 回源 → 云 LB(四层)
        → Nginx(七层:TLS 终结、路由、缓存、限流)
        → 应用服务器 → 数据库 / 缓存 / 其他微服务
  ⑫ 响应回来:200 + HTML(很可能是 gzip/brotli 压缩的、chunked 分块的)

════════ 阶段 6:解析与二次请求 ════════

  ⑬ 浏览器边收边解析 HTML,遇到 <link> <script> <img> 就发起新请求
     └─ HTTP/2:全部在同一条连接上多路复用
     └─ HTTP/1.1:最多 6 条并发连接,其余排队
     └─ 跨域资源要重新走 ③–⑫ 全流程(DNS + TCP + TLS)
        ← 这就是「减少域名数量」和 dns-prefetch / preconnect 的价值

  ⑭ 构建 DOM + CSSOM → 布局 → 绘制 → 首屏出现

════════ 阶段 7:收尾 ════════

  ⑮ 连接不立刻关闭(keep-alive),留着复用
  ⑯ 真正关闭时四次挥手,主动关闭方进入 TIME_WAIT 60 秒

从这条链路能读出的优化优先级

把上面的耗时按"能省几个 RTT"排序,优化的性价比就一目了然了:

手段省掉什么收益
CDN / 就近接入每个 RTT 都变短★★★★★(乘数效应,所有阶段受益)
强缓存 / immutable整条链路★★★★★(0 个 RTT)
连接复用(keep-alive / h2)TCP + TLS 握手,2 个 RTT★★★★
TLS 1.3 / 会话复用1 个 RTT★★★
减少域名数量、preconnect每个新域名的 DNS+TCP+TLS★★★
压缩(brotli/gzip)传输字节数★★★
升级带宽只对大文件下载有效★(对短请求几乎无效,见第四节慢启动)

注意最后一行。带宽是最容易买、最没用的优化——只要你的问题是"很多个小请求",那瓶颈在 RTT 数量,不在带宽。

七、现实世界的网络:云、CDN 与穿透

教科书讲到第六节就结束了。但真实生产环境还有一层。

CDN 与 Anycast:把内容搬到用户身边

CDN 的本质是用地理分布对抗光速。既然跨洋 RTT 无法低于 100ms,那就在用户所在的城市放一份内容。

它怎么让用户"自动"访问最近的节点?两种技术:

① DNS 调度(最常见)
   用户查 www.example.com
     → CNAME 到 example.com.cdn-provider.net
     → CDN 的 DNS 根据「请求来自哪个 DNS 服务器的 IP」判断地理位置
     → 返回最近节点的 IP
   缺点:判断依据是「本地 DNS 的位置」而非用户位置。用户用了 8.8.8.8
        就可能被调度到很远的节点(EDNS Client Subnet 扩展缓解了这个问题)

② Anycast(更优雅)
   全球几十个节点宣告【同一个 IP】
     → BGP 让每个用户的流量自然流向「路由意义上最近」的节点
   优点:无需 DNS 参与,故障时路由自动收敛到其他节点
   典型用户:DNS 根服务器、1.1.1.1、8.8.8.8、大型 CDN

Anycast 的妙处在于它把"选最近的服务器"这个问题外包给了 BGP——本来就在做选路的系统。

负载均衡:四层与七层

四层(L4)七层(L7)
看到什么IP + 端口完整的 HTTP 请求
能做什么按连接分发按 URL / Header / Cookie 路由,改写,缓存
性能极高(可做到线速)较低(要解析、可能要解 TLS)
典型实现LVS、F5、云厂商 NLB、DPDK 方案Nginx、Envoy、HAProxy、云厂商 ALB
TLS透传通常在这里终结

生产环境几乎总是两层叠加

用户 → [Anycast/DNS] → 四层 LB(抗量、分发)→ 七层 LB(路由、TLS、限流)→ 应用

四层 LB 有个重要变体 DSR(直接服务器返回):请求经过 LB,但响应绕过 LB 直接回客户端。因为绝大多数场景响应比请求大得多(下载、页面),绕过 LB 能让它的带宽压力降低一个数量级。

NAT 穿透:P2P 怎么绕过 NAT

回到第三节留下的问题:双方都在 NAT 后面,怎么直连?

① STUN(Session Traversal Utilities for NAT)
   客户端问一台公网服务器:"你看到的我的地址和端口是什么?"
   → 得知自己的公网映射(IP:Port)
   → 把它通过信令通道(比如 WebSocket)告诉对方
   → 双方同时向对方的公网映射发包
   → 这个「同时发」会在各自的 NAT 上打出临时映射(打洞),之后就能直连

   成功率:约 70–80%,取决于 NAT 类型

② TURN(Traversal Using Relays around NAT)
   打洞失败时(对称 NAT、CGNAT、严格企业防火墙),
   老实用一台公网服务器中转全部流量。
   一定成功,但服务器要承担全部带宽成本。

③ ICE(Interactive Connectivity Establishment)
   把所有可能的路径(本地地址、STUN 得到的公网地址、TURN 中继地址)
   都收集起来叫「候选」,全部并发尝试,选最先连通且最优的一条。
   → WebRTC 用的就是这套

这套机制是所有视频通话、游戏联机、P2P 传输的基础设施。它存在的唯一原因就是 IPv4 地址不够、被迫使用 NAT——如果全世界都跑 IPv6,理论上不需要这一整套东西。

隧道与 VPN

隧道的本质是把一种协议的包装进另一种协议(封装):

技术封装方式用途
IPsecIP in IP + 加密企业站点互联,标准但配置复杂
WireGuardUDP + 现代密码学代码量极小(几千行),性能好,配置简单
OpenVPNTLS over UDP/TCP兼容性最好,性能一般
GREIP in IP,不加密纯组网,常配合 IPsec 使用
VXLAN二层帧 in UDP:4789云上虚拟网络,24 位 VNI 支持 1600 万个隔离网络
SSH 隧道TCP over SSH临时打通一个端口,运维常用
SOCKS5应用层代理通用代理协议,不做封装只做转发

隧道一定会缩小可用 MTU

每加一层封装就多一层头部:

原始:                            [IP 20][TCP 20][数据 1460]  = 1500
WireGuard 隧道:      [IP 20][UDP 8][WG 32][IP 20][TCP 20][数据 1400] = 1500
                      └────── 60 字节被隧道吃掉 ──────┘

所以隧道内的有效 MSS 必须减小。这是"连了 VPN 之后部分网站打不开、SSH 卡在登录横幅"的头号原因——本质是第三节讲的 PMTUD 黑洞。

标准解法:在隧道接口上显式设小 MTU(WireGuard 常用 1420),或在网关做 MSS 钳制。

数据中心与云网络

现代数据中心早就不是"一堆交换机串起来"了:

        ┌───────┐   ┌───────┐   ┌───────┐   ┌───────┐
        │Spine 1│   │Spine 2│   │Spine 3│   │Spine 4│     ← 脊
        └─┬─┬─┬─┘   └─┬─┬─┬─┘   └─┬─┬─┬─┘   └─┬─┬─┬─┘
          │ │ │       │ │ │       │ │ │       │ │ │
          └─┼─┼───────┼─┼─┼───────┼─┼─┼───────┼─┼─┘   全互联
      ┌─────┴─┴─┐ ┌───┴─┴───┐ ┌───┴─┴───┐
      │ Leaf 1  │ │ Leaf 2  │ │ Leaf 3  │                ← 叶
      └────┬────┘ └────┬────┘ └────┬────┘
        服务器机架    服务器机架   服务器机架

这叫 Leaf-Spine(Clos 架构)。特点是任意两台服务器之间的跳数完全相同(都是 leaf → spine → leaf),且有多条等价路径(用 ECMP 做多路径负载均衡)。这解决了传统三层树形架构的两个问题:东西向流量(服务器之间)要绕到核心层,以及带宽收敛比过高。

云上的 VPC 则是完全的软件定义网络:

你看到的:一个私有网段 10.0.0.0/16,划了几个子网,配了路由表和安全组

实际发生的:
  你的虚拟机发出的每个包,被宿主机的虚拟交换机拦下,
  封装进 VXLAN(外层是数据中心的物理 IP),
  在物理网络里传输,
  到目标宿主机后解封装,交给目标虚拟机。

  所以「你的 10.0.0.5」和物理网络毫无关系——
  它只是一个写在 VXLAN 内层头部的标识。

这就是 Overlay 网络:在物理网络(Underlay)之上叠一层虚拟网络。它让"给每个租户一套完全隔离的私有网络"成为可能,也让容器网络(Calico、Flannel、Cilium)得以实现。

容器网络是同一套思路的延伸:每个 Pod 一个 IP,跨节点通信靠 overlay(VXLAN/IPIP)或路由(BGP)。K8s 的 Service 则是在这之上再加一层虚拟 IP + 负载均衡(早期用 iptables,现在多用 IPVS 或 eBPF)。

至于 RDMA / RoCE:在 AI 训练和高性能存储场景,连内核网络栈都嫌慢,直接让网卡访问对端内存,绕过 CPU 和内核——延迟从几十微秒降到个位数微秒。这是 GPU 集群互联的标准配置。

八、性能:三个指标与三种队头阻塞

带宽、延迟、吞吐不是一回事

指标定义类比能不能买到
带宽单位时间的最大传输量管道有多粗能,花钱就行
延迟(RTT)一个往返要多久管道有多长不能,受光速限制
吞吐实际达到的传输速率实际流了多少水是结果,不是配置项

三者的关系由 带宽延迟积(BDP) 联系起来:

BDP = 带宽 × RTT

例:1 Gbps 链路,RTT 100ms
    BDP = 1,000,000,000 bit/s × 0.1 s / 8 = 12.5 MB

含义:要把这条链路填满,必须允许「12.5 MB 的数据在途中还没被确认」。
      如果 TCP 窗口小于 12.5 MB,无论带宽多大,吞吐都上不去。

这解释了一个常见困惑:跨国专线明明是千兆,单条 TCP 连接只能跑几十兆。 因为窗口不够大(需要开启窗口扩大选项、调大 net.ipv4.tcp_rmem),或者拥塞控制算法因为随机丢包一直在降速(换 BBR)。

也解释了为什么大文件跨国传输要开多线程:多条连接 = 多个窗口叠加

三种队头阻塞

"队头阻塞"(HOL blocking)这个词在不同层含义不同,混淆会导致完全错误的优化判断:

① 应用层 HOL —— HTTP/1.1 的流水线
   一条连接上,请求 1 的响应必须先返回,请求 2、3 才能返回
   ┌──── 慢请求 1 ────┐[2][3]
   解法:HTTP/2 多路复用 ✓

② 传输层 HOL —— HTTP/2 over TCP
   多个 stream 共享一条 TCP。TCP 必须按序交付,
   所以任何一个包丢了,所有 stream 都要等它重传
   stream A: [██][██][XX 丢了][██]
   stream B: [██][██][ 等待… ][ 等待… ]  ← B 明明没丢包,也被堵住
   解法:QUIC / HTTP/3 ✓(每个 stream 独立重传)

③ 队列 HOL —— 网络设备的缓冲区
   路由器缓冲区排满了大包,小的延迟敏感包排在后面干等
   解法:AQM(主动队列管理)、FQ-CoDel、优先级队列

第三种关联着一个被长期忽视的问题:缓冲膨胀(Bufferbloat)

设备厂商为了"减少丢包"配了巨大的缓冲区。结果是拥塞时数据包不再被丢弃,而是在缓冲区里排几百毫秒的队。基于丢包的拥塞控制算法收不到丢包信号,以为一切正常,继续猛发——延迟飙升但带宽还在跑满

典型症状:家里有人下载/上传大文件时,视频通话和游戏就卡得没法用,但测速软件显示带宽正常。

解法是让队列主动管理(fq_codelcake):

bash
tc qdisc add dev eth0 root fq_codel     # 队列满之前就开始丢包/标记,给发送方及时反馈

这也是 BBR 的另一个价值:它以"延迟开始上升"为拥塞信号,而不是等丢包——在膨胀的缓冲区面前,延迟比丢包更早、更准确

九、排障:分层二分法与工具箱

一个能覆盖 90% 情况的排查顺序

关键思路是按层自下而上二分,每一步都能排除一大片可能性:

症状:"访问 https://api.example.com 失败"

① 域名能解析吗?            dig api.example.com
   └─ 不能 → DNS 问题:记录没配 / NS 错 / 本地 DNS 缓存了旧值
   └─ 能,但 IP 不对 → 缓存未过期,或被 DNS 劫持

② IP 能通吗?               ping <ip>
   └─ 不通 → 可能是网络层,也可能只是对方封了 ICMP(别轻易下结论)
   └─ 用 mtr <ip> 看在哪一跳开始丢包/延迟暴涨

③ 端口能连上吗?            nc -zv <ip> 443     或   telnet <ip> 443
   └─ 连不上 → 服务没启动 / 防火墙 / 安全组 / 端口不对
   └─ 连上了 → 网络层和传输层都没问题,往上查

④ TLS 握手成功吗?          openssl s_client -connect <ip>:443 -servername api.example.com
   └─ 失败 → 证书过期 / 域名不匹配 / SNI 没配 / 协议版本不兼容 / 本机时间不对

⑤ HTTP 层对吗?             curl -v https://api.example.com
   └─ 看状态码、响应头、重定向链
   └─ 4xx → 请求本身的问题(Host、认证、路径)
   └─ 5xx → 服务端的问题,去看服务端日志

⑥ 还查不出来 → 抓包         tcpdump -i any -nn host <ip> -w dump.pcap
   └─ 用 Wireshark 打开,看真实的包交换
   └─ 这是最后手段,也是最可靠的手段:包不会说谎

一个特别常用的隔离技巧

curl--resolve 绕过 DNS,直接指定 IP:

bash
curl -v --resolve api.example.com:443:1.2.3.4 https://api.example.com/health

这能把"DNS 问题"和"服务器问题"彻底分开——如果指定 IP 就能访问,那就是 DNS 或调度的问题,服务器本身没事。灰度发布、CDN 排障、迁移验证时极其有用。

工具速查

工具干什么常用姿势
ip / ifconfig2–3看接口、地址、路由ip aip routeip neigh
ping3通不通、RTTping -M do -s 1472 x 测 MTU
traceroute / mtr3路径与逐跳丢包mtr 比 traceroute 有用得多(持续采样)
dig / nslookup7DNS 查询dig +trace x.com(看完整迭代过程)、dig @8.8.8.8 x.com
ss / netstat4看连接与状态ss -tanpss -s(统计各状态数量)
nc (netcat)4测端口、造流量nc -zv host 443nc -l 8080
curl7HTTP 全能工具-v 看握手、-w 看分段耗时、--resolve 绕 DNS
openssl s_client4.5TLS 诊断-servername 指定 SNI,-showcerts 看整条链
tcpdump抓包-nn 不解析名字、-w 存文件、-i any 所有接口
wireshark分析包过滤器 tcp.analysis.retransmission 找重传
iperf34测真实带宽-P 8 多流测(单流受窗口限制,测不出真实上限)
sysctl3–4内核网络参数sysctl -a 后过滤 tcp 相关项
nftables / iptables3–4防火墙、NAT排查时先 -L -n -v 看有没有规则在挡

curl -w 的分段计时值得单独推荐,它把第六节那条链路的耗时直接拆开了:

bash
curl -o /dev/null -s -w "\
DNS 解析:      %{time_namelookup}s
TCP 建连:      %{time_connect}s
TLS 握手完成:  %{time_appconnect}s
服务端首字节:  %{time_starttransfer}s
总耗时:        %{time_total}s
" https://example.com

一眼就能看出慢在哪一层——DNS 慢是解析问题,connect 慢是网络或后端队列问题,appconnect 慢是 TLS 问题,starttransfer 慢是应用处理慢。

十、常见误解清单

最后清理一批流传很广的错误认知:

说法实际情况
"ping 不通就是网络不通"很多设备封了 ICMP。用 nc -zv host port 测端口才准
"traceroute 中间某跳延迟高就是那里的问题"路由器处理 ICMP 是最低优先级。只有最后一跳的延迟可信
"带宽升级能解决网页慢"短请求的瓶颈是 RTT 数量,不是带宽(第四节慢启动)
"单机最多 65535 个连接"65535 是源端口数上限。连接由四元组标识,服务端可以有几百万连接
"HTTPS 只是加密"它同时提供身份认证和完整性,而身份认证是最难也最关键的那部分
"自签名证书不安全"加密强度一样,问题是无法验证对方身份——这正是中间人的入口
"Cache-Control: no-cache 表示不缓存"它表示"可以存但用前必须验证"。不缓存是 no-store
"UDP 不可靠所以低级"它是故意不做承诺。QUIC 就在 UDP 上实现了比 TCP 更好的传输层
"TCP 保证数据一定送达"只保证"送到了或者告诉你失败"。网线拔掉它也没办法
"MAC 地址全程不变"每跳都被改写。全程不变的是 IP(除 NAT)
"HTTP/2 之后要做域名分片"恰恰相反,分片在 h2 下是负优化
"IPv6 只是地址更长"头部固定、无路由器分片、NDP 代替 ARP、SLAAC 自动配置,行为差异很大
"TIME_WAIT 是个 bug,要调参消灭它"它是正确性的一部分。多说明你在频繁建连,该用长连接
"丢包一定说明网络拥塞"无线和长距链路有随机丢包,这是 BBR 存在的理由
"改了 DNS 记录立刻生效"取决于 TTL 与各级缓存。迁移前先降 TTL
"内网所以不用加密"ARP 欺骗、DHCP 欺骗都在内网。链路层没有任何认证

结语

回过头看,这张地图上最值得记住的是三件事。

第一,分层是为了让不同变化速度的东西解耦。 这不只是网络的道理。你能在 2026 年用同一个 IP 协议同时跑光纤和卫星、跑 HTTP/3 和 MQTT,靠的就是那个"细腰"足够简单、足够不做承诺。任何长寿的系统都有这样一个细腰——它的价值恰恰在于它拒绝提供的那些功能。

第二,几乎所有网络性能问题最终都回到 RTT。 光速给了硬下限,慢启动让新连接前几个来回浪费掉,握手要花掉 2–3 个 RTT,每个新域名都要重走一遍。CDN、连接复用、TLS 1.3、HTTP/2、QUIC——这些看起来毫不相干的技术,全都在做同一件事:减少 RTT 的数量或长度。 想清楚这一点,优化就不再需要靠猜。

第三,网络的信任模型是逐层建立的,而底层几乎没有信任。 ARP 可以伪造,DHCP 可以伪造,DNS 可以劫持,BGP 可以劫持,公共 WiFi 里的人可以看你的流量。每一个"可以伪造"都只能靠更上层的加密和认证来解决——这就是 HTTPS、DoH、WireGuard、RPKI 存在的全部理由。理解了这个,你就明白为什么"端到端加密"不是可选项,而是唯一可行的答案。

最后一句实用的:排障时永远从下往上、从简到繁。 大部分你以为是"应用有 bug"的问题,最后发现是 DNS 缓存了旧记录、安全组没放端口、或者 MTU 被隧道吃掉了几十字节。先花三分钟按第九节那个顺序走一遍,比直接扎进代码里读三小时有效得多。