目录
  1. 1. 一、为什么需要分层模型
  2. 2. 二、OSI 七层模型详解
    1. 2.1. 第 1 层:物理层(Physical Layer)
    2. 2.2. 第 2 层:数据链路层(Data Link Layer)
    3. 2.3. 第 3 层:网络层(Network Layer)
    4. 2.4. 第 4 层:传输层(Transport Layer)
    5. 2.5. 第 5 层:会话层(Session Layer)
    6. 2.6. 第 6 层:表示层(Presentation Layer)
    7. 2.7. 第 7 层:应用层(Application Layer)
  3. 3. 三、TCP/IP 四层模型详解
    1. 3.1. 3.1 网络接口层(Link Layer / Network Interface Layer)
    2. 3.2. 3.2 网际层(Internet Layer)
    3. 3.3. 3.3 传输层(Transport Layer)
    4. 3.4. 3.4 应用层(Application Layer)
  4. 4. 四、OSI 七层 vs TCP/IP 四层:深度对比
  5. 5. 五、应用层协议深度解析
    1. 5.1. 5.1 HTTP/1.1 —— 万维网的基石
    2. 5.2. 5.2 HTTP/2 —— 二进制革命
    3. 5.3. 5.3 HTTP/3 —— 基于 QUIC 的新一代协议
    4. 5.4. 5.4 DNS —— 互联网的电话簿
    5. 5.5. 5.5 TLS 1.3 —— 安全传输的现代标准
    6. 5.6. 5.6 WebSocket —— 全双工实时通信
  6. 6. 六、传输层协议深度解析
    1. 6.1. 6.1 TCP 连接状态机(全状态与转换)
    2. 6.2. 6.2 TCP 可靠传输机制
    3. 6.3. 6.3 UDP —— 简单即是力量
    4. 6.4. 6.4 QUIC —— 重新定义传输层
  7. 7. 七、网际层协议详解
    1. 7.1. 7.1 IPv4 地址与寻址
    2. 7.2. 7.2 IPv6 —— 下一代互联网协议
    3. 7.3. 7.3 ICMP —— 互联网诊断协议
    4. 7.4. 7.4 BGP 基础 —— 互联网的路由协议
  8. 8. 八、链路层协议详解
    1. 8.1. 8.1 以太网 —— 局域网的主宰
    2. 8.2. 8.2 ARP —— 从 IP 到 MAC
    3. 8.3. 8.3 802.11 WiFi —— 无线网络的基石
  9. 9. 九、数据封装过程:字节级逐步走读
    1. 9.1. 示例场景
    2. 9.2. Step 1: 应用层 → QUIC 帧
    3. 9.3. Step 2: QUIC 包封装
    4. 9.4. Step 3: UDP 封装
    5. 9.5. Step 4: IP 封装
    6. 9.6. Step 5: 以太网帧封装(WiFi 场景下更复杂,这里以有线以太网为例)
    7. 9.7. Step 6: 物理层编码(以 1000BASE-T 为例)
    8. 9.8. 封装层次总览
  10. 10. 十、Android 网络栈:从应用到内核
    1. 10.1. 10.1 应用层:OkHttp 架构
    2. 10.2. 10.2 系统服务层:ConnectivityManager
    3. 10.3. 10.3 守护进程层:netd
    4. 10.4. 10.4 内核层:iptables 与 netfilter
  11. 11. 十一、QUIC 深度剖析
    1. 11.1. 11.1 为什么 Google 造了 QUIC?
    2. 11.2. 11.2 QUIC 的核心设计
    3. 11.3. 11.3 QUIC 的连接建立过程
    4. 11.4. 11.4 QUIC 的连接迁移
    5. 11.5. 11.5 QUIC 的拥塞控制
    6. 11.6. 11.6 QUIC vs TCP+TLS 性能对比
  12. 12. 十二、Socket 编程:从理论到实践
    1. 12.1. 12.1 BSD Socket API
    2. 12.2. 12.2 非阻塞 I/O
    3. 12.3. 12.3 epoll —— Linux 高性能网络编程的核心
  13. 13. 十三、总结
【深入理解TCP协议】TCP/IP分层模型

一、为什么需要分层模型

计算机网络是一个极其复杂的系统。设想一下,你需要让两台位于地球两端的机器可靠地交换数据,这中间涉及物理信号传输、数据编码、路由选择、拥塞控制、应用协议解析等无数细节。如果不加以抽象,任何开发者都无法完整理解并实现一个网络应用。

分层模型的核心思想是:将复杂的网络通信过程拆解为若干个独立但又相互协作的层次。每一层:

  • 为上一层提供服务
  • 使用下一层提供的服务
  • 只关心本层的职责,不关心其他层的内部实现

这种设计哲学带来了三大好处:

1. 模块化与解耦。 每一层可以独立演进。例如,物理介质从铜缆升级到光纤不影响 IP 层工作;HTTP 协议从 1.1 升级到 3.0 不需要修改 TCP 层的代码。

2. 互操作性。 不同厂商的设备只要实现了相同的协议规范,就可以互联互通。你的 Android 手机可以无障碍访问运行在 Linux 服务器上的 Nginx,因为它们都遵循 TCP/IP 协议族。

3. 可替换性。 链路层可以在以太网和 WiFi 之间切换,而传输层以上的应用完全无感知——这就是你在家里走动时,手机从 WiFi 自动切换到 4G/5G 而视频通话不中断的根本原因(当然,这里还涉及 QUIC 的连接迁移等更高级的机制,后文会深入讨论)。

目前存在两个主要的网络分层参考模型:国际标准化组织(ISO)提出的 OSI 七层模型,以及实际部署中广泛使用的 TCP/IP 四层模型


二、OSI 七层模型详解

OSI(Open Systems Interconnection)模型由 ISO 于 1984 年发布,它将网络通信分为七个层次。虽然它在工程实践中从未被完整实现过,但其概念框架至今仍是理解网络协议的黄金标准。

第 1 层:物理层(Physical Layer)

物理层定义了比特流在物理介质上的传输方式。它回答的是最底层的问题:0 和 1 如何变成信号?

核心概念:

  • 信号编码: 曼彻斯特编码、NRZ(Non-Return-to-Zero)、4B/5B 编码等
  • 传输介质: 双绞线(Cat5e/Cat6/Cat7)、光纤(单模/多模)、同轴电缆、无线电波
  • 物理拓扑: 总线型、星型、环型、网状
  • 设备: 集线器(Hub)、中继器(Repeater)——它们只处理信号,不理解数据

在物理层,数据单元是比特(bit)。一个典型的物理层标准是以太网的 1000BASE-T(千兆以太网,使用 Cat5e 以上双绞线,采用 PAM-5 编码,每符号携带 2 比特信息,在 4 对线上同时传输)。

数据链路层解决了在相邻节点之间可靠传输帧的问题。它把物理层不可靠的比特流变成了有结构的、可验证的数据帧。

数据链路层又分为两个子层:

  • MAC(Media Access Control)子层: 控制对共享介质的访问
  • LLC(Logical Link Control)子层: 提供与上层的统一接口,负责流控和差错控制

核心概念:

  • 帧(Frame): 链路层的数据单元,包含源/目标 MAC 地址和 FCS(Frame Check Sequence)
  • MAC 地址: 48 位全球唯一标识符,如 aa:bb:cc:dd:ee:ff
  • 设备: 交换机(Switch)、网桥(Bridge)——它们基于 MAC 地址转发帧,维护 MAC 地址表
  • 典型协议: 以太网、802.11 WiFi、PPP、ARP(地址解析协议)

第 3 层:网络层(Network Layer)

网络层负责在任意两个主机之间选择路径并转发数据。它解决了”数据怎么从 A 到达 B”的问题,无论 A 和 B 之间隔着多少个路由器。

核心概念:

  • 分组(Packet): 网络层的数据单元,包含源/目标 IP 地址
  • IP 地址: 逻辑地址,与物理位置相关(非硬件绑定),有 IPv4(32 位)和 IPv6(128 位)
  • 路由: 路由器根据路由表决定分组的下一跳
  • 设备: 路由器(Router)、三层交换机
  • 典型协议: IPv4、IPv6、ICMP、IGMP、OSPF、BGP

第 4 层:传输层(Transport Layer)

传输层提供端到端(end-to-end)的数据传输服务。它不关心数据在中间经过了多少个路由器,只管源主机上某个进程发出的数据是否完整、有序地到达目标主机上对应的进程。

核心概念:

  • 端口号: 16 位整数(0-65535),标识主机上的具体进程
  • 段(Segment): 传输层的数据单元
  • 典型协议: TCP(面向连接、可靠传输)、UDP(无连接、尽力而为)

第 5 层:会话层(Session Layer)

会话层负责建立、管理和终止应用之间的会话。在 TCP/IP 模型中,会话管理通常由应用层协议自行处理。

职责包括:

  • 会话的建立与拆除
  • 对话控制(全双工/半双工)
  • 同步点的插入(用于大文件传输中断后的恢复)
  • 令牌管理

现实中,TLS 握手过程中的会话恢复(Session Resumption)、HTTP 的 Cookie/Session 机制、RPC 框架中的连接管理,都可以视为广义的会话层功能。

第 6 层:表示层(Presentation Layer)

表示层处理数据的语法和语义,确保一端发送的数据能被另一端正确理解。

职责包括:

  • 数据格式转换: 字符编码(ASCII、UTF-8)、字节序(大端/小端)
  • 数据压缩: 减少传输数据量
  • 数据加密/解密: TLS/SSL 协议在 OSI 模型中属于这一层

在 Web 开发中,JSON、XML 序列化/反序列化、Content-Encoding: gzip、TLS 加密,都是表示层的典型实践。

第 7 层:应用层(Application Layer)

应用层是离用户最近的一层,它直接为应用程序提供网络服务。注意:应用层不是应用程序本身,而是应用程序所使用的通信协议。

典型协议:

  • HTTP/HTTPS —— Web 浏览
  • SMTP/IMAP/POP3 —— 电子邮件
  • FTP —— 文件传输
  • DNS —— 域名解析
  • SSH —— 安全远程登录
  • WebSocket —— 全双工实时通信

三、TCP/IP 四层模型详解

TCP/IP 模型不是先有模型后有协议,而是先有协议实现,后归纳出模型。ARPANET 的研究者们先写出了 TCP/IP 协议栈,后来才抽象出四层模型。因此它不像 OSI 模型那样”完美”和”对称”,但它是事实上的互联网标准

TCP/IP 四层(自上而下):

┌─────────────────────────────────┐
│ 应用层 (Application) │ HTTP, DNS, TLS, WebSocket, FTP, SMTP...
├─────────────────────────────────┤
│ 传输层 (Transport) │ TCP, UDP, QUIC, SCTP...
├─────────────────────────────────┤
│ 网际层 (Internet) │ IP (v4/v6), ICMP, ARP, BGP, OSPF...
├─────────────────────────────────┤
│ 网络接口层 (Link/Network) │ Ethernet, WiFi (802.11), PPP...
└─────────────────────────────────┘

这一层对应于 OSI 的物理层和数据链路层的合并。它处理与特定物理网络技术的交互。

