搜索站内内容

← 返回文章列表

从输入 URL 到页面显示:一道题串起整个计网

沿着一次网页请求梳理 HTTP 版本、缓存、DNS、TCP、TLS、ARP 与浏览器渲染,重点展开 TIME_WAIT、可靠传输、滑动窗口和拥塞控制。

Jason计算机网络计算机网络HTTPTCPTLSDNS浏览器约 16,450 字大约 47 分钟
云雾在层叠的山岭之间流动

“从输入 URL 到页面显示,中间发生了什么?”是个经典面试题。刚开始觉得它就是一条流程:查 DNS,建连接,发请求,浏览器渲染。后来沿着每一步往下问,发现缓存、TCP、TLS、路由这些知识都能串起来。

这篇就按一次网页访问的顺序记录。主线用 HTTPS over TCP 讲,也就是 HTTP/1.1 或 HTTP/2;HTTP/3 走 QUIC,会在开头单独说明。遇到容易被追问的地方,我会把原因和回答一起写下来。

从 URL 到页面显示的完整链路,标出本地缓存、建连、网络传输和渲染之间的关系

图 1:一次访问的主线。所有示意图都可以点击查看原图。

图里的顺序是一条便于理解的主线。浏览器可能提前解析 DNS、复用已有连接,也会边收 HTML 边解析、边请求图片和脚本。协商缓存需要发请求,因此要等建连之后才能拿到 304。ARP 和路由则参与每一次网络收发,不是等 HTTP 发完了才开始。

一、先把 URL 和 HTTP 版本说清楚

1.1 地址栏里的内容怎么变成 URL

输入 ttzz1.com 时,浏览器会判断这是网址还是搜索词,再按自己的策略补全协议和路径。现代浏览器经常优先尝试 HTTPS,但不能把所有输入都理解成“无条件补 https”;显式协议、HSTS、浏览器设置和访问失败后的处理都会影响结果。

以这个地址为例:

https://www.ttzz1.com:443/blog/post?id=1&tag=net#section-2
  协议        主机      端口    路径       查询参数       片段
部分例子发请求时的作用
schemehttps指定访问方案,影响默认端口和安全连接方式
hostwww.ttzz1.com用于解析地址,也参与证书校验和 HTTP 主机识别
port443指定客户端要连接的服务端端口
path/blog/post标识服务端的资源或路由
queryid=1&tag=net携带查询参数
fragmentsection-2由客户端处理,不包含在 HTTP 请求目标中

HTTP 的默认端口是 80,HTTPS 是 443。省略端口时,浏览器按 scheme 使用默认值,地址栏不一定把它显式显示出来。服务实际监听 8443,客户端就需要写出端口,或者由 443 上的入口转发过去。DNS 的 A/AAAA 记录只提供地址,不会自动把浏览器改到应用监听的端口。

URL 语法还允许 userinfo,例如 user:pass@host,但不适合把密码放进网页地址:它容易进入历史记录或日志,浏览器也会限制这类用法。

片段有个很好记的例子:/#/user/1。服务端收到的请求目标是 /,前端再读取 hash 决定展示什么。只修改片段通常不需要重新下载整份页面。

1.2 URL 和 URI 有什么区别

URI 是统一资源标识符,URL 是其中通过访问位置和方式定位资源的一类。HTTP 地址既是 URI,也是 URL;urn:isbn:978... 则是在用名字标识资源。

面试可以这样回答:

URI 强调标识资源,URL 进一步给出如何定位、访问资源。网页里的 https 地址属于 URL,因此也属于 URI。

Java 中 URI 适合解析、规范化和拼接地址。URL 的部分操作可能涉及网络,例如某些协议处理器的相等性比较可能触发 DNS;不能笼统地说“创建 URL 对象就会请求网络”。

1.3 HTTP 和 HTTPS 的区别

HTTP 定义请求、响应、方法、状态码等应用层语义。HTTPS 在 HTTP 通信中引入 TLS,提供传输机密性、完整性,以及通常的服务端身份认证。

HTTPS 能防止链路上的人直接读取或随意修改 HTTP 内容,但不会替应用解决所有安全问题。网站自身的 XSS、错误鉴权、数据库泄漏仍然要由应用处理。TLS 通常也不会隐藏目标 IP;没有 ECH 等保护时,SNI 还可能暴露域名。

HSTS 是服务端通过响应头告诉浏览器:在有效期内,这个主机应该用 HTTPS 访问。浏览器只接受安全连接上的 HSTS 策略。已经记录策略后,即使输入 HTTP 地址,也能在浏览器内部升级,而不必先发明文请求等 301。

Strict-Transport-Security: max-age=31536000; includeSubDomains

首次访问还没拿到策略时存在空窗。预加载列表能覆盖这个场景,但加一个 preload 字样本身并不等于已进入浏览器名单。

1.4 HTTP/1.0:文本报文,默认短连接

HTTP/1.0 用文本形式表达请求行和头部,例如 GET / HTTP/1.0。这里的“文本”指协议报文的表达方式,响应体当然可以是图片、音频等二进制数据。

它默认一次请求响应后关闭连接。页面需要 HTML、CSS、JS 和多张图片时,频繁建连会重复支付握手成本。部分实现支持 Connection: keep-alive 扩展,所以“1.0 绝对不能复用连接”也不准确。

1.0 已经有 Content-Length。知道长度时按长度读,不知道时常用连接关闭表示响应结束。长度不是连接关闭的替代名称,而是消息边界的一种表达。

1.0 没有强制要求 Host 头。一个 IP 因而不容易通过标准请求头区分多个域名站点,但也不能直接推导成“一 IP 永远只能放一个网站”,还可以使用不同端口等方式。

1.5 HTTP/1.1:连接复用、Host 和分块传输

HTTP/1.1 默认使用持久连接。一次响应完成后,同一条 TCP 连接可以继续承载后续请求,除非一方要求关闭或连接因超时等原因结束。

这里容易混淆:HTTP keep-alive 指连接复用;TCP keepalive 是空闲连接的探测机制。前者不负责检测对端是否还活着。

Host 是 1.1 的必需头,一台服务器可据此把 a.example.com 和 b.example.com 分发给不同站点。HTTPS 选证书还需要更早的 SNI,后面讲 TLS 时再接上。

如果服务端要边生成边发送,无法提前确定总长度,可以使用 chunked:

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/plain

5
hello
6
 world
0

线上各行以 CRLF 分隔。每块先给出十六进制长度,再发送数据;零长度块表示数据块结束,之后还可以有 trailer,最后以空行结束整个分块消息。它解决的是消息定界,不等于多路复用,也不是 HTTP/2 的二进制帧。

1.1 还丰富了缓存控制,并支持 Range 请求,比如下载文件的一部分。服务端接受范围请求时可返回 206 Partial Content 和 Content-Range,也可能忽略 Range 返回完整的 200。

1.1 支持流水线:连续发送多个请求,不必等前一个响应结束才发送下一个。但同一连接的响应必须按请求顺序返回。排在前面的响应很慢,后面的响应就被挡住,这就是应用层队头阻塞。实际客户端经常通过多条连接并发,而没有广泛使用流水线。消息定界、持久连接和流水线规则见 RFC 9112。

1.6 HTTP/2:二进制帧、多路复用和 HPACK

HTTP/2 保留方法、状态码等 HTTP 语义,改变了它们在连接上的表达方式:把消息拆成二进制帧,按 stream 区分请求响应。

