TCP/IP 四层模型总览

层级 名称 职责 核心协议 数据单元
4 应用层 为用户程序提供网络服务接口,定义数据格式和交互规则 HTTP、DNS、FTP、SSH、SMTP 报文(Message)
3 传输层 端到端通信,通过端口号区分同一主机上的不同进程 TCP、UDP、QUIC 段 / 数据报
2 网络层 主机到主机的寻址和路由,跨网络转发 IP、ICMP、ARP 包(Packet)
1 网络接口层 在物理介质上传输比特流,帧封装与硬件寻址 以太网、WiFi、PPP 帧(Frame)

与 OSI 七层的对应

1
2
3
4
5
6
7
8
9
10
TCP/IP 四层          OSI 七层
┌──────────┐ ┌──────────────┐
│ 应用层 │ ←→ │ 应用/表示/会话 │
├──────────┤ ├──────────────┤
│ 传输层 │ ←→ │ 传输层 │
├──────────┤ ├──────────────┤
│ 网络层 │ ←→ │ 网络层 │
├──────────┤ ├──────────────┤
│ 网络接口层 │ ←→ │ 数据链路/物理 │
└──────────┘ └──────────────┘

数据封装与解封装

1
2
3
4
5
6
7
应用层:     [HTTP 数据]
↓ + TCP头
传输层: [TCP头 | HTTP 数据] ← 段 Segment
↓ + IP头
网络层: [IP头 | TCP头 | HTTP 数据] ← 包 Packet
↓ + 帧头 + FCS
网络接口层: [帧头 | IP头 | TCP头 | HTTP 数据 | FCS] ← 帧 Frame

每层只关心本层头部,对上层数据视为不透明 payload — 分层解耦的核心。接收方向逆过程,逐层剥头部。

每层解决的核心问题

  • 应用层:定义”说什么” — 消息格式、请求/响应语义
  • 传输层:解决”哪个进程” — 通过端口号区分应用,TCP 可靠有序,UDP 快速轻量
  • 网络层:解决”到哪台主机” — 通过 IP 地址全局寻址,路由器逐跳转发,IP 是尽力而为(不可靠、无连接)
  • 网络接口层:解决”下一跳怎么到” — 通过 MAC 地址在局域网内定位物理设备,ARP 把 IP 解析成 MAC

TCP/UDP

介绍一下TCP

TCP 是传输层协议,向上层提供面向连接的、可靠的、基于字节流的端到端传输服务,具备流量控制与拥塞控制能力。

TCP 建立连接的过程是三次握手,断开连接的过程是四次挥手。

建立连接的详细 timeline 与双方各自的 action:

  1. 服务端准备阶段:调用 socket() 系统调用,内核创建 struct sock 等内核对象并初始化操作函数与回调。然后调用 bind(),将 socket 绑定到具体的 IP:port。再调用 listen(),申请并初始化半连接队列(哈希表)和全连接队列(链表)。
  2. 客户端准备阶段:同样调用 socket() 创建内核对象。客户端通常不调用 bind(),端口由后续 connect() 内部自动选择。
  3. 第一次握手(客户端 → 服务端 SYN):客户端调用 connect(),内核将 socket 状态设为 TCP_SYN_SENT,通过 inet_hash_connect()ip_local_port_range 中动态选择一个可用端口,构造并发送 SYN 包,同时启动重传定时器。
  4. 第二次握手(服务端 → 客户端 SYN+ACK):服务端网卡收到 SYN 包后,通过 DMA 将数据帧写入内核预分配的 Ring Buffer(环形缓冲区),触发硬中断 → 软中断(NAPI)从 Ring Buffer 取出数据分配 skb → 送入协议栈,由 tcp_v4_rcvtcp_v4_do_rcvtcp_rcv_state_process 处理。服务端检查半连接队列和全连接队列是否已满(满则可能丢弃),创建 request_sock,构造并发送 SYN+ACK 响应,将 request_sock 添加到半连接队列,并启动 SYN+ACK 重传定时器。
  5. 第三次握手(客户端 → 服务端 ACK):客户端收到 SYN+ACK 后,将 socket 状态设为 TCP_ESTABLISHED,初始化拥塞控制,开启保活定时器,发送 ACK,并清除 connect 时设置的重传定时器。
  6. 服务端完成连接:服务端收到 ACK 后,在半连接队列中查找对应的 request_sock,创建子 socket,将其从半连接队列移除并加入全连接队列,子 socket 状态设为 TCP_ESTABLISHED。之后服务端调用 accept() 从全连接队列中取出一个已建立的连接返回给用户进程。
  7. accept() 阻塞机制accept() 默认阻塞。若全连接队列为空,内核将进程放入监听 socket 的等待队列,进程状态设为 TASK_INTERRUPTIBLE(可中断睡眠)。当新连接加入全连接队列时,通过 sk_data_ready 回调唤醒进程,进程回到 TASK_RUNNING,从队列头部取出连接返回。