TCP/IP 设计的一个精妙之处在于:它没有定义自己的链路层协议,而是复用了现有的各种网络技术。IP 协议被设计成可以在以太网、WiFi、PPP、帧中继等任意链路层技术上运行——这种”IP over Everything”的思想是互联网成功的基石。

在 Linux 内核中,链路层设备抽象为 struct net_device,网络驱动程序通过 register_netdev() 注册设备,通过 NAPI(New API)机制混合中断和轮询来处理数据包接收。

3.2 网际层(Internet Layer)

网际层是整个 TCP/IP 架构的核心。它的主要协议是 IP(Internet Protocol)

IP 协议的关键特性:

  • 无连接(Connectionless): 每个数据包独立路由,不需要预先建立连接
  • 尽力而为(Best-effort): 不保证数据一定到达,不保证有序,不保证不重复
  • 分片与重组: 当数据包超过链路 MTU 时,IP 层负责分片,目标端负责重组

IP 层之上的辅助协议:

  • ICMP(Internet Control Message Protocol): 用于诊断和错误报告(pingtraceroute
  • IGMP(Internet Group Management Protocol): 管理多播组成员
  • ARP(Address Resolution Protocol): IPv4 地址到 MAC 地址的映射

3.3 传输层(Transport Layer)

传输层在 IP 层提供的”主机到主机”通信基础上,实现了**”进程到进程”的通信**。它使用端口号来区分同一主机上的不同进程。

TCP/IP 协议族中最重要的两个传输层协议是 TCPUDP。此外,近年来 QUIC 作为一个运行在 UDP 之上的新型传输协议正在快速普及。

3.4 应用层(Application Layer)

TCP/IP 的应用层合并了 OSI 的会话层、表示层和应用层。这意味着在 TCP/IP 模型中,会话管理和数据表示的责任落在了具体的应用协议上——这正是为什么 HTTP 协议需要自己处理状态管理(Cookie)、内容协商(Content Negotiation)和压缩编码。


四、OSI 七层 vs TCP/IP 四层:深度对比

维度 OSI 七层模型 TCP/IP 四层模型
层数 7 4
来源 ISO 先制定模型,后实现协议 先有协议实践,后归纳模型
会话层/表示层 独立两层 并入应用层
物理层/链路层 分开 合并为网络接口层
理论 vs 实践 理论完备,概念清晰 工程实用,略有粗糙
协议独立性 强调协议无关 紧密绑定 TCP/IP 协议族
实际使用 教学参考 互联网实际标准

关键差异解析:

  1. 层数差异的本质。 OSI 之所以比 TCP/IP 多三层,是因为它将”会话管理”和”数据表示”从应用层中独立出来了。这在理论上很美——关注点分离——但在实践中,大多数应用协议(HTTP、SMTP、FTP)已经内建了这些功能,拆分反而增加了不必要的复杂性。

  2. 协议设计的哲学不同。 OSI 倾向于”先设计完美的模型,再填充协议”(自上而下)。TCP/IP 则遵循”先写出能工作的代码,再抽象为模型”(自下而上)。后者更务实,也更符合互联网的演进方式。

  3. 物理层与链路层分合的考量。 OSI 将物理层和链路层分开,强调了信号传输和帧传输是两个不同层面的事情。TCP/IP 将它们合并,因为在实际工程中,这两层的实现通常紧密耦合(例如以太网卡驱动同时处理物理信号和 MAC 帧)。

  4. 跨层设计(Cross-layer Design)。 在真实的 TCP/IP 实现中,常常存在”跨层”行为。例如,TCP 的拥塞控制需要感知网络层的丢包事件;QUIC 在应用层实现了传输层的可靠性和拥塞控制。这打破了严格的分层原则,但换来了更高的性能和灵活性。


五、应用层协议深度解析

5.1 HTTP/1.1 —— 万维网的基石

HTTP/1.1(RFC 2616, 后由 RFC 7230-7235 取代)是使用最广泛的 HTTP 版本。相比 HTTP/1.0,它引入了几个关键改进:

持久连接(Keep-Alive):
HTTP/1.0 默认每次请求/响应后关闭 TCP 连接。HTTP/1.1 默认启用 Connection: keep-alive,允许在同一个 TCP 连接上发送多个请求/响应。这避免了重复的 TCP 三次握手和慢启动过程。

HTTP/1.0(无 Keep-Alive):
Client ──SYN──► Server
Client ◄──SYN+ACK── Server
Client ──ACK──► Server ← 三次握手完成
Client ──GET /a──► Server
Client ◄──200 OK── Server
Client ──FIN──► Server ← 连接关闭

HTTP/1.1(Keep-Alive):
Client ──SYN──► Server
...三次握手...
Client ──GET /a──► Server
Client ◄──200 OK── Server
Client ──GET /b──► Server ← 复用同一连接
Client ◄──200 OK── Server
Client ──GET /c──► Server
Client ◄──200 OK── Server
...可继续复用...

管道化(Pipelining):
客户端可以在收到前一个响应之前发送后续请求。理论上这能减少延迟,但实践中由于队头阻塞(Head-of-Line Blocking)问题——第一个请求的响应如果慢,会阻塞后续所有响应的传递——大多数浏览器默认禁用了管道化。

分块传输编码(Chunked Transfer Encoding):
Transfer-Encoding: chunked 允许服务器在不知道内容总长度的情况下开始发送响应。这对于动态生成的内容、流式数据非常有用。

HTTP/1.1 200 OK
Transfer-Encoding: chunked

1A\r\n ← 十六进制表示块大小 (26 字节)
This is the first chunk.\r\n
1B\r\n
This is the second chunk data.\r\n
0\r\n ← 0 表示传输结束
\r\n

缓存控制:
HTTP/1.1 引入了 Cache-Control 头部,提供了比 HTTP/1.0 的 Expires 更精细的缓存策略:

  • Cache-Control: max-age=3600 —— 资源在 3600 秒内有效
  • Cache-Control: no-cache —— 需要每次向服务器验证(使用 ETag/If-None-Match)
  • Cache-Control: no-store —— 完全不缓存
  • Cache-Control: public —— 允许中间代理缓存
  • Cache-Control: private —— 只允许浏览器缓存
  • Cache-Control: immutable —— 资源永不变更,不需要验证(用于带指纹的静态资源)

5.2 HTTP/2 —— 二进制革命

HTTP/2(RFC 7540)由 Google 的 SPDY 协议演进而来,于 2015 年发布。它没有修改 HTTP 的语义(方法、头部、状态码保持不变),而是完全改变了传输方式。

二进制分帧层(Binary Framing Layer):
这是 HTTP/2 最核心的变革。HTTP/1.1 是纯文本协议,可读性好但解析效率低。HTTP/2 将所有通信拆分为二进制帧:

 HTTP/2 帧结构:
┌──────────────────────────────────────┐
│ Length (24 bits) │ Type (8 bits) │
├──────────────────────┼───────────────┤
│ Flags (8 bits) │ │
├──────────────────────┤ R (1 bit) │
│ Stream ID (31 bits) │ │
├──────────────────────┴───────────────┤
│ Frame Payload (可变长度) │
└──────────────────────────────────────┘

帧类型:

  • DATA(0x0):传输请求或响应体
  • HEADERS(0x1):传输 HTTP 头部
  • PRIORITY(0x2):指定流的优先级
  • RST_STREAM(0x3):终止一个流
  • SETTINGS(0x4):协商连接参数(如最大并发流数、初始窗口大小)
  • PUSH_PROMISE(0x5):服务器推送承诺
  • PING(0x6):心跳检测与 RTT 测量
  • GOAWAY(0x7):优雅关闭连接
  • WINDOW_UPDATE(0x8):流控窗口更新
  • CONTINUATION(0x9):延续未完成的 HEADERS 块

多路复用(Multiplexing):
在一个 TCP 连接上同时运行多个流(Stream)。每个流有独立的 Stream ID,帧可以交错发送。接收端根据 Stream ID 将帧重组——这从根本上解决了 HTTP/1.x 的队头阻塞问题。

但注意:HTTP/2 仍然存在 TCP 层面的队头阻塞。如果 TCP 层的一个数据包丢失,所有流的传输都会暂停等待重传——因为 TCP 保证字节流的严格有序性。

头部压缩(HPACK):
HTTP/1.x 每次请求都携带完整的文本头部(Cookie 经常超过 1KB)。HPACK 使用以下技术压缩头部:

  • 静态字典: 预定义了 61 个常见的头部名称/值(如 :method: GET:status: 200
  • 动态字典: 连接双方各自维护一个动态表,存储实际看到的头部组合,后续只需发送索引号
  • 霍夫曼编码: 对字符串进行压缩

服务器推送(Server Push):
服务器可以主动向客户端推送资源,而无需客户端显式请求。例如:

Client: GET /index.html
Server: 返回 index.html + PUSH_PROMISE(/style.css)+ PUSH_PROMISE(/app.js)
主动推送 style.css 和 app.js

服务器推送在实践中效果有限——浏览器可能已有缓存,过度推送会浪费带宽。Chrome 在 2022 年甚至移除了 HTTP/2 服务器推送的支持。

5.3 HTTP/3 —— 基于 QUIC 的新一代协议

HTTP/3(RFC 9114)是最新的 HTTP 版本,它不再使用 TCP,而是基于 QUIC 协议运行在 UDP 之上。

为什么需要 HTTP/3?
HTTP/2 虽然解决了应用层的队头阻塞,但 TCP 层的队头阻塞依然存在。在高速、高延迟或丢包率较高的网络(如移动网络)中,单个 TCP 包的丢失就会阻塞整个 HTTP/2 连接上的所有流。HTTP/3 通过 QUIC 的多流独立传输彻底消除了这一问题。

HTTP/3 的关键变化:

  • 传输层从 TCP 切换到 QUIC(基于 UDP)
  • 头部压缩从 HPACK 切换到 QPACK(适应 QUIC 的无序交付特性)
  • 连接不再由 IP:Port 元组标识,而是由 QUIC 的 Connection ID 标识——支持连接迁移
  • 0-RTT 握手:以前连接过的客户端可以立即发送数据

QPACK vs HPACK:
HPACK 假设接收端按顺序处理头部帧(因为 TCP 保证有序),动态表中的引用是同步的。但 QUIC 的流是无序的,QPACK 因此引入了双向流来传递动态表更新,避免了流的依赖。

5.4 DNS —— 互联网的电话簿

DNS(Domain Name System)将人类可读的域名转换为机器可读的 IP 地址。

DNS 查询过程:

用户输入 www.example.com


浏览器检查本地 DNS 缓存(Chrome 有自己的 DNS 缓存,约 1 分钟)
│ (未命中)

操作系统检查 hosts 文件(/etc/hosts 或 C:\Windows\System32\drivers\etc\hosts)
│ (未命中)

向本地 DNS 解析器发起查询(通常由 DHCP 配置,如 8.8.8.8 或 192.168.1.1)


┌─── 递归查询 ───┐
│ 本地 DNS 解析器 │
│ │ │
│ ▼ │
│ 根域名服务器 │ → 返回 .com 的 TLD 服务器地址
│ │ │
│ ▼ │
│ .com TLD 服务器 │ → 返回 example.com 的权威 DNS 地址
│ │ │
│ ▼ │
│ example.com │ → 返回 www.example.com 的 A 记录: 93.184.216.34
│ 权威 DNS 服务器 │
└─────────────────┘


结果缓存并返回给客户端(附带 TTL)

DNS 记录类型:

  • A(Address): 域名 → IPv4 地址
  • AAAA(Quad-A): 域名 → IPv6 地址
  • CNAME(Canonical Name): 域名别名 → 规范域名
  • MX(Mail Exchange): 邮件服务器地址(带优先级)
  • NS(Name Server): 域名的权威 DNS 服务器
  • TXT: 任意文本,常用于 SPF、DKIM、域名验证
  • SOA(Start of Authority): 域的管理信息
  • PTR(Pointer): 反向 DNS 查询(IP → 域名)
  • SRV(Service): 指定服务的地址和端口

DNS over HTTPS(DoH)与 DNS over TLS(DoT):
传统的 DNS 查询是明文传输(UDP/53),容易被运营商劫持或篡改。DoH(RFC 8484)将 DNS 查询封装在 HTTPS 请求中,DoT(RFC 7858)则使用 TLS 加密 DNS 连接。这两种方式都能防止 DNS 污染和中间人攻击。

DNS 报文格式(以 A 记录查询为例,字节级走读):

DNS 查询报文(请求 www.example.com 的 A 记录):
┌────────────────────────────────────────────────────────┐
│ Transaction ID (2 bytes): 0x1234 │ 标识字段,匹配请求与响应
├────────────────────────────────────────────────────────┤
│ Flags (2 bytes): 0x0100 │ 标准查询,期望递归
│ QR=0 (查询) Opcode=0000 (标准) AA=0 TC=0 RD=1 │
├────────────────────────────────────────────────────────┤
│ Questions: 0x0001 (1 个问题) │
│ Answer RRs: 0x0000 │
│ Authority RRs: 0x0000 │
│ Additional RRs: 0x0000 │
├────────────────────────────────────────────────────────┤
│ Query: │
│ 3 'w' 'w' 'w' 7 'e' 'x' 'a' 'm' 'p' 'l' 'e' │ 域名以 length-value 编码
│ 3 'c' 'o' 'm' 0x00 │ 每个标签前一个字节标明长度
│ Type: 0x0001 (A) │
│ Class: 0x0001 (IN) │
└────────────────────────────────────────────────────────┘

5.5 TLS 1.3 —— 安全传输的现代标准

TLS(Transport Layer Security)在 OSI 模型中位于传输层之上(或表示层),在 TCP/IP 模型中位于应用层和传输层之间。TLS 1.3(RFC 8446, 2018 年发布)相比 TLS 1.2 做了重大简化和强化。

TLS 1.2 的握手缺陷:
TLS 1.2 完成握手需要 2-RTT(两个往返时延):

  1. ClientHello → ServerHello + Certificate + ServerKeyExchange + ServerHelloDone(0.5 RTT)
  2. ClientKeyExchange + ChangeCipherSpec + Finished → ChangeCipherSpec + Finished(0.5 RTT)
    此时才能开始发送应用数据。在 100ms RTT 的网络上,光握手就需要 200ms。

TLS 1.3 的 1-RTT 握手:

Client                                    Server
│ │
│── ClientHello ──────────────────────► │
│ + key_share (X25519/ECDHE 公钥) │
│ + supported_ciphersuites │
│ + signature_algorithms │
│ │
│ ◄── ServerHello + │
│ {EncryptedExtensions} │
│ {Certificate} │
│ {CertificateVerify} │
│ {Finished} ────── │
│ │
│── {Finished} ────────────────────────► │
│── {Application Data} ───────────────► │ ← 此时已可发送应用数据
│ │

TLS 1.3 将握手精简为 1-RTT

  1. 客户端在 ClientHello 中直接发送密钥交换的公钥(不再协商后再交换)
  2. 服务器在第二个飞行包中同时发送 ServerHello + 证书 + 验证 + Finished
  3. 客户端验证后立即发送 Finished 和应用数据

TLS 1.3 的 0-RTT 握手(Early Data / PSK 模式):

如果客户端之前与服务器建立过连接并保存了 PSK(Pre-Shared Key),它可以在第一个飞行包中就发送应用数据:

Client                                    Server
│ │
│── ClientHello ──────────────────────► │
│ + pre_shared_key (psk_identity) │
│ + key_share │
│ + {Early Application Data} ← 0-RTT! │ ← 第一个飞行包就必须携带应用数据
│ │
│ ◄── ServerHello + │
│ {EncryptedExtensions} │
│ {Finished} ────── │
│ │
│── {Finished} ────────────────────────► │
│── {Application Data} ───────────────► │

0-RTT 的安全警告: 0-RTT 数据缺乏前向安全性——攻击者可以录制并重放 0-RTT 数据包。因此服务器应限制 0-RTT 的操作范围(只允许幂等的 GET 请求,不允许 POST/PUT/DELETE)。

TLS 1.3 移除的陈旧功能:

  • 静态 RSA 密钥交换(无前向安全性)
  • CBC 模式密码套件(受 Padding Oracle 攻击困扰)
  • RC4、3DES、MD5、SHA-1
  • 压缩(因 CRIME 攻击而移除)
  • 重协商(Renegotiation)

TLS 1.3 强制使用的算法:

  • 密钥交换:ECDHE(椭圆曲线迪菲-赫尔曼,前向安全)
  • 身份认证:RSA-PSS 或 ECDSA
  • 对称加密:AEAD(Authenticated Encryption with Associated Data),如 AES-GCM、ChaCha20-Poly1305
  • 哈希:SHA-256 或 SHA-384

5.6 WebSocket —— 全双工实时通信

WebSocket(RFC 6455)在单个 TCP 连接上提供全双工、低延迟的消息通道。

WebSocket 升级握手(Upgrade Handshake):

客户端请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com

服务器响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept 的计算方式:将客户端 Sec-WebSocket-Key 与固定 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接,计算 SHA-1 哈希,再 Base64 编码。这确保了服务器确实理解 WebSocket 协议。

WebSocket 帧格式:

WebSocket 帧结构:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64, if len=126/127) |
|N|V|V|V| |S| | |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Masking-key (4 字节,仅客户端→服务器消息需要,MASK=1 时存在) |
+---------------------------------------------------------------+
| Payload Data |
+---------------------------------------------------------------+
  • FIN(1 bit): 是否为消息的最后一帧
  • Opcode(4 bits): 0x1=文本,0x2=二进制,0x8=关闭连接,0x9=Ping,0xA=Pong
  • MASK(1 bit): 帧是否被掩码。客户端→服务器的消息必须掩码(防止缓存投毒攻击)
  • Payload Length(7 bits / 7+16 bits / 7+64 bits): 可变长度编码

WebSocket 心跳机制:
WebSocket 协议内置了 Ping/Pong 帧用于保活检测。服务器或客户端可随时发送 Ping 帧,接收方必须回复 Pong 帧。如果连续多次 Ping 无 Pong 响应,即可判定连接断开。


六、传输层协议深度解析

6.1 TCP 连接状态机(全状态与转换)

TCP 的连接管理是整个协议最精密的部分之一。TCP 连接在两端的生命周期中有 11 种状态。理解状态机是诊断网络问题的基础。

                  ┌──────────┐
│ CLOSED │
└────┬─────┘

被动打开 │ 主动打开
(listen()) │ (connect())

┌────▼─────┐
│ LISTEN │
└────┬─────┘
│ recv SYN
│ send SYN+ACK

┌────▼──────────┐
┌──────│ SYN-RECEIVED │◄──────────────┐
│ └────┬──────────┘ │
│ │ recv ACK │ 同时打开:
│ │ │ recv SYN
│ ┌────▼──────────┐ │ send SYN+ACK
│ │ ESTABLISHED │ │
│ └─┬─────────┬──┘ │
│ │ │ │
主动关闭 │ │ │ 被动关闭 │
(close())│ │ │ recv FIN │
send FIN │ │ │ send ACK │
│ ┌────▼──┐ ┌───▼──────┐ │
│ │FIN-WAIT-1│ │CLOSE-WAIT│ │
│ └─┬───┬──┘ └───┬──────┘ │
│ │ │ │ 被动方 close() │
│ │ │ │ send FIN │
│ │ │ ┌────▼──────┐ │
│ │ │ │ LAST-ACK │ │
│ │ │ └────┬──────┘ │
recv ACK │ │ │ │ recv ACK │
│ ┌───▼──┐ │ ▼ │
│ │FIN-WAIT-2│ ┌──────┐ │
│ └───┬──┘ │ CLOSED │ │
│ │ └──▲──┬─▲┘ │
│ │ │ │ │ │
│ │ recv FIN│ │ │Timeout=2MSL │
│ │ send ACK│ │ │ │
│ ┌──▼──────┐ │ │ │ │
│ │TIME-WAIT├──┘ │ │ │
│ └─────────┘ │ │ │
│ │ │ │
│ 同时关闭: │ │ │
│ FIN-WAIT-1 │ │ │
│ recv FIN │ │ │
│ send ACK │ │ │
└─►┌────────┐ │ │ │
│CLOSING │ │ │ │
└───┬────┘ │ │ │
│ recv ACK │ │ │
└────────────┘ │ │
┌──────────────┘ │
│ recv SYN+ACK │
│ send ACK │
└──────────────────────────────┘

状态详解:

CLOSED(关闭)
连接的初始状态和最终状态。此时没有活跃连接,不占用任何系统资源。

LISTEN(监听)
服务器端调用 listen() 后进入此状态,等待客户端的连接请求。在 Linux 内核中,LISTEN 状态的 socket 维护两个队列:

  • SYN 队列(半连接队列): 存放收到 SYN 但尚未完成三次握手的连接。队列大小由 net.ipv4.tcp_max_syn_backlogsomaxconn 控制。当队列满时,新的 SYN 将被丢弃(导致客户端 SYN 重传)或发送 SYN Cookie。
  • Accept 队列(全连接队列): 存放已完成三次握手但尚未被 accept() 取走的连接。队列大小由 listen()backlog 参数指定。当队列满时,新连接将被丢弃。

SYN-SENT(已发送 SYN)
客户端调用 connect() 后进入此状态,已发送 SYN 报文,等待服务器的 SYN+ACK。

  • 如果没有收到响应,TCP 会进行指数退避重传(典型重试次数:net.ipv4.tcp_syn_retries = 6,总等待时间约 127 秒)
  • 这是最容易遭受 SYN Flood 攻击的阶段

SYN-RECEIVED(已收到 SYN)
服务器收到 SYN 并回复 SYN+ACK 后进入此状态,等待客户端的 ACK。如果此时大量连接处于 SYN-RECEIVED 状态,通常意味着受到了 SYN Flood 攻击(攻击者只发 SYN 不回 ACK)。Linux 内核的 SYN Cookie 机制可以有效防御此类攻击,其原理是在 SYN+ACK 的初始序列号中编码连接信息,无需在服务端存储半连接状态。

ESTABLISHED(已建立)
三次握手完成,双方可以自由发送数据。这是连接正常工作的状态。

FIN-WAIT-1(等待 FIN 确认 1)
主动关闭方调用 close() 并发送 FIN 后进入此状态。此时仍可以接收数据。

  • 如果收到 ACK → 进入 FIN-WAIT-2
  • 如果收到 FIN → 进入 CLOSING(同时关闭)
  • 如果收到 FIN+ACK → 直接进入 TIME-WAIT

FIN-WAIT-2(等待 FIN 确认 2)
主动关闭方已收到对自己 FIN 的 ACK,等待对端发送 FIN。如果对端不关闭(半关闭状态),此状态可能持续很久。Linux 提供了 tcp_fin_timeout 参数(默认 60 秒)来超时关闭这样的连接。

CLOSE-WAIT(等待关闭)
被动关闭方收到 FIN 并回复 ACK 后进入此状态。此时被动方仍然可以发送数据。只有当被动方的应用程序也调用 close() 时,才会发送 FIN 并进入 LAST-ACK。

CLOSE-WAIT 是生产环境中最常见的连接泄露状态。如果在 netstat -antp 的输出中看到大量 CLOSE-WAIT 连接,通常说明应用程序没有正确关闭 socket——可能是代码中忘记调用 close()

LAST-ACK(等待最终确认)
被动关闭方调用 close() 发送 FIN 后进入此状态,等待主动方的最后一个 ACK。收到 ACK 后进入 CLOSED。

TIME-WAIT(等待超时)
主动关闭方收到对端的 FIN 并发送 ACK 后进入此状态。TIME-WAIT 持续 2MSL(Maximum Segment Lifetime,通常为 60 秒到 120 秒)。为什么需要 TIME-WAIT?

  1. 确保最后的 ACK 能到达对端。 如果最后一个 ACK 丢失,对端会重传 FIN,主动方需要能够重传 ACK。
  2. 防止旧连接的数据包干扰新连接。 让网络中所有属于此连接的”游荡”数据包在网络中超时。

高并发服务器上可能出现大量 TIME-WAIT 连接,可以通过以下手段优化:

  • 启用 tcp_tw_reuse(允许将 TIME-WAIT 连接重用于新的出站连接)
  • 调整 tcp_max_tw_buckets(限制 TIME-WAIT 连接总数)
  • 应用层使用 SO_LINGER 配置异常关闭的行为

CLOSING(同时关闭中)
两端同时发送 FIN 时进入的短暂状态。收到对方的 ACK 后进入 TIME-WAIT。

6.2 TCP 可靠传输机制

序号与确认:
TCP 将数据流中的每个字节编号。连接建立时双方各自随机选择一个初始序号(ISN, Initial Sequence Number)。

  • 序号(Sequence Number): 本报文段第一个数据字节的编号
  • 确认号(Acknowledgment Number): 期望收到的下一个字节的序号(累积确认)

例:A 发送了字节 1001-2000,B 回复 ACK 2001 表示”我已经收到了 1001-2000,期望下一个字节是 2001”。

滑动窗口(Sliding Window):
TCP 使用滑动窗口实现流量控制。窗口大小由接收方在 ACK 报文的 Window Size 字段中通告,告诉发送方”我还能接收多少字节”。

发送方的滑动窗口:
字节编号: ...| 已发送已确认 | 已发送未确认 | 可发送未发送 | 不可发送 |...
← 窗口左沿 ← 窗口右沿 ← 窗口右沿 + 窗口大小
←──── 发送窗口 ────→

当窗口大小为 0 时,发送方停止发送(Zero Window Probe 除外,会定期发送探测包)。

拥塞控制(Congestion Control):
TCP 的拥塞控制由四个核心算法组成:

1. 慢启动(Slow Start):
连接建立或超时重传后,cwnd(拥塞窗口)从一个较小的值开始(Linux 中初始为 10 个 MSS,initcwnd)。每收到一个 ACK,cwnd 增加 1 个 MSS——相当于每个 RTT 翻倍(指数增长)。当 cwnd 达到 ssthresh(慢启动阈值)时,进入拥塞避免阶段。

2. 拥塞避免(Congestion Avoidance):
cwnd 每个 RTT 线性增加约 1 个 MSS(实际实现通常是 cwnd += MSS * (MSS / cwnd),每收到一个 ACK 增加一个小增量)。这是 AIMD(Additive Increase, Multiplicative Decrease)的”加性增加”部分。

3. 快速重传(Fast Retransmit):
当发送方连续收到 3 个重复的 ACK(Dup ACK)时,它认为该报文段已丢失,立即重传而不等待超时。三个重复 ACK 的阈值的合理之处在于:少量重复 ACK 可能由数据包乱序引起(在网络中并不罕见),但连续 3 个重复 ACK 几乎可以确定是丢包。

4. 快速恢复(Fast Recovery):
执行快速重传后,TCP 不会回到慢启动(那样会将 cwnd 重置为初始值),而是:

  • ssthresh = cwnd / 2(乘性减半)
  • cwnd = ssthresh + 3 * MSS(因为收到了 3 个重复 ACK,说明有 3 个包已被接收)
  • 进入拥塞避免阶段,线性增长

这种机制避免了单个丢包导致吞吐量剧烈下降。但如果是超时重传(RTO, Retransmission Timeout),说明网络拥塞严重,TCP 会回到慢启动。

TCP 拥塞控制算法演进:

  • Reno(1990): 实现了快速重传和快速恢复
  • New Reno(2004): 改进了在一个窗口内多个丢包时的恢复
  • CUBIC(2008,Linux 默认): 使用三次函数代替 AIMD 的线性增长,在高带宽高延迟网络(LFN)中表现更好
  • BBR(2016,Google): 基于瓶颈带宽和 RTT 来建模,不再依赖丢包作为拥塞信号

6.3 UDP —— 简单即是力量

UDP(User Datagram Protocol, RFC 768)是最简单的传输层协议。它只做了两件事:端口复用校验和

UDP 报文头(仅 8 字节):

 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
| Source Port | Destination Port |
+--------+--------+--------+--------+
| Length | Checksum |
+--------+--------+--------+--------+
| data bytes ...
+-------------------------------------
  • 无连接: 不需要握手,直接发送
  • 无状态: 不需要维护连接状态
  • 不保证交付: 丢包不重传
  • 不保证顺序: 后发的包可能先到
  • 不拥塞控制: 可以按任意速率发送

UDP 的适用场景:

  • 实时音视频: 丢几个包不影响观看,但延迟必须低(WebRTC)
  • DNS 查询: 请求和响应通常各是一个包,不需要连接
  • DHCP: 获取 IP 地址时还没有 IP 地址,更谈不上建立 TCP 连接
  • QUIC/HTTP/3: 在 UDP 之上重新实现可靠性
  • 游戏: 位置更新需要低延迟,丢一两帧无所谓

6.4 QUIC —— 重新定义传输层

QUIC(Quick UDP Internet Connections)是 Google 设计的新一代传输协议,最初用于加速 Chrome 与 Google 服务器的连接。2021 年,QUIC 被标准化为 RFC 9000,成为 HTTP/3 的底层传输协议。

QUIC 本质上是在 UDP 之上实现了一套完整的传输层:可靠交付、拥塞控制、TLS 1.3 加密、多路复用——所有这些都集成在同一个协议中。后续的 QUIC 深度剖析章节将全面展开。


七、网际层协议详解

7.1 IPv4 地址与寻址

IPv4 使用 32 位地址空间,理论上可提供约 43 亿个地址,在 2011 年已基本耗尽。

地址分类(有类寻址,历史概念):

  • A 类: 0.0.0.0 ~ 127.255.255.255,前 8 位网络号,后 24 位主机号(如 10.0.0.0/8
  • B 类: 128.0.0.0 ~ 191.255.255.255,前 16 位网络号,后 16 位主机号(如 172.16.0.0/12
  • C 类: 192.0.0.0 ~ 223.255.255.255,前 24 位网络号,后 8 位主机号(如 192.168.0.0/16
  • D 类: 224.0.0.0 ~ 239.255.255.255,用于多播
  • E 类: 240.0.0.0 ~ 255.255.255.255,保留

CIDR(无类别域间路由):
CIDR 废弃了有类寻址,使用子网掩码(或前缀长度)来划分网络。例如 192.168.1.0/24 表示前 24 位是网络号,后 8 位是主机号。这让 IP 地址分配更加灵活,也是 NAT 等技术的基础。

NAT(网络地址转换):
由于 IPv4 地址严重不足,NAT 允许多个设备共享一个公网 IP。常见的 NAT 类型:

  • SNAT(Source NAT): 修改源 IP 地址(出站流量)
  • DNAT(Destination NAT): 修改目标 IP 地址(入站流量,如端口映射)
  • PAT(Port Address Translation)/NAPT: 使用不同端口区分不同内部主机

NAT 的工作原理(以典型的家庭路由器为例):

内部主机 192.168.1.10:45678 → 外部服务器 8.8.8.8:53

路由器 NAT 表:
内部地址:端口 外部地址:端口 远程地址:端口
192.168.1.10:45678 → 203.0.113.5:50001 → 8.8.8.8:53

响应包: 8.8.8.8:53 → 203.0.113.5:50001
路由器查 NAT 表,转换为: 8.8.8.8:53 → 192.168.1.10:45678

NAT 穿透问题: NAT 阻断了外部主动发起的连接。P2P 应用(如 WebRTC)需要使用 STUN、TURN、ICE 等技术进行 NAT 穿透。这也正是 QUIC 选择基于 UDP 的原因之一——UDP 的 NAT 超时和穿透机制比 TCP 更成熟。

7.2 IPv6 —— 下一代互联网协议

IPv6 使用 128 位地址空间(理论数量 2^128,约 3.4x10^38 个),彻底解决了地址枯竭问题。

IPv6 的改进:

  • 更大的地址空间(128 位 vs 32 位)
  • 简化的头部结构(40 字节固定长度 vs IPv4 20-60 字节可变长度)
  • 无分片(路由器不分片,由发送端负责路径 MTU 发现)
  • 无校验和(链路层和传输层已经有校验和,IP 层不再重复计算,提高转发效率)
  • 内置 IPSec 支持
  • 无广播,使用多播和任播代替

IPv6 地址表示:

完整格式:    2001:0db8:0000:0000:0000:ff00:0042:8329
压缩格式: 2001:db8::ff00:42:8329
IPv4 映射: ::ffff:192.0.2.128
回环地址: ::1
未指定地址: ::
链路本地: fe80::/10

IPv6 头部格式:

IPv6 头部(固定的 40 字节):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| Traffic Class | Flow Label |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload Length | Next Header | Hop Limit |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| Source Address (128 bit) |
+ +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| Destination Address (128 bit) |
+ +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

IPv4 vs IPv6 头部对比:

  • IPv4 头部有 13 个字段(含选项),IPv6 只有 8 个
  • IPv4 头部的校验和、标识、标志、分片偏移等字段在 IPv6 中被移除
  • 这些简化让路由器处理 IPv6 包的效率显著提升

7.3 ICMP —— 互联网诊断协议

ICMP(Internet Control Message Protocol)是 IP 层的辅助协议,用于传递错误信息和网络诊断。

ICMP 报文结构:

ICMP 报文(封装在 IP 数据报中):
┌──────────┬──────────┬────────────────────┬──────────────────┐
│ Type (8) │ Code (8) │ Checksum (16) │ Message Body │
└──────────┴──────────┴────────────────────┴──────────────────┘

常用 ICMP 类型:

  • Type 0, Code 0: Echo Reply(ping 回复)
  • Type 3: Destination Unreachable(目标不可达)
    • Code 0: Network Unreachable
    • Code 1: Host Unreachable
    • Code 3: Port Unreachable(UDP 没有监听时触发)
    • Code 4: Fragmentation Needed but DF Set(PMTU 发现的依据)
  • Type 8, Code 0: Echo Request(ping 请求)
  • Type 11: Time Exceeded
    • Code 0: TTL Exceeded in Transit(traceroute 的依靠)

traceroute 原理:
traceroute 利用 TTL(Time to Live)字段和 ICMP Time Exceeded 消息来探测路由路径:

  1. 发送 TTL=1 的 UDP 包,第一个路由器收到后 TTL 减为 0,丢弃包并返回 ICMP Time Exceeded
  2. 发送 TTL=2 的包,到达第二个路由器后超时
  3. 重复直到到达目标主机(此时目标主机返回 ICMP Port Unreachable)

每次发送 3 个包(默认),记录每个 TTL 级别收到响应的 RTT。

7.4 BGP 基础 —— 互联网的路由协议

BGP(Border Gateway Protocol)是互联网的域间路由协议,几乎所有 ISP 之间的路由交换都通过 BGP 进行。

为什么需要 BGP?

  • 互联网由数以万计的 AS(自治系统)组成
  • 每个 AS 拥有独立的 IP 地址段和路由策略
  • 需要一个协议来在各 AS 之间交换可达性信息

BGP 的核心概念:

自治系统(AS, Autonomous System):
由一个机构管理的一组路由器的集合,使用相同的路由策略。每个 AS 有一个唯一的 AS 号(ASN)。例如:

  • AS15169(Google)
  • AS32934(Facebook)
  • AS4134(中国电信)
  • AS9808(中国移动)

路径向量(Path Vector):
BGP 使用路径向量算法(区别于 RIP 的距离向量和 OSPF 的链路状态)。每条路由通告都携带着完整的 AS 路径:

Network         Next Hop       Path
8.8.8.0/24 10.0.1.1 64500 15169

这表示要到达 8.8.8.0/24 网络,需要经过 AS64500 和 AS15169。

eBGP 与 iBGP:

  • eBGP(External BGP): 不同 AS 之间的 BGP 会话。默认 AD(管理距离)= 20
  • iBGP(Internal BGP): 同一 AS 内部的 BGP 会话。默认 AD = 200

BGP 路由选择过程:
BGP 不是选择”最短路径”,而是按优先级顺序比较以下属性:

  1. 最高的 Local Preference(本地优先级)
  2. 最短的 AS Path
  3. 最低的 Origin 类型(IGP < EGP < Incomplete)
  4. 最低的 MED(Multi-Exit Discriminator)
  5. eBGP 优先于 iBGP
  6. 最低的 IGP 成本到下一跳
  7. 最低的 Router ID

BGP 安全风险:

  • BGP 路由劫持: 恶意或错误的 AS 宣告不属于自己的 IP 前缀,导致流量被重定向(2018 年,某流量劫持事件导致全球部分 Google 服务中断)
  • RPKI(Resource Public Key Infrastructure): 通过加密签名验证路由来源的合法性。ROA(Route Origin Authorization)记录了哪个 AS 被授权宣告哪些 IP 前缀

八、链路层协议详解

8.1 以太网 —— 局域网的主宰

以太网(Ethernet, IEEE 802.3)是部署最广泛的局域网技术,从 1973 年 Bob Metcalfe 在 Xerox PARC 发明至今,经历了几十年的持续演进。

以太网帧格式:

以太网帧 (Ethernet II / DIX 格式):
┌─────────────────────────────────────────────────────────────┐
│ Preamble │ SFD │ Dst MAC│ Src MAC│ Type/│ Data │ FCS │
│ (7 bytes)│(1B) │ (6B) │ (6B) │ Len │ (46- │ (4B) │
│ │ │ │ │ (2B) │ 1500B) │ │
└─────────────────────────────────────────────────────────────┘

Preamble (前导码): 7 字节的 10101010 模式,用于时钟同步
SFD (帧首定界符): 1 字节的 10101011,标记帧的真正开始
MAC 地址: 6 字节,如 00:1A:2B:3C:4D:5E
Type / Length: 当值 ≤ 1500 时表示长度,> 1500 时表示上层协议类型
常见值: 0x0800=IPv4, 0x86DD=IPv6, 0x0806=ARP
FCS (帧校验序列): CRC-32 校验和,检测传输错误

最小帧长与冲突检测:
以太网的最小帧长(不含前导码和 SFD)是 64 字节。如果数据不足 46 字节,需要用填充(Padding)补齐。这个设计是为了确保在 CSMA/CD(载波监听多路访问/冲突检测)机制下,发送方能在发送完成前检测到冲突——信号从网络一端传播到另一端的时间(slot time)必须大于最小帧的发送时间。

以太网速率演进:

标准 速率 介质 编码
10BASE-T 10 Mbps Cat3 双绞线 曼彻斯特
100BASE-TX 100 Mbps Cat5 双绞线 4B/5B + MLT-3
1000BASE-T 1 Gbps Cat5e+ 双绞线 PAM-5 x 4 对线
10GBASE-T 10 Gbps Cat6a 双绞线 PAM-16 + LDPC
100GBASE-LR4 100 Gbps 单模光纤 4x25G WDM
400GBASE-FR4 400 Gbps 单模光纤 4x100G WDM

8.2 ARP —— 从 IP 到 MAC

ARP(Address Resolution Protocol, RFC 826)负责将 IPv4 地址解析为 MAC 地址。它工作在链路层,但服务于 IP 层。

ARP 报文格式:

ARP 报文(直接封装在以太网帧中,Type=0x0806):
┌──────────────────────────────────────────────────┐
│ Hardware Type: 0x0001 (以太网) │
│ Protocol Type: 0x0800 (IPv4) │
│ HW Addr Len: 6 │
│ Proto Addr Len: 4 │
│ Operation: 1=Request, 2=Reply │
│ Sender HW Addr: aa:bb:cc:dd:ee:ff │
│ Sender Proto Addr: 192.168.1.1 │
│ Target HW Addr: 00:00:00:00:00:00 (请求时为空) │
│ Target Proto Addr: 192.168.1.2 │
└──────────────────────────────────────────────────┘

ARP 工作流程:

  1. 主机 A 需要向 192.168.1.2 发送数据,但不知道其 MAC 地址
  2. A 发送 ARP Request(广播,目标 MAC 为 ff:ff:ff:ff:ff:ff):”谁有 192.168.1.2?告诉 192.168.1.1”
  3. 主机 B(192.168.1.2)收到后,单播 ARP Reply:192.168.1.2 的 MAC 是 bb:cc:dd:ee:ff:00
  4. A 将结果缓存到 ARP 表(arp -a 查看),用于后续通信

ARP 欺骗(ARP Spoofing):
攻击者发送伪造的 ARP Reply,将自己的 MAC 地址与网关 IP 绑定,从而实现中间人攻击(MITM)。防御手段包括:

  • 静态 ARP 绑定
  • DHCP Snooping + Dynamic ARP Inspection(交换机层面的防御)
  • ARP 代理(Proxy ARP)检测

8.3 802.11 WiFi —— 无线网络的基石

IEEE 802.11 定义了无线局域网的标准,它和有线以太网在链路层有显著差异。

802.11 的核心区别:

  • 半双工通信: 无线信号在同频段上无法同时收发
  • 冲突检测不可靠: 无线环境存在”隐藏节点问题”(Hidden Node Problem),两个节点都听不到对方,但它们的信号在接收端冲突
  • 使用 CSMA/CA(Collision Avoidance)代替 CSMA/CD: 通过 RTS/CTS(请求发送/允许发送)机制解决隐藏节点问题

802.11 帧结构:

802.11 数据帧:
┌────────────┬───────┬───┬───┬───┬──────┬───┬───────────┬─────┐
│Frame Ctrl │Duration│Addr1│Addr2│Addr3│Seq Ctl│Addr4│Frame Body│ FCS │
│ (2 bytes) │(2B) │(6B)│(6B)│(6B)│(2B) │(6B) │(0-2312B) │ (4B)│
└────────────┴───────┴───┴───┴───┴──────┘───┴───────────┴─────┘

Frame Control 字段子项:
- Protocol Version (2 bit)
- Type / Subtype (2+4 bit): 00=Management, 01=Control, 10=Data
- To DS / From DS (各 1 bit): 指示帧方向,决定 Addr1-Addr4 的含义
- More Fragments, Retry, Power Management, More Data, Protected, +HTC/Order

802.11 的四地址机制:
与以太网只有两个 MAC 地址(源/目标)不同,802.11 帧最多可包含 4 个地址:

  • Addr1: 接收端(下一跳的无线 MAC)
  • Addr2: 发送端(当前无线发送者的 MAC)
  • Addr3: 最终目标/原始源(DS 场景下的目的地址或源地址)
  • Addr4: 无线分布系统(WDS)中的桥接地址

802.11 代际演进:

代际 标准 发布年份 最大速率 频段 关键特性
WiFi 4 802.11n 2009 600 Mbps 2.4/5 GHz MIMO, 40 MHz 信道
WiFi 5 802.11ac 2013 6.9 Gbps 5 GHz MU-MIMO, 160 MHz, 256-QAM
WiFi 6 802.11ax 2019 9.6 Gbps 2.4/5/6 GHz OFDMA, 1024-QAM, BSS Coloring
WiFi 7 802.11be 2024 46 Gbps 2.4/5/6 GHz 320 MHz, 4096-QAM, MLO

九、数据封装过程:字节级逐步走读

让我们通过一个具体的例子——一台 Android 手机通过 WiFi 访问 https://tufusi.com——来追踪数据从应用到物理层的完整封装过程。

示例场景

应用层数据:HTTP/3 请求(QUIC STREAM 帧,实际数据 “GET / HTTP/3” 的某一部分)

Step 1: 应用层 → QUIC 帧

原始数据: "GET / HTTP/3" (11 字节)

QUIC STREAM 帧封装:
┌──────────────┬──────────────────────────────────┐
│ Type: 0x08 │ Stream ID: 0x04 (变长整数编码) │ Type 的低 2 bit = Stream Frame
│ Offset: 0 │ Length: 11 │
├──────────────┼──────────────────────────────────┤
│ Stream Data: "GET / HTTP/3" │
└──────────────────────────────────────────────────┘
(约 20 字节,含变长整数字段)

Step 2: QUIC 包封装

QUIC Short Header Packet (1-RTT 包):
┌────────┬──────────────────────────────────┬──────────────────────┐
│Header │ Destination Connection ID │ Packet Number │
│ 1 byte │ 可变长度 (8-20 字节) │ 1-4 字节 │
│ 0x41 │ 0x1234567890ABCDEF │ 0x00000042 │
├────────┴──────────────────────────────────┴──────────────────────┤
│ Protected Payload (加密后): │
│ ├── STREAM Frame: StreamID=4, Data="GET / HTTP/3" │
│ ├── PADDING Frame (可选) │
│ └── AEAD Authentication Tag (16 字节) │
└───────────────────────────────────────────────��──────────────────┘
总计约 60 字节

QUIC 的所有载荷在传输前都经过 TLS 1.3 AEAD 加密(AES-128-GCM 或 ChaCha20-Poly1305),外部观察者只能看到 Connection ID 和 Packet Number——这也包括包类型和标志位等需要保护的内容,QUIC 通过 Header Protection 机制使用独立的密钥加密包头中的敏感字段。

Step 3: UDP 封装

UDP 数据报:
┌──────────────────────┬─────────────────┬─────────────────────────┐
│ Src Port: 54321 (2B) │ Dst Port: 443 │ Length: 68 (2B) │
│ │ │ (= 8 + 60 QUIC payload) │
├──────────────────────┴─────────────────┼─────────────────────────┤
│ Checksum: 0xABCD (2B) │ │
├────────────────────────────────────────┤ │
│ QUIC Packet (60 bytes) │ │
└────────────────────────────────────────┴─────────────────────────┘
总计 68 字节

UDP 校验和是强制性的(在 IPv6 中)。它覆盖了 UDP 头部、UDP 数据以及 IPv4 伪首部(源 IP、目标 IP、协议号、UDP 长度),确保数据在传输中未被破坏。

Step 4: IP 封装

IPv4 数据报:
┌───────┬─────┬──────────────┬─────┬─────┬──────┬─────────────┬───────────────┐
│Vers=4 │IHL=5│DSCP+ECN (1B) │Total│Ident│Flags+│ TTL = 64 │Protocol=17 │
│ │(20B)│ │ Len │ (2B)│Frag │ │(UDP) │
│ │ │ │=88 │ │Offset│ │ │
├───────┴─────┼──────────────┴─────┴─────┼──────┴─────────────┼───────────────┤
│Header │Src IP: 192.168.1.10 (4B) │Dst IP: 93.184.216.34 (4B) │
│Checksum (2B)│ │ │
├─────────────┴─────────────────────────┴────────────────────────────────────┤
│ UDP Datagram (68 bytes) │
└─────────────────────────────────────────────────────────────────────────────┘
总计 88 字节 (20B IP 头 + 68B UDP + 数据)
  • IHL(Internet Header Length)= 5,表示头部为 20 字节(5 个 32 位字)
  • Protocol = 17,表示上层是 UDP(TCP 是 6,ICMP 是 1)
  • Total Length = 88(IP 头部 + UDP 数据报,单位:字节)

在实际的 Android/Linux 实现中,IP 层还涉及路由表查找。内核查询路由表(ip route show),确定该包应从哪个网络接口发出,以及下一跳 IP 地址。

Step 5: 以太网帧封装(WiFi 场景下更复杂,这里以有线以太网为例)

以太网帧(Ethernet II):
┌──────────────┬──────────────────┬────────────────┬──────┬────────────────┐
│ Dst MAC: │ Src MAC: │ EtherType: │ Pay │ FCS (CRC-32): │
│ 00:1A:2B:... │ aa:bb:cc:... │ 0x0800 (IPv4) │ load │ 0xABCD1234 │
│ (6 bytes) │ (6 bytes) │ (2 bytes) │ (88B)│ (4 bytes) │
└──────────────┴──────────────────┴────────────────┴──────┴────────────────┘
总计 106 字节

目标 MAC 地址通过 ARP 表查询下一跳 IP 得到:

  1. 查路由表:下一跳为 192.168.1.1(默认网关)
  2. 查 ARP 表:192.168.1.1 → 00:1A:2B:3C:4D:5E
  3. 如果 ARP 表未命中,发送 ARP Request 并在 ARP 缓存中等待结果

Step 6: 物理层编码(以 1000BASE-T 为例)

PAM-5 符号编码(每对线):
以太网帧比特流 → 线路编码 → 4D-PAM5 符号 → 模拟信号

1000BASE-T 在 4 对双绞线上同时传输:
- 每对线: 125 MBaud × 2 比特/符号 (PAM-5) = 250 Mbps
- 4 对线: 250 Mbps × 4 = 1000 Mbps
- 使用 Tomlinson-Harashima Precoding (THP) 消除码间干扰
- 使用回声消除实现全双工 (同一对线上同时收发)

封装层次总览

┌─────────────────────────────────────────────────────────────────┐
│ "GET / HTTP/3" │ ← 应用数据
├───────────────────────────────────────────��─────────────────────┤
│ QUIC STREAM Frame (Stream ID, Offset, Length, Data) │ ← QUIC 帧
├─────────────────────────────────────────────────────────────────┤
│ QUIC Packet (Header + Protected Payload + AEAD Tag) │ ← QUIC 包
├─────────────────────────────────────────────────────────────────┤
│ UDP Datagram (SrcPort, DstPort, Length, Checksum + payload) │ ← UDP 层
├─────────────────────────────────────────────────────────────────┤
│ IP Datagram (SrcIP, DstIP, TTL, Protocol + UDP payload) │ ← IP 层
├─────────────────────────────────────────────────────────────────┤
│ Ethernet Frame (DstMAC, SrcMAC, Type + IP payload + FCS) │ ← 链路层
├─────────────────────────────────────────────────────────────────┤
│ Electrical Signal on Cat5e Cable (PAM-5 modulated) │ ← 物理层
└─────────────────────────────────────────────────────────────────┘

接收端的过程完全相反——每层剥离自己的头部,验证校验和,将Payload交给上一层,直到应用层拿到原始数据。


十、Android 网络栈:从应用到内核

Android 的网络栈建立在 Linux 内核之上,但通过 Java/Kotlin 框架层进行了大量封装。以下从上到下追踪一个网络请求的完整路径。

10.1 应用层:OkHttp 架构

OkHttp 是 Android(和 Java)生态中最主流的 HTTP 客户端库,自 Android 4.4 起被 AOSP 内置使用。

OkHttp 核心组件:

┌──────────────────────────────────────────────┐
│ OkHttpClient │
│ (共享实例: 连接池、缓存、拦截器) │
├──────────────────────────────────────────────┤
│ Dispatcher │
│ (管理请求队列、线程池、并发控制) │
├──────────────────────────────────────────────┤
│ Interceptor Chain │
│ ┌──────────────────────────────────────┐ │
│ │ 自定义应用拦截器 │ │
│ │ RetryAndFollowUpInterceptor │ │
│ │ BridgeInterceptor │ │
│ │ CacheInterceptor │ │
│ │ ConnectInterceptor │ │
│ │ 自定义网络拦截器 │ │
│ │ CallServerInterceptor │ │
│ └──────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ Connection Pool │
│ (复用 HTTP/1.x 连接,HTTP/2 多路复用) │
└──────────────────────────────────────────────┘

拦截器链(Interceptor Chain):
这是 OkHttp 最优雅的设计。每个拦截器处理一个关注点:

  1. 应用拦截器: 用户的 addInterceptor(),可修改请求/响应,不感知重定向和重试
  2. RetryAndFollowUpInterceptor: 处理失败重试和 HTTP 重定向(最多 20 次)
  3. BridgeInterceptor: 将用户请求转换为 HTTP 请求(添加 Host、Content-Type、Cookie、User-Agent、Accept-Encoding 等头部)
  4. CacheInterceptor: 根据缓存策略响应(返回缓存、条件请求或穿透)
  5. ConnectInterceptor: 从连接池中获取或创建新连接
  6. 网络拦截器: 用户的 addNetworkInterceptor(),可观察底层交互
  7. CallServerInterceptor: 实际向服务器写入请求并读取响应

连接池(Connection Pool):

  • 每个地址(Host:Port)最多保持 5 个空闲连接(maxIdleConnections = 5
  • 空闲连接 5 分钟后关闭(keepAliveDurationNs = 5 分钟
  • HTTP/2 连接则复用同一个 TCP 连接进行多路复用

OkHttp 发起到完成的全流程(以一次 GET 请求为例):

1. client.newCall(request).enqueue(callback)
2. Dispatcher 将 Call 放入 readyCalls 队列
3. 当 runningCalls < maxRequests (64) 且 per-host < maxRequestsPerHost (5):
→ promoteAndExecute() 将 Call 移到 runningCalls
4. RealCall.execute() → getResponseWithInterceptorChain()
5. 经过完整的拦截器链,最终到达 CallServerInterceptor
6. ConnectInterceptor 调用 ExchangeFinder.find()
- 查询 ConnectionPool 是否有可用连接
- 若无, 通过 RouteSelector 选择路由, 创建 RealConnection
- RealConnection.connect() 执行 DNS 解析 + TCP 连接 + TLS 握手
7. CallServerInterceptor 通过 Exchange (管理 HTTP/1.x 序列化或 HTTP/2 流) 发送请求
8. 读取响应, 逐级返回给回调
9. 连接归还连接池 (如连接可复用)

10.2 系统服务层:ConnectivityManager

ConnectivityManager 是 Android Framework 提供的网络状态管理服务。

核心职责:

  • 管理网络的连接和断开事件
  • 为应用提供当前网络的能力信息(带宽、延迟、是否计费、是否经过 VPN)
  • 将应用的网络请求绑定到特定网络(如强制使用 WiFi 或蜂窝网络)

关键 API:

// 获取 ConnectivityManager
ConnectivityManager cm = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);

// 获取当前活跃网络
Network currentNetwork = cm.getActiveNetwork();

// 获取网络能力
NetworkCapabilities caps = cm.getNetworkCapabilities(currentNetwork);
boolean isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI);
boolean isCellular = caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
boolean isMetered = !caps.hasCapability(
NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
int downBandwidth = caps.getLinkDownstreamBandwidthKbps();
int upBandwidth = caps.getLinkUpstreamBandwidthKbps();

// 注册网络回调
cm.registerNetworkCallback(
new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build(),
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) { }
@Override
public void onLost(Network network) { }
@Override
public void onCapabilitiesChanged(
Network network, NetworkCapabilities caps) { }
});

网络绑定(Network Binding):
Android 允许应用将 socket 绑定到特定网络,避免默认网络切换导致的连接中断:

// 将 OkHttp 请求绑定到特定网络
OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(network.getSocketFactory())
.build();

这底层通过 Network.bindSocket()setsockopt()SO_BINDTODEVICE 或 Linux 的 fwmark(防火墙标记)将流量限制在指定网络接口上。

10.3 守护进程层:netd

netd(Network Daemon)是 Android 的本地网络守护进程,以 C++ 实现,运行在 root 权限下。它是 Android Framework 和 Linux 内核网络栈之间的桥梁。

netd 的核心职责:

  1. DNS 解析管理:

    • 维护每个网络的 DNS 服务器列表(/etc/resolv.conf 已不再直接使用)
    • 管理 DNS 缓存
    • 处理 Private DNS(DoT)配置
    • Android 9+ 默认使用 DoT(dns.google
  2. 网络路由管理:

    • 为每个网络(WiFi、蜂窝、VPN)维护独立的路由表
    • 使用 Linux 的 策略路由(Policy Routing):通过 ip rule 配合 fwmark 将不同应用的流量路由到不同的路由表
    • 处理网络切换时的路由更新
  3. 防火墙 / Tethering:

    • 管理 NAT 规则(iptables
    • 配置热点(SoftAP)的 DHCP 和转发规则
    • 带宽限制(Bandwidth Controller / tethering 限速)

netd 与 Framework 的通信:
Framework 通过 Binder IPC 调用 netd。关键的服务包括:

  • INetd 接口(Framework → netd)
  • NetdNativeService(AIDL 定义)

典型调用链示例(WiFi 连接时):

WifiService (Java)
→ ConnectivityService (Java)
→ NetworkManagementService (Java)
→ netd (C++, via Binder)
→ iptables / ip route / resolv.conf (内核/系统调用)

10.4 内核层:iptables 与 netfilter

iptables 是 Linux 内核 netfilter 框架的用户空间配置工具。Android 在每个网络接口变更时都会重新配置 iptables 规则。

netfilter 的 5 个钩子点(Hook Points):

网络包流向与 hook 点:
┌────────────┐
│ NF_IP_LOCAL_IN ◄── 发给本机的包
└──────▲──────┘

┌─────────┐ ┌──────┴──────┐ ┌──────────────┐ ┌─────────┐
│ 网络接口 │───►│NF_IP_PRE_ │───►│ 路由决策: │───►│NF_IP_ │───► 发送
│ (eth0/ │ │ROUTING │ │ 目标在哪? │ │FORWARD │
│ wlan0) │ └─────────────┘ └──┬───────┬────┘ └─────────┘
└─────────┘ │ │
发给本机 ◄────┘ └────► 非本机, 转发

┌──────────┴──────┐
│ NF_IP_POST_ │
│ ROUTING │
└──────────┬──────┘


网络接口发出

Android 中典型的 iptables 规则(简化版):

# NAT 表: 热点流量共享 (Tethering)
*nat
-A POSTROUTING -s 192.168.43.0/24 -o rmnet_data0 -j MASQUERADE

# Filter 表: 防火墙规则
*filter
# 阻止来自 WiFi 的特定流量
-A bw_INPUT -i wlan0 -j bw_global_alert
# 允许已建立/关联的连接
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Mangle 表: 数据包标记 (用于策略路由)
*mangle
# 给特定 UID 的流量打标记
-A OUTPUT -m owner --uid-owner 10086 -j MARK --set-mark 0x1

Android 的策略路由(Policy Routing)实现:

应用流量 → fwmark (依据 UID) → ip rule (匹配 fwmark) → 查特定路由表 → 走特定网络接口

示例:
ip rule add fwmark 0x1 table 100 # fwmark=1 查 table 100
ip route add default via 10.0.0.1 dev wlan0 table 100 # table 100 的默认路由走 WiFi

这意味着不同的应用(不同 UID)可以同时使用不同的网络接口——比如微信走 WiFi,其他应用走 4G。Android 的 ConnectivityManager.bindProcessToNetwork() 正是通过设置进程的 fwmark 来实现这一功能的。


十一、QUIC 深度剖析

11.1 为什么 Google 造了 QUIC?

Google 面临着全球最大规模的网络流量挑战。在 2012 年前后,Google 的工程师们意识到 TCP + TLS 的组合在以下场景中无法满足需求:

1. TCP 队头阻塞(Head-of-Line Blocking):
HTTP/2 通过多路复用解决了应用层的队头阻塞,但 TCP 层的队头阻塞依然存在。因为 TCP 保证所有字节严格有序交付——如果序列号为 S 的包丢失,即使序列号为 S+1、S+2… 的包已经到达,TCP 也不会将它们交付给应用层,必须等待 S 被重传。在 2% 丢包率的高延迟移动网络上,这可能意味着数百毫秒的额外延迟。

2. 连接建立的延迟:
TCP 三次握手(1-RTT)+ TLS 1.2 握手(2-RTT)= 3-RTT 才能开始传输数据。即使使用 TLS 1.3(1-RTT)也需要 2-RTT。对于谷歌搜索这种以毫秒计的场景,这是不可接受的。

3. 连接迁移困难:
TCP 连接由四元组(源 IP、源端口、目标 IP、目标端口)唯一标识。当用户从 WiFi 切换到 4G 时,IP 地址改变,TCP 连接必然中断,需要重新建立。对于视频通话等长时间连接,这是致命的。

4. 协议僵化(Ossification):
TCP 和 TLS 的中间件(防火墙、NAT、负载均衡器)对协议的更新极其缓慢。很多中间设备会丢弃它们不理解的新选项或扩展。Google 曾试图在 TCP 层面实验新的拥塞控制算法和选项字段,但经常被中间设备”踩死”。

5. 多路复用的复杂性:
TCP 只有一个字节流。要在应用层实现多路复用(如 HTTP/2),必须在上层构建帧和流——而这些流之间仍然受制于 TCP 的队头阻塞。

11.2 QUIC 的核心设计

在 UDP 之上重建传输层:
QUIC 选择基于 UDP 而非新建 IP 协议(如 SCTP 做的那样),原因很简单:UDP 是唯一能被所有中间设备识别和转发的传输层协议。尝试定义新的 IP 协议号,会在无数中间设备(防火墙、NAT、路由器)上被丢弃。

Connection ID —— 解耦连接与地址:
QUIC 使用 Connection ID(一个可变长度的不透明标识符)来标识连接,而不是传统的四元组。这带来了连接迁移能力:

  • 当客户端 IP 从 192.168.1.x(WiFi)变为 10.0.0.x(4G)时,QUIC 可以继续使用相同的 Connection ID
  • 服务器验证新地址的到达性(Path Validation),确认后无缝切换到新路径
  • 对应用层完全透明——视频通话不会中断,下载不会失败

多流架构 —— 彻底消除队头阻塞:
QUIC 在连接内支持多个独立的流(Stream)。每个流:

  • 有自己的独立序号空间
  • 丢失的包只阻塞该包所属的流,不影响其他流
  • 流之间可以无序交付
TCP + HTTP/2 的队头阻塞:
TCP Stream: [H2_Stream1_Frame1] [H2_Stream2_Frame1] [H2_Stream1_Frame2] ← TCP 丢包
← 整个连接暂停,等待重传,所有 HTTP/2 流都被阻塞

QUIC + HTTP/3 的无队头阻塞:
QUIC Stream 1: [Frame1] ---- [Frame2] ← Stream 1 丢包, 只阻塞 Stream 1
QUIC Stream 2: [Frame1] [Frame2] [Frame3] ← Stream 2 不受影响, 继续交付
QUIC Stream 3: [Frame1] [Frame2] ← Stream 3 也不受影响

内置加密 —— 不信任下层:
QUIC 将 TLS 1.3 直接嵌入协议内部。QUIC 包除了少量头部字段(Connection ID、Packet Number 部分)外,全部加密。这意味着:

  • 中间设备无法看到或修改 QUIC 的拥塞控制信号
  • 无法进行深度包检测(DPI)来识别具体流量
  • 协议升级(新版本)只需要修改加密载荷内部,中间设备无法阻止

0-RTT 握手:
QUIC 支持两种握手模式:

  • 1-RTT 握手(首次连接): 客户端发送初始包(ClientHello),服务器回复握手包(ServerHello + Certificate + Finished),1 个 RTT 后即可开始传输数据
  • 0-RTT 握手(重新连接): 利用之前连接缓存的服务端配置和 PSK(Pre-Shared Key),客户端在第一个飞行包中就携带应用数据

11.3 QUIC 的连接建立过程

QUIC 1-RTT 握手(首次连接):

Client                                           Server
│ │
│── Initial Packet ──────────────────────────► │
│ 包含: CRYPTO Frame (ClientHello) │
│ 临时 Connection ID (客户端选择的) │
│ QUIC version negotiation fields │
│ │
│ ◄── Initial Packet ────────── │
│ CRYPTO Frame (ServerHello)│
│ ◄── Handshake Packet ──────── │
│ CRYPTO Frame (EncryptedExtensions, │
│ Certificate, CertVerify, Finished) │
│ │
│── Handshake Packet ────────────────────────► │
│ CRYPTO Frame (Finished) │
│── 1-RTT Packet (Application Data) ────────► │ ← 握手完成, 应用数据开始传输
│ │

QUIC 使用不同的包类型和加密级别:

  • Initial Packet: 使用初始密钥(从目标 Connection ID 派生),保护最初的数据交换
  • Handshake Packet: 使用 TLS 握手派生的密钥
  • 1-RTT Packet(Short Header): 使用最终的应用数据密钥

QUIC 0-RTT 握手(重新连接):

Client                                           Server
│ │
│── Initial Packet ──────────────────────────► │
│ CRYPTO Frame (ClientHello + PSK) │
│── 0-RTT Packet ───────────────────────────► │ ← 在收到服务器响应前就发送
│ Application Data (0-RTT Early Data) │
│ │
│ ◄── Handshake Packet ──────── │
│ CRYPTO Frame (ServerHello + Finished) │
│ │
│── Handshake Packet (Finished) ─────────────► │
│── 1-RTT Packet (Application Data) ────────► │

0-RTT 数据在客户端发送 Initial 包之后立即发送,不等服务器响应。这几乎消除了重连的延迟——在理想情况下,应用数据延迟接近 0-RTT。但 0-RTT 数据存在重放攻击风险;服务器可以选择接受或拒绝 0-RTT 数据。

11.4 QUIC 的连接迁移

连接迁移是 QUIC 最强大的特性之一。它的工作原理如下:

场景: 用户正在 4G 网络上进行视频通话,走进办公室后切换到 WiFi。

传统 TCP + TLS 的困境:

  1. 4G 网络(IP_A)→ WiFi 网络(IP_B):客户端的 IP 地址变了
  2. TCP 四元组(IP_A, Port_A, Server_IP, 443)失效
  3. 必须重新:TCP 三次握手 + TLS 握手 + 应用层重新建立状态
  4. 视频通话中断数秒,用户感知明显

QUIC 的处理方式:

  1. 客户端检测到网络接口切换(IP_A → IP_B)
  2. 客户端从新地址(IP_B)发送 QUIC 包,使用相同的 Connection ID
  3. 服务器收到来自新 IP 的包,识别出 Connection ID,知道这是同一个连接
  4. 服务器执行路径验证:发送 PATH_CHALLENGE 帧到新地址,要求客户端响应 PATH_RESPONSE
  5. 验证通过后,服务器切换向新地址发送数据
  6. 应用层毫不知情——视频通话无缝继续

关键技术细节:

  • Connection ID 在握手过程中协商,双方可以各自选择不同的 Connection ID
  • 路径验证是可选的(取决于实现),但推荐在迁移时执行以防止流量放大攻击
  • 客户端可以同时尝试从多个路径发送数据(多路径 QUIC,正在标准化中)

11.5 QUIC 的拥塞控制

QUIC 在用户空间实现拥塞控制(而非在内核中,像 TCP 那样)。这带来两个巨大优势:

  1. 可快速迭代: 不需要升级操作系统内核,应用升级即可更换拥塞控制算法
  2. 可定制化: 不同应用可以使用不同的拥塞控制策略

QUIC 规范(RFC 9002)描述了一种基于 New Reno 的拥塞控制器,但实际部署中 Google 使用 BBRv2,其他实现(如 Cloudflare 的 quiche)也支持 CUBIC。

QUIC 拥塞控制的关键信号:

  • ACK 包: 包含包的接收时间和 ECN 信息
  • 丢包检测: 基于包序号和 ACK 信息(不同于 TCP 的重复 ACK)
  • RTT 测量: 每个 ACK 帧都包含 ack_delay 字段(接收方处理延迟),实现更精确的 RTT 估计

11.6 QUIC vs TCP+TLS 性能对比

维度 TCP + TLS 1.3 QUIC
首次连接延迟 2-RTT (TCP握手 + TLS握手) 1-RTT
重连延迟 2-RTT 0-RTT (如 PSK 有效)
队头阻塞 TCP层存在 完全消除
连接迁移 不支持 原生支持
实现位置 内核 (TCP) + 用户空间 (TLS) 完全在用户空间
协议升级 极慢 (中间设备阻�) 快速 (载荷加密)
头部开销 TCP 20B + TLS ~25B = 45B 加密头部 ~20B
NAT 穿透 复杂 基于 UDP, 复用现有 NAT 机制
CPU 开销 硬件卸载 (TCP) + 软件加密 (TLS) 软件 (无硬件卸载) → 更高 CPU 占用

QUIC 的主要劣势是 CPU 开销——因为它在用户空间实现,无法利用网卡的 TCP Segmentation Offload(TSO)等硬件加速。但随着 CPU 性能提升和 QUIC 实现的优化(如 GSO, Generic Segmentation Offload),这个差距正在缩小。


十二、Socket 编程:从理论到实践

12.1 BSD Socket API

Socket(套接字)是操作系统提供给应用程序的网络编程接口。它起源于 1983 年的 4.2BSD Unix,至今仍是所有主流操作系统的标准网络 API。

核心系统调用:

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

// TCP 服务器端
int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建 socket
// AF_INET: IPv4, SOCK_STREAM: TCP (面向流), 0: 自动选择协议

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // 端口号,主机字节序→网络字节序
addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)); // 绑定地址

listen(server_fd, 128); // 开始监听,backlog=128

struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr,
&client_len); // 接受连接,返回新的 socket fd

char buf[4096];
ssize_t n = recv(client_fd, buf, sizeof(buf), 0); // 接收数据
send(client_fd, "HTTP/1.1 200 OK\r\n...", 19, 0); // 发送数据

close(client_fd); // 关闭连接
close(server_fd);

// TCP 客户端
int fd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(80);
inet_pton(AF_INET, "93.184.216.34", &server_addr.sin_addr);
connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

send(fd, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", 38, 0);
recv(fd, buf, sizeof(buf), 0);

close(fd);

socket() 的参数组合:

domain type protocol 含义
AF_INET SOCK_STREAM 0 IPv4 TCP
AF_INET SOCK_DGRAM 0 IPv4 UDP
AF_INET6 SOCK_STREAM 0 IPv6 TCP
AF_INET6 SOCK_DGRAM 0 IPv6 UDP
AF_UNIX SOCK_STREAM 0 Unix Domain Socket(本地进程间通信)
AF_PACKET SOCK_RAW htons(ETH_P_ALL) 原始套接字(链路层数据包,需要 CAP_NET_RAW)

12.2 非阻塞 I/O

默认情况下,recv()accept()connect() 等调用都是阻塞的——如果数据尚未到达,调用线程会被挂起,直到条件满足或超时。这对高并发服务器是不可接受的。

将 socket 设置为非阻塞模式:

int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

// 此后 recv() 如果没有数据立即返回 -1,errno = EAGAIN/EWOULDBLOCK
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// 暂时没有数据,去做别的事
}

I/O 多路复用的演进:

select(1983,4.2BSD):

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);

struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
int ready = select(max_fd + 1, &readfds, NULL, NULL, &tv);
if (ready > 0) {
if (FD_ISSET(fd1, &readfds)) { /* fd1 可读 */ }
if (FD_ISSET(fd2, &readfds)) { /* fd2 可读 */ }
}

select 的局限性:

  • fd_set 大小固定(FD_SETSIZE,默认 1024),限制了最大并发连接数
  • 每次调用需要将 fd_set 从用户空间复制到内核空间,O(n) 开销
  • 返回时需要遍历所有 fd 来找到就绪的,O(n) 开销
  • 每次调用后 fd_set 被修改,下次使用前需要重新设置

poll(1986,System V):

struct pollfd fds[MAX_CONNS];
fds[0].fd = fd1;
fds[0].events = POLLIN; // 关注可读事件
fds[1].fd = fd2;
fds[1].events = POLLIN;

int ready = poll(fds, num_fds, 5000); // 5000ms 超时
if (ready > 0) {
for (int i = 0; i < num_fds; i++) {
if (fds[i].revents & POLLIN) {
// fd 可读
}
}
}

poll 相比 select 的改进:

  • 没有 FD_SETSIZE 限制,可以处理任意数量 fd
  • 使用独立的 events(输入)和 revents(输出)字段,无需每次重建

但 poll 仍然是 O(n) 的: 每次调用都要复制整个 fds 数组到内核空间,仍需遍历所有 fd。

12.3 epoll —— Linux 高性能网络编程的核心

epoll(event poll)是 Linux 2.6 引入的高性能 I/O 事件通知机制,专为大规模并发连接设计。它是 nginx、Redis、Node.js 等高性能服务器的基础。

epoll 的三个核心系统调用:

#include <sys/epoll.h>

// 1. 创建 epoll 实例
int epfd = epoll_create1(0); // Linux 2.6.8+, 代替 epoll_create(int size)
// size 参数已被废弃, 内核动态分配

// 2. 注册 fd
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 关注可读事件,使用边缘触发
ev.data.fd = server_fd; // 可以存储 fd 或指针
epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);
// 操作: EPOLL_CTL_ADD / EPOLL_CTL_MOD / EPOLL_CTL_DEL