例如,一个大文件在 stream 1,小接口在 stream 3,CSS 在 stream 5。不同流的帧可以交错传输,接收端按流重新组装。小接口不必遵守 HTTP/1.1 那种整份响应的排队顺序。

这里不是“每个帧都有一个非零 stream ID”。帧头有 stream identifier 字段,但 SETTINGS 等连接级帧使用 0;承载具体请求响应的帧才关联相应流。

HPACK 用静态表、动态表和可选的哈夫曼编码压缩头部。方法、路径、Cookie、User-Agent 等字段往往重复,复用表项能减少传输开销。静态表由协议预定义,动态表随着连接中的编码更新;不是简单把所有头都 gzip 一遍。它压缩头部,响应体的 gzip、br 属于另一件事。

2 解决了 1.1 响应排队带来的阻塞,但底层仍是一条有序 TCP 字节流。如果前面的 TCP 段丢了,后续字节即使已经到达,也不能越过缺口交给 HTTP/2。同一连接上的多个流都可能等这次重传。

因此,“HTTP/2 已经解决全部队头阻塞”是错的。HTTP/2 的分帧和流机制见 RFC 9113。

1.7 HTTP/3:把传输层换成 QUIC

HTTP/3 把 HTTP 语义映射到 QUIC。QUIC 基于 UDP,却自己实现可靠传输、流控制、拥塞控制和加密。UDP 不可靠,不代表建立在它上面的 QUIC 不可靠。

QUIC 可以在每个流内维护有序可靠交付。一个流缺了数据,其他流的数据仍可交付,不再被同一条 TCP 字节流的缺口整体挡住。不过多个流还共享连接级拥塞预算,丢包仍可能降低整个连接的发送速度。它消除了跨流的传输层交付阻塞,没有消除一切相互影响。参见 RFC 9000。

QUIC v1 使用 TLS 1.3 进行握手与密钥协商,不是先建 TCP 再叠 TLS。正常情况下新连接可以 1-RTT 建立;恢复连接时,满足条件可发送 0-RTT 早期数据。遇到 Retry、参数不匹配等情况还可能增加往返。

0-RTT 有重放风险。不能看到 GET 就自动判断安全,更不能用“加了 TLS”证明支付请求不会重复执行;服务端需要决定哪些操作允许早期数据,并采取防重放措施。TLS 对早期数据的限制见 RFC 8446 §8。

QUIC 用 connection ID 帮助识别连接,在允许迁移且路径验证通过等条件下,能应对 NAT 重绑定或 Wi-Fi 切换蜂窝网络。不能把它背成“换网一定不断”。协议也常在用户态实现,更方便随软件更新,但这不代表 QUIC 被禁止在内核实现。

HTTP/3 使用 QPACK 压缩头部。它把动态表更新放在专门的流中,并配合确认和阻塞流上限控制依赖。如果某个头部块引用的动态表项还没到,它仍可能暂时阻塞;QPACK 是为 QUIC 的多流环境管理这种依赖,不是保证所有头部解码永不等待。参见 RFC 9204。

HTTP/1.1 的响应排队、HTTP/2 的 TCP 队头阻塞和 HTTP/3 的独立流交付对比

图 2:三个版本分别在哪一层等待。

1.8 放在一起比较,以及 TLS 的关系

版本消息表达连接与并发头部压缩还要注意什么
HTTP/1.0文本请求行、头部默认短连接,部分实现可 keep-alive没有专用头部压缩常用连接关闭定界未知长度响应
HTTP/1.1文本请求行、头部默认持久连接,流水线响应按序没有专用头部压缩应用层响应队头阻塞
HTTP/2二进制帧一条 TCP 连接上的多流并发HPACKTCP 层缺口仍影响多个流
HTTP/3HTTP 帧映射到 QUIC 流QUIC 多流并发,底层 UDPQPACK仍有流内排序、共享拥塞控制和表项依赖

HTTP 版本与 TLS 版本不是一一对应的:

  • HTTP/1.0、1.1 可以明文传输,也可以通过 HTTPS 使用 TLS。SSL、TLS 1.0/1.1 是历史版本,现代部署应使用 TLS 1.2 或 1.3。
  • HTTP/2 协议有明文使用方式;浏览器网站访问通常使用 TLS。HTTP/2 over TLS 要求 TLS 1.2 或更高,并不限定只能用 1.2。
  • HTTP/3 依赖 QUIC;QUIC v1 的握手使用 TLS 1.3。

不要按笔记中的简写,把“HTTP/1.1 → SSL/TLS,HTTP/2 → TLS 1.2”背成固定绑定。

TLS 握手中的 ALPN 用于协商应用协议,比如 h2 和 http/1.1。HTTP/3 也在 QUIC 的 TLS 握手中通过 ALPN 协商 h3;Alt-Svc、HTTPS DNS 记录承担的是发现 HTTP/3 服务入口的作用。HTTP/3 的协议映射见 RFC 9114。

二、缓存:能直接使用副本,就少走一次网络

2.1 强缓存和协商缓存

缓存有可用且新鲜的副本时,浏览器可以直接读取,不向服务端确认。开发者工具可能显示 200 (from memory cache) 或 from disk cache;这不表示刚从网络收到一份 200。

需要验证副本是否仍有效时,浏览器发条件请求:

缓存机制服务端返回的头下次请求携带的头判断方式
新鲜度Cache-Control: max-age=3600、Expires通常不需条件头根据缓存年龄和寿命判断能否直接用
修改时间验证Last-ModifiedIf-Modified-Since比较修改时间
实体标签验证ETagIf-None-Match比较资源版本标识

对普通 GET,验证通过且内容未变时返回 304,不带响应体,浏览器使用已有副本;变了则通常返回 200 和新内容。304 不是“这次从网络又下载了一份页面”。

同时收到 If-None-Match 和 If-Modified-Since 时,优先处理实体标签条件。Last-Modified 的秒级精度可能区分不了一秒内多次修改,时间变化也不一定代表内容变化。ETag 由服务端生成,未必是内容哈希;多节点应该让同一表示的标签规则保持一致。

缓存判断发生在请求前,但条件请求真正拿到结果仍需要网络。把“检查协商条件”和“已经收到 304”都写在 DNS 前面,时间顺序就反了。

2.2 几个容易背错的 Cache-Control

指令含义
max-age=3600缓存寿命为 3600 秒;年龄计算也会考虑 Date、Age 等信息
no-cache可以存储,但复用前需要成功验证
no-store不允许缓存存储这次请求或响应
private允许浏览器这类私有缓存存储,共享缓存不应存储
must-revalidate过期后必须验证,不能随意拿过期副本应付
immutable在新鲜期内声明内容不会改变,可避免不必要的再验证

immutable 不意味着“过期后仍永久有效”。也别把 no-cache 写成“不缓存”,它允许保留副本。新鲜度和验证的具体规则可对照 MDN HTTP caching。

2.3 为什么上线了,用户还在看旧界面

HTML 入口还引用旧 JS、浏览器有旧副本、CDN 返回旧 HTML、Service Worker 使用旧缓存,都可能造成这个现象。这些层不一定按一个固定顺序逐个执行,Service Worker 还可以直接拦截请求。

常见做法是入口经常验证,静态资源用内容哈希命名:

/index.html               Cache-Control: no-cache
/assets/app.a3f9c2.js      Cache-Control: public, max-age=31536000, immutable

改代码后 JS 文件名变化,最新 HTML 就引用新地址。部署时先上传新资源,再切换 HTML,并保留一段时间的旧资源,避免用户打开的旧页面继续请求旧 chunk 时遇到 404。