三次握手过程中各阶段创建的内核对象

阶段 创建的对象 存放位置
服务端 socket() 监听 socket(完整 struct sock
客户端 socket() + connect() 客户端 socket(完整 struct sock
收到 SYN(第二次握手) request_sock轻量级,仅记录握手必要信息) 半连接队列
收到 ACK(第三次握手) 子 socket(完整 struct sock,由 tcp_v4_syn_recv_sock() 创建) 全连接队列
accept() 从全连接队列取出子 socket 返回给用户进程

关键区分:半连接队列中存放的是轻量的 request_sock不是完整 socket。只有第三次握手完成后才创建完整的子 socket。这样设计是为了防御 SYN Flood 攻击——攻击者发大量伪造 SYN 时,轻量的 request_sock 消耗的资源远小于完整 socket,避免服务端内存被迅速耗尽。

监听 socket 只负责握手,不参与数据通信。服务端实际用于通信的是第三次握手时创建的子 socket(accept() 返回的 fd)。

服务端与客户端 Socket 编程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ========== 服务端 ==========
int main() {
// 1. 创建 socket — 内核创建 struct sock,初始化操作函数表
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

// 2. bind — 绑定 IP:port
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr));

// 3. listen — 初始化半连接队列(哈希表)和全连接队列(链表)
// 128 是 backlog,全连接队列长度 = min(backlog, somaxconn)
listen(listen_fd, 128);

// 4. 循环 accept — 每次从全连接队列取出一个已完成三次握手的连接
// 队列为空时阻塞,进程进入 TASK_INTERRUPTIBLE
while (1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len);

// 5. 通信(实际生产中通常 fork 子进程或交给线程池处理)
char buf[1024];
read(conn_fd, buf, sizeof(buf));
write(conn_fd, "OK", 2);

// 6. 关闭该连接 — 触发四次挥手
close(conn_fd);
}

