一、为什么需要分层模型
计算机网络是一个极其复杂的系统。设想一下,你需要让两台位于地球两端的机器可靠地交换数据,这中间涉及物理信号传输、数据编码、路由选择、拥塞控制、应用协议解析等无数细节。如果不加以抽象,任何开发者都无法完整理解并实现一个网络应用。
分层模型的核心思想是:将复杂的网络通信过程拆解为若干个独立但又相互协作的层次。每一层:
- 为上一层提供服务
- 使用下一层提供的服务
- 只关心本层的职责,不关心其他层的内部实现
这种设计哲学带来了三大好处:
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 对线上同时传输)。
第 2 层:数据链路层(Data Link Layer)
数据链路层解决了在相邻节点之间可靠传输帧的问题。它把物理层不可靠的比特流变成了有结构的、可验证的数据帧。
数据链路层又分为两个子层:
- 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 四层(自上而下):
┌─────────────────────────────────┐ |
3.1 网络接口层(Link Layer / Network Interface Layer)
这一层对应于 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): 用于诊断和错误报告(
ping、traceroute) - IGMP(Internet Group Management Protocol): 管理多播组成员
- ARP(Address Resolution Protocol): IPv4 地址到 MAC 地址的映射
3.3 传输层(Transport Layer)
传输层在 IP 层提供的”主机到主机”通信基础上,实现了**”进程到进程”的通信**。它使用端口号来区分同一主机上的不同进程。
TCP/IP 协议族中最重要的两个传输层协议是 TCP 和 UDP。此外,近年来 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 协议族 |
| 实际使用 | 教学参考 | 互联网实际标准 |
关键差异解析:
层数差异的本质。 OSI 之所以比 TCP/IP 多三层,是因为它将”会话管理”和”数据表示”从应用层中独立出来了。这在理论上很美——关注点分离——但在实践中,大多数应用协议(HTTP、SMTP、FTP)已经内建了这些功能,拆分反而增加了不必要的复杂性。
协议设计的哲学不同。 OSI 倾向于”先设计完美的模型,再填充协议”(自上而下)。TCP/IP 则遵循”先写出能工作的代码,再抽象为模型”(自下而上)。后者更务实,也更符合互联网的演进方式。
物理层与链路层分合的考量。 OSI 将物理层和链路层分开,强调了信号传输和帧传输是两个不同层面的事情。TCP/IP 将它们合并,因为在实际工程中,这两层的实现通常紧密耦合(例如以太网卡驱动同时处理物理信号和 MAC 帧)。
跨层设计(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): |
管道化(Pipelining):
客户端可以在收到前一个响应之前发送后续请求。理论上这能减少延迟,但实践中由于队头阻塞(Head-of-Line Blocking)问题——第一个请求的响应如果慢,会阻塞后续所有响应的传递——大多数浏览器默认禁用了管道化。
分块传输编码(Chunked Transfer Encoding):Transfer-Encoding: chunked 允许服务器在不知道内容总长度的情况下开始发送响应。这对于动态生成的内容、流式数据非常有用。
HTTP/1.1 200 OK |
缓存控制:
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 帧结构: |
帧类型:
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 |
服务器推送在实践中效果有限——浏览器可能已有缓存,过度推送会浪费带宽。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 记录类型:
- 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 记录): |
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(两个往返时延):
- ClientHello → ServerHello + Certificate + ServerKeyExchange + ServerHelloDone(0.5 RTT)
- ClientKeyExchange + ChangeCipherSpec + Finished → ChangeCipherSpec + Finished(0.5 RTT)
此时才能开始发送应用数据。在 100ms RTT 的网络上,光握手就需要 200ms。
TLS 1.3 的 1-RTT 握手:
Client Server |
TLS 1.3 将握手精简为 1-RTT:
- 客户端在
ClientHello中直接发送密钥交换的公钥(不再协商后再交换) - 服务器在第二个飞行包中同时发送 ServerHello + 证书 + 验证 + Finished
- 客户端验证后立即发送 Finished 和应用数据
TLS 1.3 的 0-RTT 握手(Early Data / PSK 模式):
如果客户端之前与服务器建立过连接并保存了 PSK(Pre-Shared Key),它可以在第一个飞行包中就发送应用数据:
Client Server |
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):
客户端请求: |
Sec-WebSocket-Accept 的计算方式:将客户端 Sec-WebSocket-Key 与固定 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接,计算 SHA-1 哈希,再 Base64 编码。这确保了服务器确实理解 WebSocket 协议。
WebSocket 帧格式:
WebSocket 帧结构: |
- 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(监听)
服务器端调用 listen() 后进入此状态,等待客户端的连接请求。在 Linux 内核中,LISTEN 状态的 socket 维护两个队列:
- SYN 队列(半连接队列): 存放收到 SYN 但尚未完成三次握手的连接。队列大小由
net.ipv4.tcp_max_syn_backlog和somaxconn控制。当队列满时,新的 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?
- 确保最后的 ACK 能到达对端。 如果最后一个 ACK 丢失,对端会重传 FIN,主动方需要能够重传 ACK。
- 防止旧连接的数据包干扰新连接。 让网络中所有属于此连接的”游荡”数据包在网络中超时。
高并发服务器上可能出现大量 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 |
- 无连接: 不需要握手,直接发送
- 无状态: 不需要维护连接状态
- 不保证交付: 丢包不重传
- 不保证顺序: 后发的包可能先到
- 不拥塞控制: 可以按任意速率发送
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 穿透问题: 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 |
IPv6 头部格式:
IPv6 头部(固定的 40 字节): |
IPv4 vs IPv6 头部对比:
- IPv4 头部有 13 个字段(含选项),IPv6 只有 8 个
- IPv4 头部的校验和、标识、标志、分片偏移等字段在 IPv6 中被移除
- 这些简化让路由器处理 IPv6 包的效率显著提升
7.3 ICMP —— 互联网诊断协议
ICMP(Internet Control Message Protocol)是 IP 层的辅助协议,用于传递错误信息和网络诊断。
ICMP 报文结构:
ICMP 报文(封装在 IP 数据报中): |
常用 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 消息来探测路由路径:
- 发送 TTL=1 的 UDP 包,第一个路由器收到后 TTL 减为 0,丢弃包并返回 ICMP Time Exceeded
- 发送 TTL=2 的包,到达第二个路由器后超时
- 重复直到到达目标主机(此时目标主机返回 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 网络,需要经过 AS64500 和 AS15169。
eBGP 与 iBGP:
- eBGP(External BGP): 不同 AS 之间的 BGP 会话。默认 AD(管理距离)= 20
- iBGP(Internal BGP): 同一 AS 内部的 BGP 会话。默认 AD = 200
BGP 路由选择过程:
BGP 不是选择”最短路径”,而是按优先级顺序比较以下属性:
- 最高的 Local Preference(本地优先级)
- 最短的 AS Path
- 最低的 Origin 类型(IGP < EGP < Incomplete)
- 最低的 MED(Multi-Exit Discriminator)
- eBGP 优先于 iBGP
- 最低的 IGP 成本到下一跳
- 最低的 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 格式): |
最小帧长与冲突检测:
以太网的最小帧长(不含前导码和 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): |
ARP 工作流程:
- 主机 A 需要向 192.168.1.2 发送数据,但不知道其 MAC 地址
- A 发送 ARP Request(广播,目标 MAC 为
ff:ff:ff:ff:ff:ff):”谁有 192.168.1.2?告诉 192.168.1.1” - 主机 B(192.168.1.2)收到后,单播 ARP Reply:
192.168.1.2 的 MAC 是 bb:cc:dd:ee:ff:00 - 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 数据帧: |
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 字节) |
Step 2: QUIC 包封装
QUIC Short Header Packet (1-RTT 包): |
QUIC 的所有载荷在传输前都经过 TLS 1.3 AEAD 加密(AES-128-GCM 或 ChaCha20-Poly1305),外部观察者只能看到 Connection ID 和 Packet Number——这也包括包类型和标志位等需要保护的内容,QUIC 通过 Header Protection 机制使用独立的密钥加密包头中的敏感字段。
Step 3: UDP 封装
UDP 数据报: |
UDP 校验和是强制性的(在 IPv6 中)。它覆盖了 UDP 头部、UDP 数据以及 IPv4 伪首部(源 IP、目标 IP、协议号、UDP 长度),确保数据在传输中未被破坏。
Step 4: IP 封装
IPv4 数据报: |
- 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): |
目标 MAC 地址通过 ARP 表查询下一跳 IP 得到:
- 查路由表:下一跳为 192.168.1.1(默认网关)
- 查 ARP 表:192.168.1.1 → 00:1A:2B:3C:4D:5E
- 如果 ARP 表未命中,发送 ARP Request 并在 ARP 缓存中等待结果
Step 6: 物理层编码(以 1000BASE-T 为例)
PAM-5 符号编码(每对线): |
封装层次总览
┌─────────────────────────────────────────────────────────────────┐ |
接收端的过程完全相反——每层剥离自己的头部,验证校验和,将Payload交给上一层,直到应用层拿到原始数据。
十、Android 网络栈:从应用到内核
Android 的网络栈建立在 Linux 内核之上,但通过 Java/Kotlin 框架层进行了大量封装。以下从上到下追踪一个网络请求的完整路径。
10.1 应用层:OkHttp 架构
OkHttp 是 Android(和 Java)生态中最主流的 HTTP 客户端库,自 Android 4.4 起被 AOSP 内置使用。
OkHttp 核心组件:
┌──────────────────────────────────────────────┐ |
拦截器链(Interceptor Chain):
这是 OkHttp 最优雅的设计。每个拦截器处理一个关注点:
- 应用拦截器: 用户的
addInterceptor(),可修改请求/响应,不感知重定向和重试 - RetryAndFollowUpInterceptor: 处理失败重试和 HTTP 重定向(最多 20 次)
- BridgeInterceptor: 将用户请求转换为 HTTP 请求(添加 Host、Content-Type、Cookie、User-Agent、Accept-Encoding 等头部)
- CacheInterceptor: 根据缓存策略响应(返回缓存、条件请求或穿透)
- ConnectInterceptor: 从连接池中获取或创建新连接
- 网络拦截器: 用户的
addNetworkInterceptor(),可观察底层交互 - CallServerInterceptor: 实际向服务器写入请求并读取响应
连接池(Connection Pool):
- 每个地址(Host:Port)最多保持 5 个空闲连接(
maxIdleConnections = 5) - 空闲连接 5 分钟后关闭(
keepAliveDurationNs = 5 分钟) - HTTP/2 连接则复用同一个 TCP 连接进行多路复用
OkHttp 发起到完成的全流程(以一次 GET 请求为例):
1. client.newCall(request).enqueue(callback) |
10.2 系统服务层:ConnectivityManager
ConnectivityManager 是 Android Framework 提供的网络状态管理服务。
核心职责:
- 管理网络的连接和断开事件
- 为应用提供当前网络的能力信息(带宽、延迟、是否计费、是否经过 VPN)
- 将应用的网络请求绑定到特定网络(如强制使用 WiFi 或蜂窝网络)
关键 API:
// 获取 ConnectivityManager |
网络绑定(Network Binding):
Android 允许应用将 socket 绑定到特定网络,避免默认网络切换导致的连接中断:
// 将 OkHttp 请求绑定到特定网络 |
这底层通过 Network.bindSocket() → setsockopt() → SO_BINDTODEVICE 或 Linux 的 fwmark(防火墙标记)将流量限制在指定网络接口上。
10.3 守护进程层:netd
netd(Network Daemon)是 Android 的本地网络守护进程,以 C++ 实现,运行在 root 权限下。它是 Android Framework 和 Linux 内核网络栈之间的桥梁。
netd 的核心职责:
DNS 解析管理:
- 维护每个网络的 DNS 服务器列表(
/etc/resolv.conf已不再直接使用) - 管理 DNS 缓存
- 处理 Private DNS(DoT)配置
- Android 9+ 默认使用 DoT(
dns.google)
- 维护每个网络的 DNS 服务器列表(
网络路由管理:
- 为每个网络(WiFi、蜂窝、VPN)维护独立的路由表
- 使用 Linux 的 策略路由(Policy Routing):通过
ip rule配合 fwmark 将不同应用的流量路由到不同的路由表 - 处理网络切换时的路由更新
防火墙 / Tethering:
- 管理 NAT 规则(
iptables) - 配置热点(SoftAP)的 DHCP 和转发规则
- 带宽限制(Bandwidth Controller / tethering 限速)
- 管理 NAT 规则(
netd 与 Framework 的通信:
Framework 通过 Binder IPC 调用 netd。关键的服务包括:
INetd接口(Framework → netd)NetdNativeService(AIDL 定义)
典型调用链示例(WiFi 连接时):
WifiService (Java) |
10.4 内核层:iptables 与 netfilter
iptables 是 Linux 内核 netfilter 框架的用户空间配置工具。Android 在每个网络接口变更时都会重新配置 iptables 规则。
netfilter 的 5 个钩子点(Hook Points):
网络包流向与 hook 点: |
Android 中典型的 iptables 规则(简化版):
# NAT 表: 热点流量共享 (Tethering) |
Android 的策略路由(Policy Routing)实现:
应用流量 → fwmark (依据 UID) → ip rule (匹配 fwmark) → 查特定路由表 → 走特定网络接口 |
这意味着不同的应用(不同 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 的队头阻塞: |
内置加密 —— 不信任下层:
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 |
QUIC 使用不同的包类型和加密级别:
- Initial Packet: 使用初始密钥(从目标 Connection ID 派生),保护最初的数据交换
- Handshake Packet: 使用 TLS 握手派生的密钥
- 1-RTT Packet(Short Header): 使用最终的应用数据密钥
QUIC 0-RTT 握手(重新连接):
Client Server |
0-RTT 数据在客户端发送 Initial 包之后立即发送,不等服务器响应。这几乎消除了重连的延迟——在理想情况下,应用数据延迟接近 0-RTT。但 0-RTT 数据存在重放攻击风险;服务器可以选择接受或拒绝 0-RTT 数据。
11.4 QUIC 的连接迁移
连接迁移是 QUIC 最强大的特性之一。它的工作原理如下:
场景: 用户正在 4G 网络上进行视频通话,走进办公室后切换到 WiFi。
传统 TCP + TLS 的困境:
- 4G 网络(IP_A)→ WiFi 网络(IP_B):客户端的 IP 地址变了
- TCP 四元组(IP_A, Port_A, Server_IP, 443)失效
- 必须重新:TCP 三次握手 + TLS 握手 + 应用层重新建立状态
- 视频通话中断数秒,用户感知明显
QUIC 的处理方式:
- 客户端检测到网络接口切换(IP_A → IP_B)
- 客户端从新地址(IP_B)发送 QUIC 包,使用相同的 Connection ID
- 服务器收到来自新 IP 的包,识别出 Connection ID,知道这是同一个连接
- 服务器执行路径验证:发送 PATH_CHALLENGE 帧到新地址,要求客户端响应 PATH_RESPONSE
- 验证通过后,服务器切换向新地址发送数据
- 应用层毫不知情——视频通话无缝继续
关键技术细节:
- Connection ID 在握手过程中协商,双方可以各自选择不同的 Connection ID
- 路径验证是可选的(取决于实现),但推荐在迁移时执行以防止流量放大攻击
- 客户端可以同时尝试从多个路径发送数据(多路径 QUIC,正在标准化中)
11.5 QUIC 的拥塞控制
QUIC 在用户空间实现拥塞控制(而非在内核中,像 TCP 那样)。这带来两个巨大优势:
- 可快速迭代: 不需要升级操作系统内核,应用升级即可更换拥塞控制算法
- 可定制化: 不同应用可以使用不同的拥塞控制策略
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。
核心系统调用:
|
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); |
I/O 多路复用的演进:
select(1983,4.2BSD):
fd_set readfds; |
select 的局限性:
fd_set大小固定(FD_SETSIZE,默认 1024),限制了最大并发连接数- 每次调用需要将
fd_set从用户空间复制到内核空间,O(n) 开销 - 返回时需要遍历所有 fd 来找到就绪的,O(n) 开销
- 每次调用后
fd_set被修改,下次使用前需要重新设置
poll(1986,System V):
struct pollfd fds[MAX_CONNS]; |
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 的三个核心系统调用:
|
边缘触发(Edge-Triggered, ET)vs 水平触发(Level-Triggered, LT):
水平触发(LT,默认模式):
- 只要 fd 处于就绪状态(如缓冲区有数据),每次
epoll_wait()都会返回该 fd - 与 poll/select 的行为一致,最容易理解和使用
- 适合处理得慢的场景
边缘触发(ET,高性能模式):
- fd 从”未就绪”变为”就绪”时,
epoll_wait()才会返回该 fd(仅通知一次) - 如果应用没有读完所有数据,不会再收到通知——除非新数据到达触发新事件
- 必须使用非阻塞 I/O + 循环读写直到 EAGAIN,否则会丢事件
- 减少了事件通知次数,适合高性能场景
边缘触发的正确读模式:
// ET 模式的正确读取方式 |
epoll 的高效原理:
epoll 之所以能高效处理数万个并发连接,核心在于:
事件驱动而非轮询: 内核在 fd 就绪时主动将事件加入就绪列表(ready list),
epoll_wait()只需返回这个列表,无需遍历所有 fd。时间复杂度 O(1) vs select/poll 的 O(n)。使用红黑树存储注册的 fd:
epoll_ctl()添加/删除 fd 时,内核将该 fd 插入/移除红黑树,时间复杂度 O(log n)。当设备驱动通知数据到达时,内核通过红黑树快速找到对应的 epoll 条目。回调机制: 注册 fd 时,内核在对应的设备驱动中设置回调函数。当数据到达时,硬件中断 → 驱动 → 唤醒等待队列上的进程 → 将就绪 fd 放入就绪列表。整个过程不需要应用层反复轮询。
epoll 性能基准:
在典型的测试中(10000 个空闲连接 + 100 个活跃连接):
- select:每次调用需要 ~30 微秒(随连接总数线性增长)
- epoll:每次调用需要 ~3 微秒(只取决于活跃连接数)
这就是为什么 nginx 能在一台机器上处理数十万并发连接,而基于 select 的 Apache prefork 模式只能处理数百。
完整的 epoll TCP 服务器示例:
|
这个简单的 epoll echo 服务器可以高效处理数万并发 TCP 连接,其背后的设计思想——事件驱动、非阻塞 I/O、内核级的多路复用——正是现代高性能网络服务(nginx、Envoy、Netty、tokio)的共同基础。
十三、总结
从物理层的一道电信号到应用层的一个 HTTP 响应,从上世纪 70 年代的 ARPANET 到今天的 QUIC/HTTP3,网络分层模型始终是我们理解、设计和优化网络系统的核心思维框架。
核心要点回顾:
分层是网络设计的核心哲学: 每层独立演进,向上屏蔽复杂性,向下抽象差异性。
OSI 七层模型是理论基准,TCP/IP 四层模型是工程现实: 理解两者之间的映射关系,有助于在理论和实践之间自如切换。
应用层协议各有所长: HTTP/1.1 简洁但低效,HTTP/2 解决了应用层队头阻塞,HTTP/3 基于 QUIC 彻底消除队头阻塞并支持连接迁移。
TCP 的状态机和拥塞控制是网络编程的基础知识——诊断连接泄露(CLOSE-WAIT)和端口耗尽(TIME-WAIT)是生产环境中的常见技能。
QUIC 代表了传输层协议的演进方向: 在 UDP 之上重建传输层,通过 Connection ID 支持连接迁移,通过多流架构彻底消除队头阻塞。
Android 的网络栈从应用层(OkHttp)→ 系统服务层(ConnectivityManager)→ 守护进程层(netd)→ 内核层(iptables/netfilter),每一层都有明确的职责和性能考量。
Socket 编程是连接理论和实践的桥梁: 从 BSD socket API 到 select/poll 再到 epoll,理解 I/O 模型的演进有助于写出高性能的网络服务。
在移动网络环境日益复杂(5G、边缘计算、卫星互联网)的今天,深入理解 TCP/IP 协议栈的分层架构和具体协议的运作机理,不仅能帮助开发者写出更健壮的网络代码,更能让你在面对网络故障时有条不紊地逐层排查——这正是”分层”思维给予我们的核心力量。