强制刷新不能直接清掉 CDN 的缓存;Service Worker 也有自己的生命周期。skipWaiting() 会改变激活时机,但不能单独保证新旧资源版本一致。排查时可以查看请求 URL、Cache-Control、Age、ETag,以及 CDN 的命中头,先确定旧内容来自哪层。

缓存能减少流量、延迟和源站压力,也会增加更新一致性和隐私管理的负担。个性化内容尤其要避免错误进入共享缓存。

三、DNS:域名要解析成哪个地址

3.1 先查缓存,再找递归解析器

浏览器可能已有 DNS 缓存;系统解析器也有缓存和 hosts 等本地规则。没有可用结果时,再向配置的递归 DNS 解析器查询,它可能来自路由器、运营商或公共 DNS 服务。

这是面试中常说的“浏览器、操作系统、本地 DNS”几个层次,但 hosts 不是第四层缓存,优先级也不能对所有浏览器和操作系统一概而论。浏览器使用 DoH 时,部分流程还会改变。修改 hosts 后遇到旧地址,需要结合浏览器缓存、已有连接和系统解析器检查,不能保证永远立即生效。

Windows 可用以下只读命令观察系统状态:

ipconfig /displaydns
Resolve-DnsName ttzz1.com

3.2 递归和迭代,区别在谁负责查到底

客户端向递归解析器提出的问题是:“帮我拿到这个域名的答案。”解析器没有缓存时,通常进行迭代查询:

客户端 → 递归解析器
             ├→ 根服务器:拿到 .com 的委派信息
             ├→ .com 服务器:拿到 example.com 的权威服务器信息
             └→ 权威服务器:拿到记录,或继续处理别名
客户端 ← 返回解析结果,并在相应层按 TTL 缓存

迭代回应可能告诉查询方下一步去哪问,也可能直接给答案。已有中间委派缓存时,解析器不必每次都从根重新走。根服务器是 13 个逻辑标识对应的分布式部署,不能理解成全球只有 13 台机器。

这条主线依据 RFC 1034 的解析模型。

3.3 记录类型、TTL 与传输协议

记录作用
A / AAAAIPv4 / IPv6 地址
CNAME另一个域名的别名,需要继续解析目标
NS权威服务器信息
MX邮件接收服务器
TXT文本信息,常用于验证和邮件策略
HTTPS / SVCB描述服务绑定及相关参数,客户端可据此发现服务能力

CNAME 可能增加解析工作,但解析器也可能在同一响应中得到后续结果,不等于必然再付一次网络往返。传统 CNAME 不能与同名其他数据任意共存,因此域名顶点的 SOA/NS 会限制其使用;一些平台的 ALIAS 或 flattening 是另一种处理方式。

TTL 长会降低查询压力,也让旧记录存留更久。计划切换前提前降低 TTL,仍需等待原来的较长 TTL 消耗完,不能在切换瞬间调短就期待所有缓存立即失效。

传统 DNS 通常使用 UDP,也支持 TCP。没有 EDNS 时经典 UDP DNS 的报文上限是 512 字节;现代 EDNS 可以协商更大的 UDP 负载。响应被截断,客户端通常改走 TCP;区域传送也使用 TCP。不要说成“超过 512 字节就一定不能用 UDP”。

DoT 用 TLS,DoH 用 HTTPS 保护客户端与解析器之间的查询。它们不能单独保证解析器给的记录真实。HTTPDNS 是另一类应用自己通过 HTTP(S) 接口解析的方案;如果接口仍用不受保护的明文 HTTP,也不能凭名字保证防劫持。

DNS 调度可能参考递归解析器位置、EDNS Client Subnet 等信息,结果未必总是离用户最近的机器。

四、建立 TCP 和 TLS 连接

4.1 TCP 三次握手为什么不能只做两次

客户端                                      服务端
CLOSED                                      LISTEN
   ── SYN,seq=x ─────────────────────────→
SYN_SENT                                    SYN_RECV
   ←──────────── SYN+ACK,seq=y,ack=x+1 ───
   ── ACK,ack=y+1 ───────────────────────→
ESTABLISHED                                 ESTABLISHED

客户端在发送最后 ACK 时进入 ESTABLISHED,服务端要等收到这个 ACK 才进入 ESTABLISHED,双方不是在同一个瞬间完成。

握手让双方同步初始序列号,确认对方收到了自己的序列号,并协商 MSS、窗口扩大、SACK 等选项。三次握手的最后一次确认让服务端知道:客户端确实收到了服务端的 SYN,而非只看到一个可能来自旧连接的请求。

如果只做两次,服务端对迟到的旧 SYN 回复后就进入已建立状态,即使客户端根本不打算建立这个连接,也会形成错误状态。第三次让客户端有机会接受当前连接,或者拒绝不符合当前状态的回应。握手也提供了双向通信可用的证据,但不是对未来收发永远正常的保证。

SYN 占一个序列号,所以确认号是 x+1。FIN 也占一个,纯 ACK 不占。初始序列号不是每次固定为零,避免历史连接数据混淆,也降低可预测序列号带来的风险。

4.2 三次握手丢包,分别怎么办

丢失的位置典型后果与恢复
第一次 SYN客户端等不到回应,按定时器重传 SYN
第二次 SYN+ACK服务端重传 SYN+ACK;客户端也可能因等不到回应重传 SYN
第三次 ACK服务端仍停在 SYN_RECV,超时重传 SYN+ACK;客户端再次确认,或后续合法数据段携带的 ACK 完成确认

第三次纯 ACK 没有独立的“可靠消息重传定时器”,通常依靠对端 SYN+ACK 重传触发再次确认。服务端收到 SYN 后占用半连接资源,大量伪造或不完成握手的请求就可能造成 SYN flood。

SYN cookies 等机制可以在压力下减少半连接状态占用,但正常连接也要受队列上限、应用 accept 速度等因素约束。握手与状态转换详见 RFC 9293 §3.5。

4.3 TLS 1.2 握手,以 ECDHE 为例

TCP 建好后,HTTPS 还要协商加密。TLS 1.2 有不同密钥交换方式,我这里用带服务端证书的 ECDHE 完整握手讲,避免把老式 RSA 流程当作唯一答案。

阶段消息要完成的事
客户端发起ClientHello给出版本、加密套件、client_random、SNI、ALPN 等
服务端回应ServerHello、Certificate、ServerKeyExchange、ServerHelloDone选择参数,发证书和带签名的临时 ECDHE 参数
客户端验证与交换ClientKeyExchange、ChangeCipherSpec、Finished验证身份和签名,发送自己的临时公钥,导出密钥并验证握手
服务端完成ChangeCipherSpec、Finished切换加密状态并完成握手校验

双方用各自临时私钥和对方临时公钥计算相同的共享秘密,不会把会话密钥直接明文传过去。共享秘密、client_random、server_random 等进入密钥派生流程,得到后续记录加密所需的密钥。

证书里的长期公钥用于认证签名,并不必然用来加密 pre-master secret。用服务端 RSA 公钥加密 pre-master secret,是 TLS 1.2 旧式 RSA 密钥交换的另一条流程。ECDHE 的临时密钥用完销毁后,长期证书私钥将来泄漏,也不应让已记录的历史会话被直接解密,这就是前向安全性。

Finished 校验的是握手 transcript,并证明双方掌握对应密钥,不能只理解成一个“握手结束”的通知。TLS 1.2 的消息定义可查 RFC 5246 §7.4。

