一个数据包从「敲下回车」到「页面渲染」的完整旅程
覆盖面试高频网络知识点,故事读完 = 知识闭环
周五晚上,你在浏览器地址栏敲下 https://taobao.com 然后按了回车。这一瞬间,一个「请求包」诞生了——我们叫它小包。
小包很兴奋,但它啥也不知道:taobao.com 在哪?怎么过去?路上安不安全?
于是,一场横跨整个网络协议栈的冒险开始了。
第一步:问路 —— DNS 解析
小包先去找 DNS(域名系统)。这就像出门前先查地图——把 "taobao.com" 翻译成一个 IP 地址,比如 140.205.220.96。
查询顺序:浏览器缓存 → OS缓存 → 本地hosts → 本地DNS服务器 → 根DNS → 顶级域DNS → 权威DNS,一层层问下去,直到找到答案。
「从输入URL到页面展示」这道题的第一步就是DNS解析。很多人答成直接建TCP连接,直接丢分。记住:先问路,再上路。
小包拿到了IP地址,但它不能直接闯进去。TCP是个讲礼貌的协议,必须先「握手」确认双方都在、都准备好了。
想象你和朋友打电话:
三步,缺一不可:
第一次(SYN):客户端发送 SYN=1,携带一个随机初始序列号 seq=x。客户端进入 SYN_SENT 状态。
第二次(SYN+ACK):服务端收到后回复 SYN=1, ACK=1,自己的序列号 seq=y,确认号 ack=x+1。服务端进入 SYN_RCVD 状态。
第三次(ACK):客户端发送 ACK=1,ack=y+1。双方进入 ESTABLISHED,通道建好。
核心答案:三次握手的本质是确认双方的收发能力都正常。
两次只能证明「客户端能发,服务端能收」,但服务端不知道自己发出去的包客户端能不能收到。第三次握手就是客户端告诉服务端:「你的发送能力OK,我收到了」。
另一个关键原因:防止历史重复连接。假如客户端之前发过一个SYN但网络延迟了,后来又发了新的SYN。两次握手的话,服务端收到旧SYN就直接建连接,浪费资源。三次握手下,客户端收到旧SYN的ACK后会发现序列号对不上,发RST拒绝,避免脏连接。
TCP通道建好了,但你输入的是 https://,这意味着通信必须加密。小包需要先和服务端协商一套加密方案——这就是 TLS 握手。
打个比方:TCP握手是你和快递员确认了彼此身份,TLS握手是你们再商量好用什么密码来锁快递箱,防止中途被别人打开。
| 维度 | HTTP | HTTPS |
|---|---|---|
| 端口 | 80 | 443 |
| 安全性 | 明文传输,裸奔 | TLS加密,防窃听/篡改/冒充 |
| 性能 | 无额外开销 | 多一次TLS握手(TLS 1.3 只需1-RTT) |
| 证书 | 不需要 | 需要CA颁发的数字证书 |
关键理解:TLS握手用非对称加密(RSA/ECDHE)交换密钥,之后通信用对称加密(AES)。因为非对称加密慢、对称加密快,两者结合是最优解。
加密通道就绪。小包终于可以携带真正的 HTTP 请求出发了。浏览器构造了一个 GET / HTTP/1.1 请求,附上 Host、User-Agent、Cookie 等头部信息。
| 维度 | GET | POST |
|---|---|---|
| 语义 | 获取资源(幂等) | 提交数据(非幂等) |
| 参数位置 | URL query string | 请求体 body |
| 长度限制 | 浏览器/服务器限制URL长度(通常2KB~8KB) | 无固有限制 |
| 缓存 | 可被缓存、收藏、记录历史 | 默认不缓存 |
| 安全性 | 参数暴露在URL,不适合敏感数据 | 参数在body,相对不暴露(但不加密仍不安全) |
高频追问:GET和POST在TCP层面有区别吗?本质上没有,都是TCP连接上发的数据。所谓"GET产生一个TCP包,POST产生两个"是某些浏览器实现(先发header再发body)的行为,
| 版本 | 核心改进 |
|---|---|
| HTTP/1.0 | 每次请求都要新建TCP连接(短连接),请求完就断 |
| HTTP/1.1 | ① 持久连接(keep-alive,默认开启)复用TCP连接 ② 管道化(pipelining)允许连续发请求不等响应 ③ 但响应必须按序返回 → |
| HTTP/2.0 | ① 多路复用(一个TCP连接上并行多个stream,彻底解决应用层队头阻塞) ② 头部压缩(HPACK) ③ 服务端推送 ④ 二进制分帧(不再是纯文本) |
注意:HTTP/2 解决了应用层队头阻塞,但底层 TCP 的队头阻塞仍在(一个丢包会阻塞整个连接的所有stream)。这也是 HTTP/3 改用 QUIC(基于UDP)的原因。
小包上路了,但网络不是只有它一个。就像高速公路,车太多会堵。TCP用两个机制保证不「撑爆网络」也不「饿死自己」。
一、滑动窗口(Flow Control)—— 接收方的容量
接收方通过 ACK 告诉发送方:「我的缓冲区还剩多少空间」,这个值叫 rwnd(接收窗口)。发送方不能发超过这个量的未确认数据。
类比:你朋友说「我桌上只能放5个快递,别一次寄太多」。你送了3个,他拆了2个腾出空间说「现在能放4个了」,窗口就「滑动」了。
二、拥塞控制(Congestion Control)—— 网络的承载力
滑动窗口管的是「接收方吃不吃得下」,拥塞控制管的是「路上堵不堵」。发送方维护一个 cwnd(拥塞窗口),实际发送窗口 = min(rwnd, cwnd)。
① 慢启动(Slow Start):cwnd 从 1 开始,每收到一个 ACK,cwnd 翻倍(指数增长)。虽然叫"慢",其实增长很快。
② 拥塞避免(Congestion Avoidance):当 cwnd 达到 ssthresh(慢启动阈值),改为每个 RTT 只 +1(线性增长),小心翼翼地探测上限。
③ 快重传(Fast Retransmit):收到 3 个重复 ACK(说明某个包丢了但后面的包还在到),立即重传丢失的包,不等超时。
④ 快恢复(Fast Recovery):快重传后,ssthresh = cwnd/2,cwnd = ssthresh(而不是从1重新慢启动),然后继续拥塞避免。
面试官问「TCP怎么保证可靠传输」,很多人只答重传。完整答案要包含四个机制:① 序列号+确认号 ② 超时重传+快重传 ③ 滑动窗口(流量控制) ④ 拥塞控制。这四个缺一不可。
淘宝服务器处理完请求后,返回一个 HTTP 响应。响应里最重要的就是状态码——它决定了小包的命运。
| 类别 | 含义 | 高频考点 |
|---|---|---|
| 2xx | 成功 | 200 OK · 201 Created · 204 No Content |
| 3xx | 重定向 | 301 永久重定向(可缓存) · 302 临时重定向 · 304 Not Modified(命中缓存) |
| 4xx | 客户端错误 | 400 Bad Request · 401 未认证 · 403 Forbidden · 404 Not Found · 405 Method Not Allowed |
| 5xx | 服务端错误 | 500 Internal Error · 502 Bad Gateway · 503 Service Unavailable · 504 Gateway Timeout |
301 vs 302:301是永久,浏览器会缓存新地址下次直接访问;302是临时,下次还访问原地址。SEO角度301会转移权重。
401 vs 403:401是「你没登录/认证失败」(身份不明),403是「我知道你是谁,但你没权限」(身份明确但越权)。
502 vs 504:都和网关/代理有关。502是网关从上游收到了无效响应;504是网关等上游超时了没收到响应。
HTTP 是无状态的——服务端不记得「你」。但你登录了淘宝,刷新页面还是登录状态,谁帮你记住的?
Cookie:服务端通过 Set-Cookie 响应头给浏览器塞一张「身份小纸条」,之后浏览器每次请求都自动带上这张纸条。数据存在客户端。
Session:服务端自己开一个「小本本」记你的登录状态,只给你一个 SessionID(通常放在Cookie里传递)。数据存在服务端。
| 维度 | Cookie | Session |
|---|---|---|
| 存储位置 | 客户端(浏览器) | 服务端(内存/Redis) |
| 安全性 | 用户可见可篡改,较低 | 用户拿不到数据,较高 |
| 大小限制 | 4KB左右 | 取决于服务端存储 |
| 跨域 | 受同源策略限制 | 不涉及(服务端存储) |
| 分布式问题 | 无(客户端自带) | 有(多节点要共享Session,通常用Redis) |
进阶追问:现在更主流的做法是用 JWT(JSON Web Token) 替代 Session。JWT 把用户信息编码成一个签名Token放在客户端,服务端无状态验签即可,天然解决分布式Session问题。trade-off 是 Token 无法主动失效(除非引入黑名单机制)。你做的秒杀系统如果用的 Dubbo 微服务架构,面试官很可能从这里追问到你的认证方案。
页面数据传完了。TCP要断开连接,但不能直接挂电话——万一对方还有话没说完呢?所以需要四次挥手。
为什么是四次不是三次?因为TCP是全双工的——两个方向的通道独立关闭。收到对方的FIN只表示对方不发了,但自己可能还有数据要发,所以 ACK 和 FIN 不能合并成一步(除非恰好同时没数据了,实际中偶尔也会三次)。
TIME_WAIT 是什么:主动关闭方发完最后一个ACK后,不直接关,而是等待 2MSL(MSL = 报文最大存活时间,通常60秒)。
为什么要等:
① 确保最后的ACK到达:如果这个ACK丢了,服务端会重发FIN,客户端还在TIME_WAIT就能重发ACK。如果直接关了,服务端重发的FIN会收到RST,导致异常。
② 让旧连接的包在网络中消散:防止新连接收到上一次连接的残留数据包。
生产问题:高并发短连接场景下,大量 TIME_WAIT 会
小包走的是 TCP 这条「保镖护送路线」,它还有个兄弟叫 UDP包,性格完全相反——不握手、不确认、不管丢包,拿到数据就直接扔出去。
这不是缺点,而是设计选择。
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接,直接发 |
| 可靠性 | 可靠(重传、确认、排序) | 不可靠(丢了就丢了) |
| 有序性 | 保证有序 | 不保证 |
| 传输方式 | 字节流 | 数据报(保留消息边界) |
| 头部开销 | 20字节 | 8字节 |
| 速度 | 较慢(可靠有代价) | 快(轻量无负担) |
| 典型场景 | HTTP、文件传输、邮件 | DNS、视频直播、游戏、QUIC |
「TCP一定比UDP好吗?」不是。视频通话中丢一帧画面,用户几乎感知不到,但如果等TCP重传那一帧,整个画面就卡住了。所以实时性要求高、能容忍少量丢包的场景用UDP。
你的 RPC 框架用的 Netty 默认走 TCP,面试官可能追问「Dubbo 为什么用TCP不用UDP」——因为 RPC 调用要求每个请求都必须有完整响应,可靠性是刚需。
小包的旅程快结束了。让我们把整个「从输入URL到页面展示」的闭环合起来:
面试官问这道题时,按「网络层 → 传输层 → 应用层 → 浏览器渲染」四个层次回答,每层点到关键词即可。不需要每个细节都展开,但每层都要提到,展示你的全栈视野。如果面试官对某层感兴趣,他会追问,那时候再深入。
作为Java后端,重点说 ②③④⑤⑥ 这几步,浏览器渲染部分简单带过就行。
把所有知识点压缩成一句话记忆:
| 知识点 | 一句话记忆 |
|---|---|
| 三次握手 | 确认双方收发能力 + 防历史连接 |
| 四次挥手 | 全双工所以两个方向分别关闭 |
| TIME_WAIT | 等2MSL:保最后ACK到达 + 旧包消散 |
| TCP vs UDP | 可靠有序 vs 快速轻量,按场景选 |
| 拥塞控制 | 慢启动(指数) → 拥塞避免(线性) → 快重传(3次dupACK) → 快恢复 |
| 滑动窗口 | 接收方告诉发送方"我还能吃多少" |
| HTTP状态码 | 2成功 3跳转 4你的错 5我的错 |
| HTTPS | HTTP + TLS = 非对称换密钥 + 对称加密通信 |
| HTTP/2 | 多路复用 + 头部压缩 + 二进制分帧 + 服务端推送 |
| GET vs POST | 语义不同(获取 vs 提交),本质都是TCP数据 |
| Cookie vs Session | 客户端纸条 vs 服务端小本本,Session常靠Cookie传ID |
| URL→页面 | DNS → TCP → TLS → HTTP → 服务端 → 响应 → 渲染 → 挥手 |
以下问题在大厂面试中高频出现,答案需要你把前面的知识融会贯通。先自己想,再展开看参考答案。
Q1:三次握手中,如果第三次 ACK 丢了,会发生什么?
服务端视角:服务端停留在 SYN_RCVD 状态,没收到 ACK,会认为自己的 SYN+ACK 丢了,于是重传 SYN+ACK(Linux 默认重传 5 次,间隔指数退避)。重传次数耗尽后,服务端放弃该半连接,回收资源。
客户端视角:客户端已经进入 ESTABLISHED。如果客户端此时发送数据,数据包里自然携带 ACK 信息,服务端收到后也会进入 ESTABLISHED——所以连接最终大概率还是能建立。
追问陷阱:面试官常接着问「那 SYN Flood 攻击是什么原理?」——攻击者伪造大量 SYN 但不回 ACK,服务端半连接队列被塞满,正常用户无法建连。防御手段:SYN Cookie(不在半连接队列保存状态,把状态编码进 SYN+ACK 的序列号里)。
Q2:服务端出现大量 CLOSE_WAIT 是什么原因?和大量 TIME_WAIT 有什么本质区别?
CLOSE_WAIT 堆积:被动关闭方收到对端 FIN 后进入 CLOSE_WAIT,等待自己调用 close()。如果大量堆积,几乎一定是代码 bug——程序没有正确关闭 socket(连接泄漏)。这是
TIME_WAIT 堆积:主动关闭方的正常行为,不是 bug。在高并发短连接场景(如 HTTP/1.0)下,大量 TIME_WAIT 会耗尽可用端口。
| 维度 | CLOSE_WAIT 堆积 | TIME_WAIT 堆积 |
|---|---|---|
| 本质 | 代码 bug(未 close) | 协议正常行为 |
| 出现在 | 被动关闭方 | 主动关闭方 |
| 解决方式 | 修代码,确保关闭连接 | tcp_tw_reuse / 长连接池 / 调大端口范围 |
面试加分点:能说出「CLOSE_WAIT 查代码,TIME_WAIT 调参数」这个结论,说明你有线上排障经验。
Q3:TCP 的 keepalive 和 HTTP 的 Keep-Alive 是同一个东西吗?
| 维度 | TCP keepalive | HTTP Keep-Alive |
|---|---|---|
| 层级 | 传输层(内核实现) | 应用层(HTTP 头部) |
| 作用 | 心跳探测,检测死连接 | 复用 TCP 连接,减少握手开销 |
| 默认行为 | Linux 默认 2 小时才发第一个探测包 | HTTP/1.1 默认开启 |
| 解决的问题 | 对端主机崩溃/网线断了,我怎么知道? | 每个请求都重新三次握手太慢 |
追问:「那为什么应用层(Dubbo、gRPC)还要自己做心跳,不直接用 TCP keepalive?」——因为 2 小时太长,中间件需要秒级感知节点下线。而且 TCP keepalive 只能检测连接是否存活,无法检测应用层是否健康(比如进程假死、线程池满)。
Q4:一个 TCP 连接上能同时发多个 HTTP 请求吗?HTTP/1.1、HTTP/2、HTTP/3 分别是什么情况?
| 版本 | 能否并行 | 队头阻塞 |
|---|---|---|
| HTTP/1.1 | pipelining 允许连续发,但响应必须按序返回 | |
| HTTP/2 | 多路复用,同一连接上并行多个 stream | 应用层无,但 |
| HTTP/3 | 基于 QUIC(UDP),每个 stream 独立 | 彻底解决:丢包只影响对应 stream |
关键理解:HTTP/2 的队头阻塞发生在传输层——TCP 把所有 stream 的数据看作一个字节流,一个包丢了整个流都要等重传。这就是 HTTP/3 必须换底层协议的根本原因。
面试加分点:如果你能画出「HTTP/2 一个 TCP 连接 → 多个 stream → TCP 层只看到一个字节流 → 丢包阻塞所有 stream」这条因果链,面试官会认为你真正理解了协议演进的驱动力。
Q5:TCP「保证可靠传输」这句话准确吗?TCP 的可靠性边界到底在哪?
但它不保证:
① 应用层已读取:ACK 代表内核收到了数据,不代表应用层 read() 了。如果接收方进程在读取前崩溃,数据就丢了。
② 业务已处理:你的 HTTP 响应返回 200,只代表数据到了对端内核。对端应用可能还没解析完就挂了。
③ 网络永远可达:如果网络中断时间超过 TCP 重传上限,连接会被 reset,数据照样丢。
面试高光时刻:能说出「TCP 的可靠性是传输层承诺,不是端到端承诺」这句话,说明你对分层模型有深刻理解。这也是为什么关键业务(如支付)需要应用层确认机制(如消息队列的 ACK、幂等接口 + 对账)来兜底。
从DNS出发,经过TCP握手、TLS加密、HTTP传输、拥塞控制,到达服务端拿到响应,最终回到浏览器渲染成页面,再通过四次挥手优雅告别。
整个网络知识闭环,你现在都走过一遍了。