close(listen_fd);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ========== 客户端 ==========
int main() {
// 1. 创建 socket — 同样创建内核对象
int fd = socket(AF_INET, SOCK_STREAM, 0);

// 不调用 bind(),端口由 connect() 内部自动选择

// 2. connect — 选端口 + 发 SYN + 三次握手
// 内部:状态 → TCP_SYN_SENT → 选端口 → 发 SYN → 等 SYN+ACK → 发 ACK → ESTABLISHED
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
connect(fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

// 3. 通信
write(fd, "Hello", 5);
char buf[1024];
read(fd, buf, sizeof(buf));

// 4. 关闭
close(fd);
return 0;
}

断开连接的过程(四次挥手)

断开连接的双方是对等的,任何一方都可以主动发起关闭。以客户端主动关闭为例:

  1. 第一次挥手(客户端 → 服务端 FIN):客户端调用 close(fd),内核进入 tcp_close()。将 socket 状态设为 TCP_FIN_WAIT1,构造并发送 FIN 包(表示”我不再发送数据了”),启动重传定时器。注意 FIN 包会占用一个序列号。
  2. 第二次挥手(服务端 → 客户端 ACK):服务端收到 FIN 后,协议栈在 tcp_rcv_state_process 中处理,将服务端 socket 状态设为 TCP_CLOSE_WAIT立即回复 ACK。此时服务端仍可向客户端发送数据(半关闭状态)。客户端收到 ACK 后,状态从 TCP_FIN_WAIT1 变为 TCP_FIN_WAIT2
  3. 第三次挥手(服务端 → 客户端 FIN):服务端处理完剩余数据后,调用 close(fd),内核同样进入 tcp_close(),将 socket 状态设为 TCP_LAST_ACK,构造并发送 FIN 包,启动重传定时器。
  4. 第四次挥手(客户端 → 服务端 ACK):客户端收到 FIN 后,发送 ACK,状态从 TCP_FIN_WAIT2 变为 TIME_WAIT。服务端收到 ACK 后,状态从 TCP_LAST_ACK 变为 TCP_CLOSE,连接彻底释放。

为什么需要 TIME_WAIT?

客户端进入 TIME_WAIT不会立即释放,而是等待 2MSL(Maximum Segment Lifetime,通常 60s)后才进入 TCP_CLOSE。原因:

  • 确保最后一个 ACK 到达服务端:如果服务端没收到 ACK,会重传 FIN。客户端在 TIME_WAIT 状态下仍能接收并重新回复 ACK。如果直接关闭,服务端的重传 FIN 会收到 RST,导致异常断连。
  • 防止旧连接的延迟报文干扰新连接:等 2MSL 确保网络中属于该连接的所有报文都已过期消亡,不会被后续复用相同四元组的新连接误收。

TIME_WAIT 过多的影响与应对

TIME_WAIT 状态的 socket 会占用端口和内核内存。高并发短连接场景下(如 HTTP 短连接),大量 TIME_WAIT 可能导致端口耗尽。常见应对:

  • net.ipv4.tcp_tw_reuse = 1:允许在 TIME_WAIT 状态下复用端口建立新的出站连接(依赖 TCP 时间戳判断新旧报文)
  • 使用长连接(HTTP Keep-Alive)减少连接建立/断开频率
  • 扩大 ip_local_port_range 范围

四次挥手状态变迁总结

阶段 客户端状态 服务端状态
客户端 close() 发 FIN FIN_WAIT_1
服务端收到 FIN 回 ACK FIN_WAIT_2 CLOSE_WAIT
服务端 close() 发 FIN FIN_WAIT_2 LAST_ACK
客户端收到 FIN 回 ACK TIME_WAIT CLOSE
等待 2MSL CLOSE

TCP Keep-Alive(保活机制)

TCP 保活定时器是内核级别的心跳机制,用于检测长时间空闲的连接对端是否还存活。

工作原理:开启 SO_KEEPALIVE 的一方,在连接空闲一段时间后,发送一个序列号比期望值小 1 的 ACK 包(不是空包),对端收到后回复正常 ACK。如果收不到回复,则按间隔重复探测,连续多次无响应后判定对端死亡,内核关闭连接。

三个内核参数

参数 默认值 含义
tcp_keepalive_time 7200s(2小时) 连接空闲多久后开始发探测包
tcp_keepalive_intvl 75s 每次探测之间的间隔
tcp_keepalive_probes 9 连续多少次探测无响应就判定对端死亡

注意

  • 保活探测双方都可以开启,不是只有客户端。实际更常见的是服务端开启,用于清理死连接释放资源
  • 需要应用层显式设置 SO_KEEPALIVE 套接字选项才生效
  • 默认 2 小时太慢,很多场景需要秒级感知存活(如 RPC、WebSocket),因此应用层通常自己实现心跳协议(如每 30s 发 ping/pong),比内核 Keep-Alive 更灵活

MSS 与 MTU

MTU(Maximum Transmission Unit)数据链路层对单个帧能承载的最大数据量的限制。以太网默认 MTU = 1500 字节(指 IP 包的最大长度,不含以太网帧头 14B 和 FCS 4B)。

MSS(Maximum Segment Size)传输层 TCP 单个报文段能携带的最大应用数据长度,不含 TCP 头部和 IP 头部。计算关系:

1
MSS = MTU - IP头部(20B) - TCP头部(20B) = 1500 - 20 - 20 = 1460 字节

MSS 协商发生在三次握手期间:双方在 SYN 和 SYN+ACK 包的 TCP Options 字段中各自携带自己的 MSS 值,通信时取双方较小值作为实际 MSS。

为什么需要 MSS? 如果 TCP 不限制报文段大小,一个很大的 TCP 段在 IP 层会被分片(IP Fragmentation)。IP 分片的问题:

  • 任何一个分片丢失,整个 IP 数据报都要重传,效率极低
  • 分片和重组消耗路由器/主机资源
  • 中间设备可能有更小的 MTU(路径 MTU),导致二次分片

MSS 让 TCP 在源端就将数据切成不超过链路 MTU 的大小,避免 IP 层分片,提高传输效率。

常见值

场景 MTU MSS
以太网 1500B 1460B
PPPoE(拨号/光猫) 1492B 1452B
本地回环(lo) 65535B 65495B

拥塞控制与流量控制

流量控制拥塞控制解决的是两个不同层面的问题:

流量控制 拥塞控制
解决什么问题 发送方发太快,接收方来不及处理 所有发送方合起来发太多,网络承载不住
作用范围 端到端(发送方 ↔ 接收方) 全局(发送方 ↔ 网络)
核心机制 滑动窗口(接收窗口 rwnd) 拥塞窗口(cwnd)

实际发送窗口 = min(rwnd, cwnd),两者中更小的那个决定发送速率。


流量控制 — 滑动窗口(rwnd)

  • 接收方在每个 ACK 中通过 TCP 头部的 Window 字段告知发送方自己还能接收多少数据(即 rwnd)
  • 发送方保证已发送但未确认的数据量不超过 rwnd
  • 如果接收方缓冲区满了,rwnd = 0,发送方停止发送,进入窗口探测(Window Probe):定期发送 1 字节的探测包,等待接收方窗口恢复
  • 这是一个端到端的、被动的速率调节机制

拥塞控制 — 四个阶段

拥塞控制的核心变量是 cwnd(拥塞窗口)和 ssthresh(慢启动阈值)。

  1. 慢启动(Slow Start):连接刚建立时 cwnd = 1 MSS(或 initcwnd,Linux 默认 10),每收到一个 ACK,cwnd 翻倍(指数增长)。目的是快速探测网络容量。当 cwnd ≥ ssthresh 时,进入拥塞避免。
  2. 拥塞避免(Congestion Avoidance):cwnd 每个 RTT 只增加 1 MSS(线性增长),谨慎探测网络极限。持续增长直到检测到丢包。
  3. 快重传(Fast Retransmit):发送方收到 3 个重复 ACK 时,不等超时定时器,立即重传丢失的报文段。这比等待超时更快地恢复丢包。
  4. 快恢复(Fast Recovery):快重传后,ssthresh = cwnd / 2,cwnd = ssthresh + 3 MSS,然后进入拥塞避免(线性增长)。不回到慢启动,因为收到重复 ACK 说明网络还能传数据,没有严重拥塞。

超时重传(RTO Timeout) 的处理更激进:ssthresh = cwnd / 2,cwnd 直接重置为 1 MSS,回到慢启动。因为超时意味着网络可能严重拥塞。


拥塞控制算法演进

算法 特点
Reno 经典四阶段(慢启动 + 拥塞避免 + 快重传 + 快恢复)
Cubic Linux 默认算法,cwnd 增长函数是三次方曲线,对高带宽长延迟(BDP 大)网络更友好
BBR Google 提出,不依赖丢包信号,而是主动探测瓶颈带宽和最小 RTT,适合高丢包率和长肥管道场景

RTO 计算 — Jacobson 算法

RTO(Retransmission Timeout)决定”等多久没收到 ACK 就判定丢包”,与拥塞控制(决定”发多快”)是独立的两个机制。RTO 直接基于 RTT 采样计算(RFC 6298):

1
2
3
SRTT    = (1 - α) × SRTT + α × RTT_sample          // 平滑 RTT,α = 1/8
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample| // RTT 偏差,β = 1/4
RTO = SRTT + 4 × RTTVAR // 重传超时
  • RTT 波动大 → RTTVAR 大 → RTO 长,避免误判丢包
  • RTT 稳定 → RTTVAR 小 → RTO 短,快速检测真正的丢包
  • RTO 有下限(通常 200ms)和上限(通常 120s),防止极端值
  • 连续超时重传时,RTO 指数退避(每次翻倍),直到收到有效 ACK 才恢复正常计算

TCP与UDP 优缺点对比

维度 TCP UDP
连接 面向连接,需三次握手建立、四次挥手断开 无连接,直接发送
可靠性 可靠传输(确认、重传、序列号、校验和) 不可靠,尽力而为,不保证到达和顺序
有序性 保证有序交付(通过序列号重排) 不保证顺序
流量控制 有(滑动窗口)
拥塞控制 有(慢启动、拥塞避免、快重传、快恢复)
传输方式 字节流(无边界,应用层需自行分包) 数据报(保留消息边界,一次 send 对应一次 recv)
头部开销 最小 20B(含序列号、确认号、窗口等) 固定 8B(源端口、目的端口、长度、校验和)
通信模式 一对一(点对点) 一对一、一对多(广播/组播)
速度 较慢(连接建立 + 确认重传开销) 快(无连接、无确认)
资源消耗 高(维护连接状态、缓冲区、定时器)

TCP 适用场景:需要可靠传输的场景 — HTTP/HTTPS、FTP、SSH、数据库连接、邮件(SMTP/IMAP)

UDP 适用场景:对实时性要求高、允许少量丢包的场景 — DNS 查询、视频/语音通话(RTP)、直播、游戏状态同步、QUIC(基于 UDP 实现的可靠传输协议)

核心取舍:TCP 用复杂机制换可靠性,UDP 用简单性换速度。实际中也可以在 UDP 之上自行实现可靠传输(如 QUIC、KCP),兼得灵活性和性能。

HTTP

HTTP 报文结构

请求报文

1
2
3
4
5
6
7
请求行:    方法 URL HTTP版本        例: GET /index.html HTTP/1.1
请求头: Host: example.com
Content-Type: application/json
Connection: keep-alive
...
空行: \r\n
请求体: (GET 通常无 body,POST/PUT 有)

响应报文

1
2
3
4
5
6
7
状态行:    HTTP版本 状态码 原因短语    例: HTTP/1.1 200 OK
响应头: Content-Type: text/html
Content-Length: 1234
Set-Cookie: ...
...
空行: \r\n
响应体: (HTML/JSON/二进制等)

HTTP 常见状态码

分类 含义 常见状态码
1xx 信息性,请求已接收,继续处理 100 Continue、101 Switching Protocols
2xx 成功 200 OK、201 Created、204 No Content
3xx 重定向,需要进一步操作 301 永久重定向、302 临时重定向、304 Not Modified
4xx 客户端错误 400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、405 Method Not Allowed
5xx 服务端错误 500 Internal Server Error、502 Bad Gateway、503 Service Unavailable、504 Gateway Timeout

302 状态码

302 Found(临时重定向):服务端告诉客户端”你请求的资源临时在另一个 URL”,响应头中包含 Location: <新URL>,浏览器会自动跳转到该 URL。

与 301 的区别

  • 301 Moved Permanently:永久重定向,浏览器和搜索引擎会缓存这个重定向,后续请求直接访问新 URL
  • 302 Found:临时重定向,浏览器不缓存,每次仍先访问原 URL,再被重定向

实际场景:302 常用于登录跳转(未登录 → 302 → 登录页)、短链接服务、A/B 测试等。

没有 Keep-Alive 时 TCP 连接的关闭时机

HTTP/1.0(默认短连接):每个请求/响应完成后,服务端发送完响应体的最后一个字节后立即 close(fd),触发四次挥手。每个 HTTP 请求独占一个 TCP 连接的完整生命周期,下一个请求需要重新三次握手。

HTTP/1.1 显式关闭:如果设置 Connection: close,行为与 HTTP/1.0 一样,服务端发完响应后关闭连接。

接收方如何判断”响应结束”

  • Content-Length:明确告知响应体长度,收满即完成
  • Transfer-Encoding: chunked:分块传输,收到长度为 0 的 chunk 表示结束
  • 连接关闭本身:如果既没有 Content-Length 也没有 chunked,接收方只能靠收到 FIN 来判断数据结束。短连接模式下,关闭连接不仅是释放资源,还承担了标记消息边界的作用

同浏览器打开同网页两次是否复用 TCP 连接

答案:通常会复用

HTTP/1.1 默认开启 Connection: keep-alive(长连接),同一域名下的多个请求会复用同一个 TCP 连接,避免重复三次握手的开销。

具体行为

  • HTTP/1.1:浏览器对同一域名通常维护 6 个并发 TCP 连接(Chrome 默认),请求在这些连接上排队复用
  • HTTP/2:同一域名只需 1 个 TCP 连接,通过多路复用(Multiplexing)在单连接上并发传输多个请求/响应
  • 如果连接空闲超过 Keep-Alive 超时时间(服务端配置,如 nginx 默认 75s),连接会被关闭,下次需要重新建连

如何验证

  1. 浏览器 DevTools → Network 面板:查看 Connection ID 列,相同 ID 表示复用了同一个 TCP 连接
  2. chrome://net-internals/#sockets:查看活跃的 socket 连接池
  3. 抓包(Wireshark/tcpdump):观察两次 HTTP 请求是否在同一个四元组的 TCP 连接上进行,没有额外的三次握手

HTTP/2 与 HTTP/1.1 的区别

维度 HTTP/1.1 HTTP/2
传输格式 文本协议(明文可读) 二进制帧(Binary Framing),更高效解析
多路复用 单连接串行(队头阻塞),靠多个 TCP 连接并发 单连接多路复用(Stream),多个请求/响应并发交错传输
队头阻塞 HTTP 层有(前一个请求未完成,后面的排队等待) HTTP 层解决了(但 TCP 层队头阻塞仍存在)
头部压缩 无,每次携带完整头部(Cookie 可能很大) HPACK 算法压缩头部,维护动态/静态表,增量传输
服务端推送 不支持 Server Push:服务端可以主动推送客户端可能需要的资源
连接数 同域名通常 6 个并发 TCP 连接 同域名 1 个 TCP 连接即可
优先级 支持流优先级和依赖关系

HTTP/2 的核心改进:在传输层和 HTTP 语义之间加了一层二进制帧层,将请求/响应拆成帧,帧带有 Stream ID,不同 Stream 的帧可以交错发送,从而在单个 TCP 连接上实现真正的并发。

HTTP/2 未解决的问题:TCP 层队头阻塞 — 如果某个 TCP 包丢失,即使是不同 Stream 的数据,都要等重传完成后才能交付。这正是 HTTP/3(QUIC)要解决的问题。

QUIC 协议(HTTP/3)

QUIC 是什么

QUIC(Quick UDP Internet Connections)是 Google 设计、IETF 标准化的传输层协议,基于 UDP 实现,是 HTTP/3 的底层传输协议。核心目标:解决 TCP + TLS 的固有缺陷。

为什么需要 QUIC — TCP 的三个痛点

  1. TCP 队头阻塞:TCP 是字节流协议,一个包丢失会阻塞该连接上所有数据的交付,即使其他 Stream 的数据已到达。HTTP/2 的多路复用在 TCP 上反而放大了这个问题。
  2. 连接建立慢:TCP 三次握手(1 RTT)+ TLS 1.2 握手(2 RTT)= 首次连接需要 3 RTT。即使 TLS 1.3 优化到 1 RTT,TCP + TLS 1.3 仍需 2 RTT。
  3. 协议僵化:TCP 实现在操作系统内核中,中间设备(防火墙、NAT)对 TCP 头部有固定预期,升级 TCP 协议极其困难。

QUIC 如何解决这些问题

问题 QUIC 的解决方案
队头阻塞 每个 Stream 独立可靠传输,一个 Stream 丢包不影响其他 Stream。丢包重传粒度是 Stream 级别而非连接级别
连接建立慢 首次连接 1 RTT(握手 + TLS 合并),恢复连接 0 RTT(利用缓存的密钥直接发送数据)
协议僵化 基于 UDP,协议逻辑在用户态实现,不受内核和中间设备限制,迭代升级更快
连接迁移 Connection ID 标识连接(而非四元组),WiFi ↔ 4G 切换时 IP 变了连接不断

QUIC 的核心特性

1. 内建加密:QUIC 强制使用 TLS 1.3,不仅加密应用数据,连大部分传输层头部也加密。没有明文传输的选项。

2. 独立 Stream:QUIC 的多路复用是在传输层原生实现的(不像 HTTP/2 是在应用层),每个 Stream 有独立的序列号和重传机制,真正消除了队头阻塞。

3. 0-RTT 恢复:客户端缓存之前协商的密钥参数,恢复连接时可以在握手的同时发送加密数据,实现 0-RTT。代价是 0-RTT 数据存在重放攻击风险,需要服务端做幂等保护。

4. 连接迁移:TCP 用四元组(源IP、源端口、目的IP、目的端口)标识连接,IP 变了连接就断了。QUIC 用随机生成的 Connection ID 标识,网络切换时只要 Connection ID 不变,连接就可以无缝迁移。

HTTP/1.1 vs HTTP/2 vs HTTP/3(QUIC)总结

维度 HTTP/1.1 HTTP/2 HTTP/3(QUIC)
传输层 TCP TCP UDP(QUIC)
加密 可选(HTTPS) 实践中强制 TLS 强制 TLS 1.3
多路复用 无(多连接并发) 有(单 TCP 连接) 有(单 QUIC 连接,Stream 级独立)
队头阻塞 HTTP 层 + TCP 层 TCP 层
连接建立 TCP 1 RTT + TLS TCP 1 RTT + TLS 首次 1 RTT,恢复 0 RTT
连接迁移 不支持 不支持 支持(Connection ID)

综合

输入 URL 到渲染内容发生了什么

一、宏观端到端流程

  1. URL 解析:浏览器解析输入的 URL,提取协议(https)、域名(example.com)、端口(443)、路径(/index.html)。
  2. DNS 解析:将域名解析为 IP 地址。查找顺序:浏览器缓存 → 操作系统缓存(/etc/hosts) → 本地 DNS 服务器(递归查询) → 根域名服务器 → 顶级域名服务器 → 权威域名服务器(迭代查询)。
  3. TCP 连接建立:浏览器与目标 IP:port 进行三次握手建立 TCP 连接。
  4. TLS 握手(HTTPS):在 TCP 之上进行 TLS 握手,协商加密算法、交换密钥、验证服务端证书。TLS 1.2 需要额外 2 RTT,TLS 1.3 需要 1 RTT。
  5. 发送 HTTP 请求:浏览器构造 HTTP 请求报文(请求行 + 请求头 + 请求体),通过已建立的加密连接发送。
  6. 服务端处理:服务端(Nginx/应用服务器)接收请求,可能涉及反向代理、负载均衡、路由分发、业务逻辑处理、数据库查询等,最终构造 HTTP 响应。
  7. 接收 HTTP 响应:浏览器接收响应报文(状态行 + 响应头 + 响应体)。
  8. HTML 解析与渲染
    • 解析 HTML 构建 DOM 树
    • 遇到 <link> 加载 CSS,解析构建 CSSOM 树
    • 遇到 <script> 加载并执行 JS(可能阻塞 DOM 解析,除非 async/defer)
    • DOM + CSSOM 合成 渲染树(Render Tree)
    • 布局(Layout):计算每个节点的位置和大小
    • 绘制(Paint):将节点绘制成像素
    • 合成(Composite):GPU 合成各图层,最终显示到屏幕

二、微观视角 — 协议栈、中断与 Ring Buffer

客户端发出 HTTP 请求、服务端响应数据到达客户端为例,展开收包链路:

发送方向(客户端 → 网络)

  1. 应用层:浏览器调用 write(fd, data) / send(),数据通过系统调用进入内核
  2. 传输层:TCP 将数据按 MSS 分段,加上 TCP 头部(序列号、端口等),放入发送队列
  3. 网络层:加上 IP 头部(源/目的 IP),进行路由查找
  4. 数据链路层:加上以太网帧头(源/目的 MAC),通过网卡驱动发送
  5. 网卡:将帧转成电信号/光信号发到网线上

接收方向(网络 → 客户端浏览器)

  1. 网卡收包:网卡接收电信号,还原成数据帧
  2. DMA 写入 Ring Buffer:网卡通过 DMA 将数据帧直接写入内核预分配的环形缓冲区,CPU 不参与数据搬运
  3. 硬中断:网卡触发硬中断通知 CPU “有数据到了”。硬中断处理极短,只做两件事:记录时间戳 + 调度软中断(NAPI)
  4. 软中断(NET_RX_SOFTIRQ)
    • 从 Ring Buffer 中取出数据帧,分配 skb(socket buffer)
    • 送入协议栈处理
  5. 网络层ip_rcv() 解析 IP 头,校验,根据协议字段分发到传输层
  6. 传输层tcp_v4_rcv() 解析 TCP 头,根据四元组找到对应的 socket,将数据放入 socket 的接收队列(sk_receive_queue
  7. 唤醒进程:调用 sk_data_ready 回调,唤醒在 read() / recv() / epoll_wait() 上阻塞的进程
  8. 应用层:浏览器从内核接收队列读出数据,交给 HTTP 解析器 → HTML 解析器 → 渲染引擎
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──────────────────────────────────────────────────────────┐
│ 网卡收到帧 │
│ ↓ DMA │
│ Ring Buffer │
│ ↓ 硬中断 → 调度软中断 │
│ 软中断(NAPI)取出 skb │
│ ↓ │
│ ip_rcv() → 网络层解包 │
│ ↓ │
│ tcp_v4_rcv() → 传输层解包 → 放入 sk_receive_queue │
│ ↓ sk_data_ready 唤醒 │
│ 用户进程 read() 读取数据 │
│ ↓ │
│ 浏览器 HTTP 解析 → DOM/CSSOM → 渲染 │
└──────────────────────────────────────────────────────────┘

DNS 的迭代查询和递归查询

递归查询:客户端问本地 DNS 服务器,本地 DNS 代替客户端去逐级查询,最终把结果返回给客户端。客户端只发一次请求,等一个最终答案。

迭代查询:本地 DNS 服务器向上级服务器查询时,上级服务器不代替查询,而是返回”你去问谁”,本地 DNS 再自己去问下一个服务器。

实际查询流程

1
2
3
4
5
6
7
8
9
客户端 ──递归──→ 本地DNS服务器

├─迭代→ 根域名服务器 → 返回 ".com 找这个TLD服务器"

├─迭代→ .com TLD服务器 → 返回 "example.com 找这个权威服务器"

├─迭代→ 权威DNS服务器 → 返回 "example.com → 93.184.216.34"

└──────→ 客户端 ← 最终 IP 地址

关键区分

  • 客户端 → 本地 DNS递归查询(客户端发一次,本地 DNS 负责到底)
  • 本地 DNS → 根/TLD/权威迭代查询(本地 DNS 自己一轮一轮地问)

DNS 缓存层级(越靠前越先命中则越快):

  1. 浏览器缓存:Chrome 默认缓存 60s
  2. 操作系统缓存:如 /etc/hosts、nscd/systemd-resolved 缓存
  3. 本地 DNS 服务器缓存:ISP 提供的 DNS 或 8.8.8.8 等公共 DNS,根据 TTL 缓存
  4. 以上都未命中才走完整的迭代查询链路

DNS 使用的传输层协议

  • 默认 UDP 端口 53(查询报文通常很小,一个 UDP 包搞定)
  • 如果响应超过 512 字节(如区域传输),会切换到 TCP 端口 53
  • 现代趋势:DoH(DNS over HTTPS)、DoT(DNS over TLS)提供加密 DNS 查询

域名层级结构

1
2
3
4
5
6
www . example . com .
│ │ │ │
│ │ │ └─ 根域(通常省略的最后一个点)
│ │ └─ 顶级域(TLD,如 .com .cn .org)
│ └─ 二级域(SLD,如 example)
└─ 主机名 / 子域名

DNS 查询就是从根开始逐级缩小范围:根 → .comexample.comwww.example.com

TTL(Time To Live):权威 DNS 返回的记录带 TTL(如 3600s = 1小时),各级缓存按 TTL 决定缓存多久。TTL 到期后缓存失效,下次查询需要重新走迭代。TTL 越短 → DNS 变更生效越快,但缓存命中率低、DNS 压力大。

常见 DNS 记录类型

记录类型 含义 示例
A 域名 → IPv4 地址 example.com → 93.184.216.34
AAAA 域名 → IPv6 地址 example.com → 2606:2800:220:1:...
CNAME 域名 → 另一个域名(别名) www.example.com → cdn.example.net,还需再查 cdn.example.net 的 A 记录
MX 邮件服务器记录 example.com → mail.example.com
NS 域名的权威 DNS 服务器 example.com → ns1.example.com
TXT 任意文本(常用于 SPF/DKIM 验证) v=spf1 include:_spf.google.com

CNAME 与 CDN:CDN 场景大量使用 CNAME。例如 www.example.com CNAME 到 www.example.com.cdn.cloudflare.net,CDN 的 DNS 再根据用户地理位置返回最近的边缘节点 IP。

DNS 负载均衡:权威 DNS 可以对同一域名返回不同的 IP(轮询/权重/地理位置),实现简单的服务端负载均衡。但 DNS 负载均衡粒度粗、生效慢(受 TTL 影响),通常作为第一层分流,配合 Nginx/LVS 等做更精细的负载均衡。

正向代理与反向代理

正向代理(Forward Proxy) 反向代理(Reverse Proxy)
代理谁 代理客户端 代理服务端
谁知道它的存在 客户端知道,服务端不知道 服务端知道,客户端不知道
典型场景 VPN、科学上网、企业出口网关 Nginx、CDN、API Gateway
隐藏谁 隐藏客户端的真实 IP 隐藏后端服务器的真实 IP 和架构

一句话区分:正向代理是”客户端的代言人”,反向代理是”服务端的代言人”。

反向代理工作流程

1
2
3
客户端  ──→  反向代理(Nginx)  ──→  后端服务器 A
──→ 后端服务器 B
──→ 后端服务器 C

客户端访问域名,DNS 指向反向代理的 IP。反向代理接收请求,根据规则转发给后端服务器,后端响应经反向代理返回给客户端。客户端全程不知道后端架构。

反向代理的核心作用

  1. 负载均衡:把请求分发到多台后端,策略有轮询、加权轮询、IP Hash、最少连接
  2. 隐藏后端架构:后端用内网 IP,不暴露在公网,提升安全性
  3. SSL 终结:反向代理统一处理 TLS 握手和加解密,后端之间走 HTTP 明文,减轻后端压力
  4. 缓存加速:缓存静态资源直接响应客户端,不必每次打到后端
  5. 请求过滤/限流:在代理层做 WAF、限速、黑白名单
  6. 路径路由:根据 URL 路径分发到不同后端服务(/api → 应用服务器,/static → 文件服务器)

CDN 本质上就是分布在全球的反向代理缓存节点。