4.4 证书、SNI 和 TLS 1.3

证书校验要检查信任链、有效期、目标主机名、用途和签名等;吊销检查的策略取决于实现。证书由可信 CA 签发,但证书域名对不上当前站点,仍然不能通过。

HTTP Host 在加密连接建好后才发送,服务端选证书时还读不到它。SNI 把目标域名放在 ClientHello 扩展里,让共用 IP 的不同 HTTPS 站点能够选各自证书。普通 SNI 可被链路观察者看到,ECH 用于保护 ClientHello 中的相关信息。

TLS 1.3 通过更早发送 key share 等调整,把常规完整握手减少到 1-RTT;TLS 1.2 的上述完整握手通常需要 2-RTT。这里计算的是 TLS 自身,不包含之前的 TCP 建连,也不保证所有握手路径都恰好这么多。

1.3 移除了静态 RSA 密钥交换等旧机制。PSK 恢复和 0-RTT 是可选路径,且不能简单宣称“所有恢复连接都零往返、所有 PSK 模式都具有相同的前向安全性”。

五、发送 HTTP 请求,服务端返回响应

5.1 一次请求经过哪些处理

以 HTTP/1.1 为例:

GET /blog/post?id=1 HTTP/1.1
Host: www.ttzz1.com
Accept: text/html
Accept-Encoding: gzip, br
Cookie: session_id=abc123
If-None-Match: "a3f9c2"

请求行是方法、请求目标和版本,头部描述主机、内容协商、状态与条件等信息,空行结束头部。这个 GET 示例没有请求体。

Cookie 只有满足域、路径、Secure、SameSite 等条件时才会携带,不是任何请求都会带上所有 Cookie。HTTP/1.1 默认持久连接,不必额外写 Connection: keep-alive;HTTP/2、3 也不能照搬 1.1 的连接专用头。

服务端可能由 Web 服务器直接返回静态内容,也可能先过反向代理,再由应用路由分发到 handler,执行鉴权、参数验证和业务逻辑,访问数据库或其他服务,最后序列化响应。客户端的 TCP 对端可能是代理,不一定是执行业务的应用进程。

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 29
Cache-Control: no-cache
ETag: "a3f9c2"

<!DOCTYPE html><h1>Hello</h1>

这里的 29 字节按这段 ASCII 正文本身计算,不包含代码块展示时的末尾换行。实际服务要按真正发送的正文计算长度,不能把字符数直接当字节数,中文和压缩响应尤其如此。

5.2 常见状态码,以及重试的边界

状态码含义与常见追问
200 / 201 / 204成功、已创建、成功且没有响应内容
206成功返回部分表示,常用于范围下载
301 / 302永久或临时重定向;历史兼容行为可能把 POST 改为 GET
304条件 GET/HEAD 的缓存验证结果,不带消息内容
307 / 308临时或永久重定向,保留方法
400 / 401 / 403 / 404请求有问题、需要认证、拒绝访问、资源不存在
429请求过多,可能给出 Retry-After
500 / 502 / 503 / 504内部错误、网关收到无效上游响应、服务不可用、网关等待上游超时

401 表示缺少有效认证凭证。403 表示服务端理解请求但拒绝执行,可能是权限问题,也可能来自其他策略,不能限定成“一定知道你是谁”。

502、504 都发生在网关一侧,但看到错误不代表业务没执行。比如支付已成功,网关却没等到响应。客户端能否重试,要结合幂等键、操作语义、重试预算和结果查询判断。

5.3 收到 200,是不是整份数据已经收完

收到状态行或响应头,只能证明这部分信息已经到达。响应体可能还在路上,甚至中途断掉。

协议或消息类型怎么判断传输结束
HTTP/1.1,Content-Length收到声明的正文长度
HTTP/1.1,chunked收到完整分块终止结构及 trailer 结束
HTTP/1.1,关闭定界靠关闭判断;难以区分有序关闭导致的完整响应和被截短内容
HTTP/2根据流的 END_STREAM 等状态判断,声明长度时还需校验一致性
HTTP/3根据 QUIC 流的正常结束和 HTTP 消息规则判断,复位不等于正常完成

HEAD、204、304 等本来就没有响应内容,不能盯着 Content-Length 一条规则判断所有消息。拿到完整字节后,还可能存在解压、格式解析或业务校验错误。

面试回答可以落在这句:

状态码说明服务端给出的 HTTP 结果。是否收到完整响应,要看消息定界和流结束状态;是否完成业务,还要看应用协议和业务结果。

六、TCP:这条连接到底保证了什么

这一部分我想多写一点。能背出三次握手,只是开始;继续问“发送成功没有”“接收方读没读”“为什么连接关了还要等”,才会碰到 TCP 真正的边界。

6.1 send() 返回成功,对方就收到了吗

对普通 TCP socket,send() 成功通常表示本机协议栈接受了这次提交的若干字节。数据可能还在发送缓冲区里,受拥塞窗口、接收窗口和网络调度限制,甚至尚未发上链路。

把几个阶段拆开就清楚了:

应用调用 send()
  → 本机接受待发送字节
  → 数据发出并到达对端 TCP
  → 对端 TCP 确认,返回 ACK
  → 对端应用 read() 读取
  → 应用解析并执行
  → 事务提交 / 消息持久化
  → 返回应用层结果
观察到什么能说明什么还不能保证什么
send 返回 n本次有 n 字节被本机接受对端收到、应用读取、业务成功
TCP ACK 前移相应字节被对端 TCP 确认应用已经消费或持久化
应用层确认对端按应用协议确认到了某阶段阶段定义以外的持久性或下游结果
收到完整业务成功响应服务端声明业务成功仍需按该业务约定理解“成功”

ACK 丢失时,发送端也不能因“没看到 ACK”断言对端没收到。对端可能已经收到了,只是确认没回来,发送端随后重传,对端按序列号去重。

服务端 accept 后不 read,也不意味着所有数据都只留在客户端。对端内核仍可接收并 ACK,直到接收缓冲区逐渐填满,通告窗口缩小,发送端才会受到更明显的限制。

send() 还可能部分写。例如提交 100 KB,只接受了 16 KB,调用方必须处理剩余数据。阻塞 socket 也不应该无条件假设一次 send 全部完成。下面是 Python 普通阻塞 socket 的示例:

def send_all(sock, data: bytes) -> None:
    remaining = memoryview(data)
    while remaining:
        written = sock.send(remaining)
        if written == 0:
            raise ConnectionError("socket stopped accepting data")
        remaining = remaining[written:]

# 标准库已有对应封装:
# sock.sendall(data)

非阻塞 socket 遇到暂时不可写,还要处理 BlockingIOError 并等待可写事件,不要原地忙等。sendall 成功同样不代表业务执行成功;失败时还可能不知道此前究竟发送了多少。

这里不能照搬到所有语言 API。Java 阻塞 OutputStream.write(byte[]) 返回 void,不能写 offset += out.write(...);Java NIO 的 SocketChannel.write(ByteBuffer) 才有返回写入字节数的语义。

Linux 对 send 的说明明确不提供送达保证,见 send(2)。需要知道消息是否落库,就要设计“持久化后确认”的应用协议,而不能把 TCP ACK 当作数据库提交通知。

6.2 TCP 如何实现可靠传输

TCP 面向的是有序字节流。它不保留应用调用 send 的边界,应用要自己设计消息长度、分隔符或其他 framing。

假设发送端依次发送:

seq=1001,长度=1000  → 字节 [1001, 2001)
seq=2001,长度=1000  → 字节 [2001, 3001)
seq=3001,长度=1000  → 字节 [3001, 4001)

第二段丢失,第三段先到时,接收方还期待 2001,所以累计确认停在 ACK=2001。它可以缓存后面的乱序数据,但不能把缺口之后的字节直接按完整顺序交给应用。第二段补齐后,累计确认就可前移到 4001。

可靠传输依靠几种机制共同工作:

  • 序列号按字节编号,接收方据此重组、排序和识别重复数据。
  • 累计 ACK 告诉发送方,下一个期望收到的字节在哪里。
  • 校验和帮助检测传输损坏,但不是加密完整性保护,不能替代 TLS。
  • 发送端保留未确认数据,在检测到丢失时重传。
  • 窗口限制在途数据,避免无限发送压垮接收方或网络。

“可靠”不等于网络永久断开后也能必然送达。无法恢复时连接会失败,应用需要处理错误。可靠字节流同样不提供跨连接的业务去重,更不保证 exactly-once 执行。

6.3 超时重传:RTO 为什么不能是固定值

RTT 是一次往返的时间,RTO 是判断等待确认过久的超时阈值。阈值太短,正常抖动也会被误判丢包;太长,真正丢包后的恢复又慢。

经典计算保留平滑 RTT 和波动估计:

首次样本 R:
  SRTT = R
  RTTVAR = R / 2

后续样本 R':
  RTTVAR = (1 - β) × RTTVAR + β × |旧 SRTT - R'|
  SRTT   = (1 - α) × SRTT   + α × R'

  α = 1/8,β = 1/4
  RTO = SRTT + max(G, 4 × RTTVAR)

G 是时钟粒度。先用旧 SRTT 更新 RTTVAR,再更新 SRTT。具体实现还会设置超时上下限,不能把公式当成实际等待时间的全部规则。

发送数据后,重传定时器跟踪未确认数据。确认前移就更新定时器;超时则重传最早未确认的段,并通常把 RTO 翻倍,称为指数退避。继续失败时等待越来越长,避免网络出问题后反而被重传流量淹没。

重传后的 ACK 到底确认的是原包还是重传包,可能分不清。Karn 算法要求这类歧义样本不用于 RTT 更新;TCP timestamp 能帮助消除歧义时有例外。算法依据 RFC 6298。

TCP 重传超时与 HTTP 请求超时不是同一个东西。客户端业务超时返回失败后,TCP 可能还在尝试送数据,服务端也可能继续执行。要取消业务任务,需要应用自己定义取消语义。

6.4 快速重传、SACK 和现代丢包恢复

不必所有丢包都等 RTO。经典快速重传观察重复 ACK:收到三个重复 ACK,发送方推测前面出现缺口,尽早重传对应段。

这里是“原先那次 ACK 之后又收到三个重复 ACK”,不要和“总共看见三次同一个确认号”混成一件事。重复 ACK 也可能由乱序造成,所以是丢包推断,不能视为绝对证据。

上面三个段的例子只有一个后续段,可能产生不了足够多重复 ACK。因此快速重传不能覆盖所有丢包,RTO 仍然是兜底。

SACK 在握手中协商支持后,让接收方报告累计确认之后哪些字节区间已到达。例如:

累计 ACK = 2001
SACK 报告已收到 [3001, 5001)
发送方据此优先补 [2001, 3001) 的缺口

它减少多处丢包时盲目重传,但累计 ACK 仍负责确认连续前缀,SACK 不会让应用越过缺口读到乱序字节。选择确认定义见 RFC 2018。

现代实现还可能使用 RACK、尾部丢包探测等恢复机制,见 RFC 8985。面试先讲清超时和重复 ACK,再说明这是一套经典模型,实际内核不只这一种判据。

6.5 滑动窗口:为什么不发一段就等一次 ACK

如果每发一个段就停下来等确认,网络往返很长时,大多数时间都在空等。滑动窗口允许发送方连续发送一批数据,确认前移后再释放新的发送空间。

滑动窗口中的已确认、已发送未确认、可以发送和暂不可发送四个区域

图 3:先看字节处于哪个区域,再看窗口如何移动。

从发送端看,字节大致分成四段:

区域状态
已确认对端累计确认已经覆盖,可释放相应重传数据
已发送未确认正在途中或 ACK 还没回来,要保留以备重传
窗口内未发送在当前约束下可以继续发送
窗口外需要等窗口变化才允许发送

接收窗口 rwnd 由接收端在 TCP 头中通告,反映它愿意接收的字节范围。发送端也有自己的拥塞窗口 cwnd。简化理解:

允许的在途量 ≈ min(rwnd, cwnd)
还能新增的量 ≈ max(0, min(rwnd, cwnd) - 已在途未确认量)

第二行是帮助理解的预算模型,真实 TCP 还会考虑序列号右边界、SACK 恢复、pacing 等规则;不能拿它代替所有内核发送逻辑。发送缓冲区则是本机存放待发送数据的地方,不等于网络已经允许全部发出去。

举个简单数字:rwnd=32 KB,cwnd=10 KB,已经有 8 KB 在途,那么可新增预算约 2 KB。即使本机 send 又接受了 100 KB,也不会立刻把这 100 KB 全发上链路。

TCP 头原始窗口字段是 16 位。窗口扩大选项在握手阶段协商缩放,使高带宽、高延迟链路能使用更大窗口。其规则见 RFC 7323。

6.6 接收窗口变成 0,怎么恢复

对端应用读得慢,接收缓冲区填满,就可能通告零窗口。发送端要暂停普通新数据发送,不能靠重传不停轰炸。

应用读走一些数据后,接收端发窗口更新。但这个更新也可能丢失:接收端以为已经告诉对方有空位,发送端却还在等零窗口解除。

因此 TCP 有零窗口探测,也称 persist 机制。发送端间隔探测,促使接收端再次报告当前窗口,避免双方永远等下去。它解决窗口更新丢失的死锁,和空闲连接 keepalive、丢包重传定时器是不同问题。

6.7 流量控制和拥塞控制到底差在哪

对比项流量控制拥塞控制
要保护谁接收端,避免它处理不过来网络路径,避免队列和链路被压垮
主要变量接收窗口 rwnd拥塞窗口 cwnd,以及具体算法状态
主要信息来源对端通告可接收的范围ACK、丢包、RTT、ECN 等反馈
典型现象接收端不读,窗口越来越小链路排队、时延变大、丢包导致退让

rwnd 很大,说明接收方有空间,不代表沿途网络有足够带宽。cwnd 很大,也不代表接收方此刻还能接收。

还有个常见估算:带宽时延积 BDP = 带宽 × RTT。例如 100 Mbit/s、RTT=100 ms,BDP 大约 1.25 MB。想充分利用这样的链路,需要容纳一定在途数据;窗口仅有 10 KB 时,很难跑满。但持续堆出远超路径能力的数据又会增加排队延迟。

6.8 拥塞控制具体怎么做:先讲经典 Reno

面试常问慢启动、拥塞避免、快速重传和快速恢复。我先用 Reno 的经典模型解释,不把它说成所有系统正在执行的唯一算法。

慢启动阶段,cwnd 随确认增长。在确认模式等条件合适时,每个 RTT 近似翻倍,例如 1、2、4、8 个 MSS。这个从 1 开始的数字是教学例子,实际初始窗口不必是 1 MSS;“慢”是相对于一上来把所有数据全发出去,并非线性增长。

