计算机网络
TCP/IP 四层模型总览
| 层级 | 名称 | 职责 | 核心协议 | 数据单元 |
|---|---|---|---|---|
| 4 | 应用层 | 为用户程序提供网络服务接口,定义数据格式和交互规则 | HTTP、DNS、FTP、SSH、SMTP | 报文(Message) |
| 3 | 传输层 | 端到端通信,通过端口号区分同一主机上的不同进程 | TCP、UDP、QUIC | 段 / 数据报 |
| 2 | 网络层 | 主机到主机的寻址和路由,跨网络转发 | IP、ICMP、ARP | 包(Packet) |
| 1 | 网络接口层 | 在物理介质上传输比特流,帧封装与硬件寻址 | 以太网、WiFi、PPP | 帧(Frame) |
与 OSI 七层的对应
1 | TCP/IP 四层 OSI 七层 |
数据封装与解封装
1 | 应用层: [HTTP 数据] |
每层只关心本层头部,对上层数据视为不透明 payload — 分层解耦的核心。接收方向逆过程,逐层剥头部。
每层解决的核心问题
- 应用层:定义”说什么” — 消息格式、请求/响应语义
- 传输层:解决”哪个进程” — 通过端口号区分应用,TCP 可靠有序,UDP 快速轻量
- 网络层:解决”到哪台主机” — 通过 IP 地址全局寻址,路由器逐跳转发,IP 是尽力而为(不可靠、无连接)
- 网络接口层:解决”下一跳怎么到” — 通过 MAC 地址在局域网内定位物理设备,ARP 把 IP 解析成 MAC
TCP/UDP
介绍一下TCP
TCP 是传输层协议,向上层提供面向连接的、可靠的、基于字节流的端到端传输服务,具备流量控制与拥塞控制能力。
TCP 建立连接的过程是三次握手,断开连接的过程是四次挥手。
建立连接的详细 timeline 与双方各自的 action:
- 服务端准备阶段:调用
socket()系统调用,内核创建struct sock等内核对象并初始化操作函数与回调。然后调用bind(),将 socket 绑定到具体的 IP:port。再调用listen(),申请并初始化半连接队列(哈希表)和全连接队列(链表)。 - 客户端准备阶段:同样调用
socket()创建内核对象。客户端通常不调用bind(),端口由后续connect()内部自动选择。 - 第一次握手(客户端 → 服务端 SYN):客户端调用
connect(),内核将 socket 状态设为TCP_SYN_SENT,通过inet_hash_connect()从ip_local_port_range中动态选择一个可用端口,构造并发送 SYN 包,同时启动重传定时器。 - 第二次握手(服务端 → 客户端 SYN+ACK):服务端网卡收到 SYN 包后,通过 DMA 将数据帧写入内核预分配的 Ring Buffer(环形缓冲区),触发硬中断 → 软中断(NAPI)从 Ring Buffer 取出数据分配
skb→ 送入协议栈,由tcp_v4_rcv→tcp_v4_do_rcv→tcp_rcv_state_process处理。服务端检查半连接队列和全连接队列是否已满(满则可能丢弃),创建request_sock,构造并发送 SYN+ACK 响应,将request_sock添加到半连接队列,并启动 SYN+ACK 重传定时器。 - 第三次握手(客户端 → 服务端 ACK):客户端收到 SYN+ACK 后,将 socket 状态设为
TCP_ESTABLISHED,初始化拥塞控制,开启保活定时器,发送 ACK,并清除connect时设置的重传定时器。 - 服务端完成连接:服务端收到 ACK 后,在半连接队列中查找对应的
request_sock,创建子 socket,将其从半连接队列移除并加入全连接队列,子 socket 状态设为TCP_ESTABLISHED。之后服务端调用accept()从全连接队列中取出一个已建立的连接返回给用户进程。 - 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 | // ========== 服务端 ========== |
1 | // ========== 客户端 ========== |
断开连接的过程(四次挥手)
断开连接的双方是对等的,任何一方都可以主动发起关闭。以客户端主动关闭为例:
- 第一次挥手(客户端 → 服务端 FIN):客户端调用
close(fd),内核进入tcp_close()。将 socket 状态设为TCP_FIN_WAIT1,构造并发送 FIN 包(表示”我不再发送数据了”),启动重传定时器。注意 FIN 包会占用一个序列号。 - 第二次挥手(服务端 → 客户端 ACK):服务端收到 FIN 后,协议栈在
tcp_rcv_state_process中处理,将服务端 socket 状态设为TCP_CLOSE_WAIT,立即回复 ACK。此时服务端仍可向客户端发送数据(半关闭状态)。客户端收到 ACK 后,状态从TCP_FIN_WAIT1变为TCP_FIN_WAIT2。 - 第三次挥手(服务端 → 客户端 FIN):服务端处理完剩余数据后,调用
close(fd),内核同样进入tcp_close(),将 socket 状态设为TCP_LAST_ACK,构造并发送 FIN 包,启动重传定时器。 - 第四次挥手(客户端 → 服务端 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(慢启动阈值)。
- 慢启动(Slow Start):连接刚建立时 cwnd = 1 MSS(或 initcwnd,Linux 默认 10),每收到一个 ACK,cwnd 翻倍(指数增长)。目的是快速探测网络容量。当 cwnd ≥ ssthresh 时,进入拥塞避免。
- 拥塞避免(Congestion Avoidance):cwnd 每个 RTT 只增加 1 MSS(线性增长),谨慎探测网络极限。持续增长直到检测到丢包。
- 快重传(Fast Retransmit):发送方收到 3 个重复 ACK 时,不等超时定时器,立即重传丢失的报文段。这比等待超时更快地恢复丢包。
- 快恢复(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 | SRTT = (1 - α) × SRTT + α × RTT_sample // 平滑 RTT,α = 1/8 |
- 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 | 请求行: 方法 URL HTTP版本 例: GET /index.html HTTP/1.1 |
响应报文:
1 | 状态行: HTTP版本 状态码 原因短语 例: HTTP/1.1 200 OK |
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),连接会被关闭,下次需要重新建连
如何验证:
- 浏览器 DevTools → Network 面板:查看
Connection ID列,相同 ID 表示复用了同一个 TCP 连接 chrome://net-internals/#sockets:查看活跃的 socket 连接池- 抓包(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 的三个痛点
- TCP 队头阻塞:TCP 是字节流协议,一个包丢失会阻塞该连接上所有数据的交付,即使其他 Stream 的数据已到达。HTTP/2 的多路复用在 TCP 上反而放大了这个问题。
- 连接建立慢:TCP 三次握手(1 RTT)+ TLS 1.2 握手(2 RTT)= 首次连接需要 3 RTT。即使 TLS 1.3 优化到 1 RTT,TCP + TLS 1.3 仍需 2 RTT。
- 协议僵化: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 到渲染内容发生了什么
一、宏观端到端流程
- URL 解析:浏览器解析输入的 URL,提取协议(https)、域名(example.com)、端口(443)、路径(/index.html)。
- DNS 解析:将域名解析为 IP 地址。查找顺序:浏览器缓存 → 操作系统缓存(/etc/hosts) → 本地 DNS 服务器(递归查询) → 根域名服务器 → 顶级域名服务器 → 权威域名服务器(迭代查询)。
- TCP 连接建立:浏览器与目标 IP:port 进行三次握手建立 TCP 连接。
- TLS 握手(HTTPS):在 TCP 之上进行 TLS 握手,协商加密算法、交换密钥、验证服务端证书。TLS 1.2 需要额外 2 RTT,TLS 1.3 需要 1 RTT。
- 发送 HTTP 请求:浏览器构造 HTTP 请求报文(请求行 + 请求头 + 请求体),通过已建立的加密连接发送。
- 服务端处理:服务端(Nginx/应用服务器)接收请求,可能涉及反向代理、负载均衡、路由分发、业务逻辑处理、数据库查询等,最终构造 HTTP 响应。
- 接收 HTTP 响应:浏览器接收响应报文(状态行 + 响应头 + 响应体)。
- HTML 解析与渲染:
- 解析 HTML 构建 DOM 树
- 遇到
<link>加载 CSS,解析构建 CSSOM 树 - 遇到
<script>加载并执行 JS(可能阻塞 DOM 解析,除非 async/defer) - DOM + CSSOM 合成 渲染树(Render Tree)
- 布局(Layout):计算每个节点的位置和大小
- 绘制(Paint):将节点绘制成像素
- 合成(Composite):GPU 合成各图层,最终显示到屏幕
二、微观视角 — 协议栈、中断与 Ring Buffer
以客户端发出 HTTP 请求、服务端响应数据到达客户端为例,展开收包链路:
发送方向(客户端 → 网络):
- 应用层:浏览器调用
write(fd, data)/send(),数据通过系统调用进入内核 - 传输层:TCP 将数据按 MSS 分段,加上 TCP 头部(序列号、端口等),放入发送队列
- 网络层:加上 IP 头部(源/目的 IP),进行路由查找
- 数据链路层:加上以太网帧头(源/目的 MAC),通过网卡驱动发送
- 网卡:将帧转成电信号/光信号发到网线上
接收方向(网络 → 客户端浏览器):
- 网卡收包:网卡接收电信号,还原成数据帧
- DMA 写入 Ring Buffer:网卡通过 DMA 将数据帧直接写入内核预分配的环形缓冲区,CPU 不参与数据搬运
- 硬中断:网卡触发硬中断通知 CPU “有数据到了”。硬中断处理极短,只做两件事:记录时间戳 + 调度软中断(NAPI)
- 软中断(NET_RX_SOFTIRQ):
- 从 Ring Buffer 中取出数据帧,分配
skb(socket buffer) - 送入协议栈处理
- 从 Ring Buffer 中取出数据帧,分配
- 网络层:
ip_rcv()解析 IP 头,校验,根据协议字段分发到传输层 - 传输层:
tcp_v4_rcv()解析 TCP 头,根据四元组找到对应的 socket,将数据放入 socket 的接收队列(sk_receive_queue) - 唤醒进程:调用
sk_data_ready回调,唤醒在read()/recv()/epoll_wait()上阻塞的进程 - 应用层:浏览器从内核接收队列读出数据,交给 HTTP 解析器 → HTML 解析器 → 渲染引擎
1 | ┌──────────────────────────────────────────────────────────┐ |
DNS 的迭代查询和递归查询
递归查询:客户端问本地 DNS 服务器,本地 DNS 代替客户端去逐级查询,最终把结果返回给客户端。客户端只发一次请求,等一个最终答案。
迭代查询:本地 DNS 服务器向上级服务器查询时,上级服务器不代替查询,而是返回”你去问谁”,本地 DNS 再自己去问下一个服务器。
实际查询流程:
1 | 客户端 ──递归──→ 本地DNS服务器 |
关键区分:
- 客户端 → 本地 DNS:递归查询(客户端发一次,本地 DNS 负责到底)
- 本地 DNS → 根/TLD/权威:迭代查询(本地 DNS 自己一轮一轮地问)
DNS 缓存层级(越靠前越先命中则越快):
- 浏览器缓存:Chrome 默认缓存 60s
- 操作系统缓存:如
/etc/hosts、nscd/systemd-resolved 缓存 - 本地 DNS 服务器缓存:ISP 提供的 DNS 或 8.8.8.8 等公共 DNS,根据 TTL 缓存
- 以上都未命中才走完整的迭代查询链路
DNS 使用的传输层协议:
- 默认 UDP 端口 53(查询报文通常很小,一个 UDP 包搞定)
- 如果响应超过 512 字节(如区域传输),会切换到 TCP 端口 53
- 现代趋势:DoH(DNS over HTTPS)、DoT(DNS over TLS)提供加密 DNS 查询
域名层级结构:
1 | www . example . com . |
DNS 查询就是从根开始逐级缩小范围:根 → .com → example.com → www.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 | 客户端 ──→ 反向代理(Nginx) ──→ 后端服务器 A |
客户端访问域名,DNS 指向反向代理的 IP。反向代理接收请求,根据规则转发给后端服务器,后端响应经反向代理返回给客户端。客户端全程不知道后端架构。
反向代理的核心作用:
- 负载均衡:把请求分发到多台后端,策略有轮询、加权轮询、IP Hash、最少连接
- 隐藏后端架构:后端用内网 IP,不暴露在公网,提升安全性
- SSL 终结:反向代理统一处理 TLS 握手和加解密,后端之间走 HTTP 明文,减轻后端压力
- 缓存加速:缓存静态资源直接响应客户端,不必每次打到后端
- 请求过滤/限流:在代理层做 WAF、限速、黑白名单
- 路径路由:根据 URL 路径分发到不同后端服务(
/api→ 应用服务器,/static→ 文件服务器)
CDN 本质上就是分布在全球的反向代理缓存节点。