// 3. 等待事件
#define MAX_EVENTS 1024
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms);
// timeout_ms = -1: 无限阻塞; 0: 立即返回; >0: 超时 (毫秒)

for (int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
uint32_t revents = events[i].events;

if (revents & EPOLLIN) {
// fd 可读
}
if (revents & EPOLLOUT) {
// fd 可写
}
if (revents & (EPOLLERR | EPOLLHUP)) {
// fd 出错或挂断
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
}

边缘触发(Edge-Triggered, ET)vs 水平触发(Level-Triggered, LT):

水平触发(LT,默认模式):

  • 只要 fd 处于就绪状态(如缓冲区有数据),每次 epoll_wait() 都会返回该 fd
  • 与 poll/select 的行为一致,最容易理解和使用
  • 适合处理得慢的场景

边缘触发(ET,高性能模式):

  • fd 从”未就绪”变为”就绪”时,epoll_wait() 才会返回该 fd(仅通知一次
  • 如果应用没有读完所有数据,不会再收到通知——除非新数据到达触发新事件
  • 必须使用非阻塞 I/O + 循环读写直到 EAGAIN,否则会丢事件
  • 减少了事件通知次数,适合高性能场景

边缘触发的正确读模式:

// ET 模式的正确读取方式
int fd = ...; // 已设为 O_NONBLOCK,并注册为 EPOLLIN | EPOLLET
char buf[4096];
while (1) {
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n > 0) {
// 处理数据
} else if (n == 0) {
// 对端关闭连接
close(fd);
break;
} else { // n == -1
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 缓冲区已空,没有更多数据,退出循环
break;
} else {
// 真正的错误
close(fd);
break;
}
}
}