达到慢启动阈值 ssthresh 后,进入拥塞避免,增长变缓,经典模型大约每个 RTT 增加 1 MSS。用较缓的增速继续探索容量,避免刚恢复就再次把网络冲满。

检测到丢失后,超时和三个重复 ACK 的反应不同:

反馈Reno 的经典处理
RTO 超时降低 ssthresh,cwnd 降至 1 MSS,重传并重新慢启动
三个重复 ACK快速重传缺失段,降低 ssthresh,进入快速恢复
快速恢复中的额外重复 ACK临时增加窗口预算,使已经离开网络的数据释放空间
确认恢复范围的新 ACK退出经典 Reno 快速恢复,把 cwnd 收到 ssthresh,继续拥塞避免

经典快速恢复中,可把 ssthresh 设为不大于 max(FlightSize/2, 2×MSS) 的值,cwnd 临时设为 ssthresh + 3×MSS。FlightSize 是实际在途未确认数据,不一定等于 cwnd,所以“丢包后一律把当前 cwnd 除二”只是粗略说法。

超时通常被视为更严重的拥塞或传输失效信号;重复 ACK 则表明后续一些数据仍能通过,因此快速恢复不用像超时那样退回最小窗口。遇到多个丢包,NewReno、SACK 恢复会比这个基本模型复杂。经典规则见 RFC 5681。

实际系统可能运行 CUBIC、BBR 等算法。CUBIC 以时间和上次拥塞事件的窗口等构造三次函数,适合高速长距离网络;BBR 根据带宽、RTT 等建立路径模型。它们不能用一张 Reno 的“锯齿图”完全解释。CUBIC 的标准描述见 RFC 9438。

6.9 四次挥手:关闭的是两个发送方向

TCP 是全双工。A 不想再发,不表示 B 也已经发完,因此两个方向分别结束:

A 主动关闭                                    B 被动关闭
ESTABLISHED                                   ESTABLISHED
   ── FIN,seq=u ──────────────────────────→
FIN_WAIT_1                                    CLOSE_WAIT
   ←──────────────────────── ACK,ack=u+1 ──
FIN_WAIT_2                         B 仍可继续发送剩余数据
   ←──────────────────────── FIN,seq=v ────
TIME_WAIT                                     LAST_ACK
   ── ACK,ack=v+1 ────────────────────────→
等待 2MSL                                     CLOSED
CLOSED

图是典型分开的四个报文,ACK 和 FIN 也可能合并成一个,因此线上不保证每次都恰好四个包。

收到 FIN 表示对方的发送方向结束,接收方把先前数据读完后,再读会得到 EOF。这时本地发送方向仍可以继续工作。API 要表达这种半关闭,通常使用 shutdown 的写方向关闭;直接 close 的具体行为还受实现和选项影响。

B 的 CLOSE_WAIT 等待的是本地应用关闭发送方向。如果代码忘了关闭 socket,就可能长时间停在这里。它和 A 侧由协议定时器管理的 TIME_WAIT,排查方向不同。

6.10 TIME_WAIT 为什么要等 2MSL

MSL 是 Maximum Segment Lifetime,报文段在网络中的最大生存时间假设。它不是 RTT,也不是 IP TTL:TTL 主要按路由跳数消耗,不能直接当秒数。

TIME_WAIT 有两个目的。

第一,保留旧连接状态,以便再次确认对端重传的 FIN。最后一个 ACK 如果丢了,B 还停在 LAST_ACK,会重传 FIN。A 不能立刻忘记这条连接,需要能识别重传并再发 ACK。收到重复 FIN 后,TIME_WAIT 的等待通常会重新计时。

第二,让旧连接残留的报文有时间退出网络,降低同一四元组很快重建连接后,把旧数据误认成新连接数据的风险。四元组是源 IP、源端口、目的 IP、目的端口。

2MSL 为旧报文及其可能引发的反向报文保留了一个双向时间尺度;“一个 MSL 让旧段消失,一个 MSL 留给反向回应”是帮助理解的模型。不要把它当成“FIN 重传 RTO 一定小于 MSL”或者“等 2MSL 就在任意恶劣网络中保证关闭成功”的数学证明。

TCP 四次挥手及最后 ACK 丢失后,通过重传 FIN 再确认的 TIME_WAIT 时间线

图 4:如果最后一个 ACK 丢失,仍保留状态的一方可以再次确认 FIN。

可以直接这样回答:

主动正常关闭连接的一方通常进入 TIME_WAIT,等待 2MSL。一方面,如果最后的 ACK 丢失,它还能确认对端重传的 FIN;另一方面,让旧连接报文在网络中消退,避免同一四元组立即复用时发生混淆。MSL 是报文最大生存时间假设,实际系统的等待长度由实现决定。

同时关闭时,双方都可能进入 TIME_WAIT。协议状态和重传处理见 RFC 9293 §3.6。

为什么有人说两分钟,也有人说一分钟?因为协议解释与系统实现不能混在一起。Linux 常见实现的 TIME_WAIT 长度是 60 秒,可查看内核的 TCP_TIMEWAIT_LEN 定义。这不是所有操作系统的统一值。

6.11 为什么会有大量 TIME_WAIT

先看它在哪台机器、对应哪类连接。TIME_WAIT 通常集中在主动正常关闭方,可能是客户端,也可能是服务端或代理。

常见来源有:

  • 请求量很高,但每次都新建短连接。
  • HTTP 或数据库客户端没有连接池,或者没有正确归还、复用连接。
  • 服务端主动关闭很多连接,例如 keep-alive 空闲超时、请求次数上限、连接寿命限制。
  • 客户端与代理的超时设置不协调,不断关闭和重建。
  • 故障后的高频重试放大建连与关闭量。
  • 部署、实例下线或流量突增,引起一段时间的连接周转。

对反向代理还要分清两条链路:浏览器到代理是一条连接,代理到上游是另一条。代理作为上游客户端频繁建连,可能在这一侧产生很多 TIME_WAIT,并不证明浏览器那侧没有复用。

稳定条件下可粗估:

TIME_WAIT 数量 ≈ 每秒进入 TIME_WAIT 的连接数 × 状态驻留时间

假设每秒 2000 条连接进入此状态,驻留 60 秒,就可能看到约 12 万条。这个数字是估算示例,受连接复用、实现、清理和流量变化影响。

所以“TIME_WAIT 很多”首先说明连接周转很快,不自动等于攻击或系统故障。

6.12 大量 TIME_WAIT 会有什么后果

最值得关注的是出站连接的临时端口压力。某台客户端不断用同一个源 IP 连接同一个目的 IP 和端口,旧四元组尚不可复用时,新连接需要其他临时源端口;可用组合有限,就可能建连失败。

不能把它简单说成“全机器一共只有 65535 条 TCP 连接”。TCP 连接由四元组区分,不同目的地址可以在系统允许的条件下使用相同源端口;实际限制还取决于临时端口范围、绑定方式和复用规则。

影响需要理解的边界
出站端口耗尽或建连失败尤其是单一源地址高频连接固定上游时
内核状态占用TIME_WAIT 有状态表、内存和查找管理成本,但不等于完整活跃 socket
NAT / 防火墙状态压力这些设备有自己的映射和超时,不能假定与主机同步释放
重建连接开销重复 TCP、TLS 握手,可能还失去已有拥塞窗口带来的收益

应用 close 后,TIME_WAIT 一般不继续占用原应用的文件描述符,所以它通常不是“进程 FD 泄漏”的直接证据。同样,服务端很多客户端都访问 443,不会因为 TIME_WAIT 就把监听 443 端口“用完”。

6.13 如何排查与处理

先确认数量、增长速率、出现在哪个地址和端口,以及是否伴随建连错误、延迟升高、重试增多。只看一张 netstat 截图,缺少时间和业务上下文。

Linux 可做只读观察:

ss -s
ss -tan state time-wait
ss -tin
sysctl net.ipv4.ip_local_port_range
sysctl net.ipv4.tcp_tw_reuse

Windows 可按状态计数:

Get-NetTCPConnection -State TimeWait | Group-Object RemoteAddress, RemotePort
netsh int ipv4 show dynamicport tcp

处理优先回到连接生命周期:复用 HTTP 客户端、连接池、合理的 keep-alive,确认响应体被读取或正确关闭,使连接能返回池中;让客户端、代理、上游的超时协调,并限制失败重试。

确有容量问题时,再结合系统评估临时端口范围、源地址或实例扩容。内核参数要针对版本、拓扑和连接方向验证,不能照抄“优化三件套”。

特别记住几个坑:

  1. tcp_fin_timeout 管的是孤儿 FIN_WAIT_2 连接,不是调 TIME_WAIT 时长。
  2. tcp_tw_reuse 是满足安全条件时的复用规则,取值及适用范围依实现,不等于删除全部 TIME_WAIT。
  3. SO_REUSEADDR 主要解决相应地址绑定场景,不能当作客户端临时端口问题的通用解法。
  4. 不要为了降低数字就强行缩小 TIME_WAIT 状态上限,也别通过 RST 式关闭绕过正常关闭语义,可能丢弃尚未交付的数据。
  5. 旧文章里的 tcp_tw_recycle 已从现代 Linux 移除,尤其不能照搬到 NAT 环境。

前两项和 TIME_WAIT 数量上限的语义可核对 Linux IP Sysctl 文档。

6.14 TIME_WAIT 与 CLOSE_WAIT,面试怎么区分

状态所处位置主要在等什么大量出现时先查什么
TIME_WAIT通常在主动正常关闭方协议等待期结束,期间可再确认 FIN连接周转、池复用、端口容量
CLOSE_WAIT收到对方 FIN 的一方本地应用结束发送方向是否忘记 close、异常路径漏释放
FIN_WAIT_2本方 FIN 已确认后对端最终结束发送方向对端关闭行为、半关闭是否符合预期
LAST_ACK被动关闭方已发 FIN对方确认自己的 FIN最后 ACK 是否丢失、路径是否异常

TIME_WAIT 由协议在正常条件下自动走完;CLOSE_WAIT 通常要应用采取关闭动作。把二者都回答成“调参数缩短等待”会错过真正的问题。

6.15 TCP 粘包、Nagle 与三个 keep-alive

所谓粘包和拆包,是应用期待“每次 send 对应一次 read”,但 TCP 只给有序字节流。两个短消息可能一起读到,一个大消息也可能分多次读到。解决办法在应用协议:长度前缀、分隔符或固定长度。HTTP 的消息定界就是这一类设计。

Nagle 会在有未确认数据时抑制部分小段发送,尝试合并数据;延迟 ACK 又可能让接收方稍等再确认,在某些小请求交互中叠加延迟。低延迟场景可能使用 TCP_NODELAY,但要结合发送模式测量,不能统一断言所有业务都应该关闭 Nagle。

最后把名字很像的几件事分开:

机制用途
HTTP keep-alive多次 HTTP 交换复用连接
TCP keepalive在配置的空闲周期后探测连接是否仍可通信
应用心跳判断对端应用是否按业务协议响应,也可帮助保活映射

对端 TCP 回探测包,不代表业务线程没有卡死;应用超时、任务状态和 TCP 连通性需要分别管理。

七、数据包怎么封装,一跳一跳走到对面

7.1 HTTPS 数据的逐层封装

这部分在每次发送时都发生。对于 HTTPS over TCP:

HTTP 消息
  ↓ TLS 编码成加密记录
TCP 字节流 / TCP 段:源端口、目的端口、序列号、ACK、窗口等
  ↓
IP 包:源 IP、目的 IP、TTL / Hop Limit 等
  ↓
链路帧:当前链路的源地址、目的地址和校验信息
  ↓
物理信号:网线、光纤、无线介质

TLS 记录、TCP 段、IP 包和应用消息之间不是一一对应关系。一个 HTTP 响应可以被拆进多个 TLS 记录和 TCP 段,一个 TCP 段也不必正好放一个完整 HTTP 消息。

HTTP/3 的这条链变成 HTTP/3 帧、QUIC、UDP、IP、链路帧,不经过 TCP。

设备的典型角色主要依据
二层交换机帧的 MAC 地址
路由器目的 IP 与路由表
四层负载均衡IP、端口及连接状态等
HTTP 反向代理主机、路径、头部等应用信息

这是基础模型,现实中还有防火墙、NAT、隧道和组合设备。不能因为讲分层,就断言网络设备永远不会查看其他层的字段。

7.2 ARP 问的是最终服务器,还是网关

IPv4 在以太网这类链路上发包,需要知道下一跳的 MAC。系统先按路由表选出接口和下一跳,然后查邻居缓存;没有可用映射时,发 ARP 请求。

同一链路直连的目标,可以问目标自己的 IP:

本机:192.168.1.100/24
目标:192.168.1.5

广播:谁是 192.168.1.5?
回应:我是 192.168.1.5,我的 MAC 是 ...

普通 ARP 请求广播出去,目标通常单播回应,本机缓存映射。广播不会被路由器按普通 IP 路由转发到远端去查服务器。

目标如果是公网地址,路由表可能选择默认网关 192.168.1.1。此时问的是网关的 MAC:

IP 包目的地址:远端服务器 IP
这一跳帧的目的 MAC:网关 MAC

准确回答不是“一跨网段必然问默认网关”,而是问路由所选下一跳的 MAC;常见家庭网络里,这个下一跳就是默认网关。更具体路由存在时可能选择其他路由器。

ARP 的基本请求响应机制见 RFC 826。

7.3 ARP 缓存失效了怎么办

邻居缓存避免每发一个包都广播。条目老化后,系统会按邻居状态机重新确认或重新解析。stale 不一定立即删除,也不一定马上阻止第一次使用;具体老化时间依系统和状态配置而定,不能背成统一“缓存 20 分钟”。

解析期间数据可能暂时排队,多次尝试仍失败时通信会报错或超时。ARP 缺少强身份认证,伪造映射可造成中间人攻击。网络侧可通过动态 ARP 检测等措施缓解;HTTPS 的证书验证也有助于避免把链路劫持直接变成明文业务泄露。

IPv6 不用 ARP,而是通过 ICMPv6 的邻居发现机制处理相应问题。

7.4 路由:目标 IP 一样,下一跳为什么一直变

路由器依据目的 IP 做最长前缀匹配:

目的:203.0.113.5

0.0.0.0/0       → 默认出口
203.0.113.0/24  → 下一跳 A
203.0.113.0/25  → 下一跳 B

该目的同时匹配时,/25 比 /24 更具体,优先选择。

每经过一个路由器,IPv4 TTL 减少,耗尽后丢弃并通常返回 ICMP Time Exceeded。traceroute 逐渐增大 TTL,利用沿途回应推测路径;部分节点不回应、路径变化时,结果也会不完整。