epoll 的高效原理:

epoll 之所以能高效处理数万个并发连接,核心在于:

  1. 事件驱动而非轮询: 内核在 fd 就绪时主动将事件加入就绪列表(ready list),epoll_wait() 只需返回这个列表,无需遍历所有 fd。时间复杂度 O(1) vs select/poll 的 O(n)。

  2. 使用红黑树存储注册的 fd: epoll_ctl() 添加/删除 fd 时,内核将该 fd 插入/移除红黑树,时间复杂度 O(log n)。当设备驱动通知数据到达时,内核通过红黑树快速找到对应的 epoll 条目。

  3. 回调机制: 注册 fd 时,内核在对应的设备驱动中设置回调函数。当数据到达时,硬件中断 → 驱动 → 唤醒等待队列上的进程 → 将就绪 fd 放入就绪列表。整个过程不需要应用层反复轮询。

epoll 性能基准:
在典型的测试中(10000 个空闲连接 + 100 个活跃连接):

  • select:每次调用需要 ~30 微秒(随连接总数线性增长)
  • epoll:每次调用需要 ~3 微秒(只取决于活跃连接数)

这就是为什么 nginx 能在一台机器上处理数十万并发连接,而基于 select 的 Apache prefork 模式只能处理数百。

完整的 epoll TCP 服务器示例:

#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define MAX_EVENTS 1024
#define PORT 8080

// 设置 socket 为非阻塞
static void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
set_nonblocking(listen_fd);

int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY
};
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, SOMAXCONN);

int epfd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

struct epoll_event events[MAX_EVENTS];

while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);

for (int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;

if (fd == listen_fd) {
// 新连接
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd,
(struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
break;
}
set_nonblocking(client_fd);
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
}
} else {
// 客户端数据
char buf[4096];
while (1) {
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n > 0) {
// 回显 (echo)
send(fd, buf, n, 0);
} else if (n == 0) {
// 对端关闭
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
}
}
}
}

close(listen_fd);
close(epfd);
return 0;
}

这个简单的 epoll echo 服务器可以高效处理数万并发 TCP 连接,其背后的设计思想——事件驱动、非阻塞 I/O、内核级的多路复用——正是现代高性能网络服务(nginx、Envoy、Netty、tokio)的共同基础。


十三、总结

从物理层的一道电信号到应用层的一个 HTTP 响应,从上世纪 70 年代的 ARPANET 到今天的 QUIC/HTTP3,网络分层模型始终是我们理解、设计和优化网络系统的核心思维框架。

核心要点回顾:

  1. 分层是网络设计的核心哲学: 每层独立演进,向上屏蔽复杂性,向下抽象差异性。

  2. OSI 七层模型是理论基准,TCP/IP 四层模型是工程现实: 理解两者之间的映射关系,有助于在理论和实践之间自如切换。

  3. 应用层协议各有所长: HTTP/1.1 简洁但低效,HTTP/2 解决了应用层队头阻塞,HTTP/3 基于 QUIC 彻底消除队头阻塞并支持连接迁移。

  4. TCP 的状态机和拥塞控制是网络编程的基础知识——诊断连接泄露(CLOSE-WAIT)和端口耗尽(TIME-WAIT)是生产环境中的常见技能。

  5. QUIC 代表了传输层协议的演进方向: 在 UDP 之上重建传输层,通过 Connection ID 支持连接迁移,通过多流架构彻底消除队头阻塞。

  6. Android 的网络栈从应用层(OkHttp)→ 系统服务层(ConnectivityManager)→ 守护进程层(netd)→ 内核层(iptables/netfilter),每一层都有明确的职责和性能考量。

  7. Socket 编程是连接理论和实践的桥梁: 从 BSD socket API 到 select/poll 再到 epoll,理解 I/O 模型的演进有助于写出高性能的网络服务。

在移动网络环境日益复杂(5G、边缘计算、卫星互联网)的今天,深入理解 TCP/IP 协议栈的分层架构和具体协议的运作机理,不仅能帮助开发者写出更健壮的网络代码,更能让你在面对网络故障时有条不紊地逐层排查——这正是”分层”思维给予我们的核心力量。

打赏
  • 微信
  • 支付宝

评论