在无 NAT 等改写的普通路由中,端到端源、目的 IP 通常保持,而每一跳的链路封装重新建立。以太网链路上的源、目的 MAC 随下一跳变化。隧道、NAT 等场景则需要另外讨论。

7.5 NAT:内网地址如何和公网通信

内网常用私有 IPv4 地址,如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16,这些地址不能作为普通公网路由目标直接访问。

出口设备可做地址转换,家庭网络常见的是带端口转换的 NAPT/PAT:

内网连接:192.168.1.100:54321 → 198.51.100.20:443
出口转换:203.0.113.7:40001   → 198.51.100.20:443
响应回来:依据映射状态,把目标还原为 192.168.1.100:54321

多个内网连接因此能共用公网地址。Basic NAT 主要转地址,NAPT 才通过地址加端口复用;不能把所有 NAT 都等同为改端口。

映射过期后,已有通信也可能受影响。外网主动连接没有映射的内网服务,需要端口转发、反向连接或其他穿透方案,实际还要考虑防火墙策略。

服务端看到的地址可能是出口设备或代理地址,不能用一个 IP 精确代表一个用户。

八、浏览器收到 HTML 后,怎样把它显示出来

8.1 解析、布局、绘制和合成

浏览器会边接收边解析 HTML。解析时发现 CSS、JS、图片等资源,再安排更多请求,这些请求又回到缓存、解析地址和连接复用的流程。

HTML 字节 → 解码、解析 → DOM
CSS 内容  → 解析       → CSSOM
DOM + CSSOM → 样式计算与渲染 / 布局相关树
            → 布局:算位置和尺寸
            → 绘制:生成绘制记录并栅格化
            → 合成:组合相应图层,显示到屏幕

“DOM 和 CSSOM 合成渲染树”是适合面试的概括,真实浏览器内部有更细的树和阶段。

渲染树也不应描述成“只包含肉眼可见元素”:visibility: hidden 的元素仍占据布局空间;display: none 会让元素不生成相应盒子,其子树也不参与通常布局。伪元素可以参与渲染,却没有对应普通 DOM 元素。

渲染流程的入门说明可看 MDN:How browsers work。

8.2 CSS 与 JS 分别阻塞什么

资源典型行为
需要用于当前显示的 CSS可能阻塞渲染;不直接阻塞 HTML 解析,但也可能让依赖它的脚本等待
普通外部经典 script解析器通常等待下载和执行,期间暂停继续解析
defer 经典 script并行下载,解析完成后按文档顺序执行,DOMContentLoaded 等它们
async script并行下载,可用后执行,次序不保证;执行期间仍可能占住主线程
module script默认有类似 defer 的行为,还涉及模块依赖加载

所以“async 不阻塞解析”只能用来概括下载阶段,执行时仍可能打断解析。把所有 JS 都放在底部,也不意味着资源能尽早下载;defer 往往能兼顾发现时机和执行顺序。

8.3 重排、重绘与合成

改变几何属性,例如尺寸、位置、字体,通常需要重新布局;只改变颜色、背景等通常无需改几何,但要重绘;某些 transform、opacity 动画在合适的图层条件下,可以主要通过合成更新。

这是一组典型规律,不是浏览器对所有 CSS 操作的硬保证。transform 也可能受图层分配等影响,过多提升图层还会占内存。

一个常见性能问题是布局抖动:刚写样式,又读 offsetWidth 等几何信息,迫使浏览器立即计算布局,再继续写。可以把读和写分批,减少反复强制布局;动画优先选择适合合成的属性,再用性能工具验证。

8.4 页面显示、DOMContentLoaded、load 不是同一时刻

DOMContentLoaded 发生在文档解析完成、需要等待的 defer 与模块脚本执行之后。它不直接等所有图片,但相关脚本对样式表的等待可能间接推迟它,见 MDN DOMContentLoaded。

load 要等文档及会阻塞此事件的依赖资源,例如普通图片和样式表;懒加载图片等不在“所有资源都必须结束”的简单定义里。页面可以在这两个事件前显示一部分内容,也可以在 load 后仍等待业务接口才可用。

LCP 观察最大内容元素的呈现,INP 观察交互到下一次绘制的延迟,CLS 观察布局偏移。它们分别反映加载、响应和稳定性,也不能只凭某个事件就证明用户体验良好,指标定义见 Web Vitals。

可以采用的优化包括:尽早发现关键 CSS,合理使用 preload、preconnect 和 dns-prefetch,减少主线程长任务,为图片预留尺寸,用 font-display 控制字体加载显示,以及延迟处理首屏外内容。preconnect 节省的是提前完成的连接准备时间,不保证固定“省一个 RTT”,也不应该对所有域名滥用。

九、面试时怎样把整条链路讲顺

我会先说主线,再根据追问展开 TCP、TLS 或渲染,不在第一句话就把所有名词堆上去:

浏览器先识别并解析 URL,确定协议、主机、端口和请求目标,片段部分留在客户端处理。它检查可用缓存,能直接使用新鲜副本就不用发网络请求;需要验证或没有缓存时,才继续网络访问。

浏览器通过自身和系统解析能力拿到目标地址,没有缓存时由递归解析器查询相应 DNS 服务器。已有合适连接可以复用;新建 HTTPS over TCP 连接时,先三次握手同步序列号,再进行 TLS 握手、验证证书并协商密钥与应用协议。如果使用 HTTP/3,这部分换成 QUIC。

请求在应用层构造,经过 TLS、传输层、IP 和链路层封装。主机和沿途路由器依据路由选下一跳,IPv4 以太网链路上可通过 ARP 获取下一跳 MAC,出口还可能做 NAT。服务端或代理收到请求后完成解析、路由和处理,返回 HTTP 响应。

浏览器判断响应状态与缓存规则,边接收边解析 HTML,再加载 CSS、JS 和其他资源,构建 DOM、计算样式,经过布局、绘制和合成显示页面。收到 200 不等于正文已经收完;页面能显示也不等于业务已经可交互。响应结束后连接可能继续复用,真正关闭时再走 TCP 关闭流程。

下面这些追问可以用来检查自己是不是只背了流程:

追问回答落点
HTTP/2 为什么还会阻塞?HTTP 流多路复用,但共用有序 TCP 字节流
HTTP/3 基于 UDP,为什么还可靠?QUIC 自己实现确认、恢复、流控制和拥塞控制
send 成功说明什么?本机接受字节;送达、消费、事务结果是更晚的阶段
ACK 丢了会重复执行业务吗?同连接 TCP 按序列号去重;应用重试仍需业务幂等
TIME_WAIT 为何 2MSL?再确认重传 FIN,并隔离旧连接残留报文
TIME_WAIT 多该先改参数吗?先查主动关闭方、连接周转、复用和实际资源压力
流量控制与拥塞控制?rwnd 保护接收方,cwnd 等机制约束网络负载
ARP 查询谁?路由选择的下一跳;直连目标或常见默认网关
304 出现在 DNS 前吗?缓存检查可以,真正条件请求与 304 需要网络
网关超时能直接重试支付吗?结果可能未知,需幂等键或结果查询

写到最后,我更愿意把这道题当成一条检查线索。沿着它问“当前是谁在负责”“确认到哪一步”“失败后谁来恢复”,很多原本分散的概念就接起来了。

FIELD NOTES / DISCUSS

文章讨论

读完后,欢迎留下你的补充、疑问或不同看法。

◎ 292 浏览

全部评论 (0)

正在读取评论…

    GUEST IDENTITY

    设置访客身份

    评论、回复与留言板将复用这份身份。

    选择头像