1. 引言
在技术面试中,网络基础与缓存算法是两大必问方向。本文将围绕三个高频考点展开:TCP 三次握手与四次挥手、从输入 URL 到页面展示的完整过程、以及LRU 缓存淘汰策略的实现与应用。这三者看似独立,实则共同构成了现代 Web 应用在数据传输、资源加载和性能优化层面的核心知识体系。
2. TCP 三次握手
三次握手是 TCP 协议建立可靠连接的过程,发生在客户端与服务器之间。其核心目的是同步双方的序列号、确认双方的收发能力正常。
2.1 三次握手过程
- 第一次握手——SYN:客户端发送一个 SYN(同步)报文给服务端,其中包含一个随机生成的初始序列号 seq=x,客户端进入
SYN_SENT状态。 - 第二次握手——SYN+ACK:服务端收到 SYN 报文后,确认客户端的发送能力和自己的接收能力均正常。服务端发送一个 SYN+ACK 报文,ack=x+1 表示确认已收到客户端的数据,seq=y 为服务端的初始序列号。此时服务端进入
SYN_RCVD状态。 - 第三次握手——ACK:客户端收到 SYN+ACK 后发送 ACK 报文,ack=y+1,seq=x+1。此时客户端进入
ESTABLISHED状态。服务端收到 ACK 后也进入ESTABLISHED状态,至此双向连接建立。
2.2 关键问题
- 为什么不是两次握手?两次握手只能确认客户端的发送能力和服务端的接收能力,无法确认服务端的发送能力和客户端的接收能力。如果只有两次握手,服务端在收到 SYN 后就会认为连接已建立,无法保证客户端是否收到了自己的回复,也无法防止旧的重复连接请求导致的资源浪费。
- 为什么不是四次握手?第二次握手时服务端可以将 SYN 和 ACK 合并到一起发送,效率更高。分开发送会增加一次不必要的往返,没有实际意义。
- SYN 泛洪攻击:攻击者伪造大量不存在 IP 的 SYN 报文,服务端在
SYN_RCVD状态分配大量资源后得不到回复,最终耗尽半连接队列。防御方式包括 SYN Cookie、减少超时重传次数、增加半连接队列容量等。
3. TCP 四次挥手
四次挥手是 TCP 连接断开的过程,由于 TCP 的连接是全双工的,每个方向都需要单独关闭,因此需要四次报文交互。
3.1 四次挥手过程
- 第一次挥手——FIN:主动关闭方发送 FIN 报文(finish),seq=u,表示本端数据已发送完毕,主动方进入
FIN_WAIT_1状态。 - 第二次挥手——ACK:被动关闭方收到 FIN 后回复 ACK,ack=u+1,seq=v。被动方进入
CLOSE_WAIT状态,主动方收到 ACK 后进入FIN_WAIT_2状态。此时半关闭状态:主动方不能再发送数据,但仍可接收被动方发来的数据。 - 第三次挥手——FIN:被动关闭方数据发送完毕后,也发送 FIN 报文,seq=w,ack 仍为 u+1。被动方进入
LAST_ACK状态。 - 第四次挥手——ACK:主动方收到 FIN 后回复 ACK,ack=w+1,seq=u+1,并进入
TIME_WAIT状态。等待 2MSL(最大报文生存时间的两倍)后进入CLOSED状态。被动方收到 ACK 后立即进入CLOSED状态。
3.2 关键问题
- 为什么要四次挥手?TCP 是全双工的,当一方完成数据发送时可以发送 FIN 告知对方,但对方可能还有未发送完的数据,因此需要先确认收到 FIN,等自身数据也发送完毕后再发送 FIN。这两个阶段分开执行,共需四次报文。
- TIME_WAIT 为什么要等待 2MSL?一是确保最后一个 ACK 能被被动方接收到,如果 ACK 丢失,被动方会重发 FIN,主动方可以重新回复 ACK;二是让本次连接的所有报文从网络中消失,防止旧连接的报文被新连接误收。2MSL 保证了两个方向上的报文都失效。
- 大量 TIME_WAIT 状态如何解决?常见手段包括:开启
tcp_tw_reuse复用 TIME_WAIT 连接、调整tcp_tw_recycle、缩短tcp_fin_timeout、使用长连接或连接池减少频繁建连断连。
3.3 三次挥手与四次挥手的对比
| 维度 | 三次握手 | 四次挥手 |
|---|---|---|
| 阶段 | 连接建立 | 连接释放 |
| 报文数量 | 3 个 | 4 个 |
| 触发方 | 客户端主动发起 | 任意一方均可发起 |
| 可否合并 | ACK 与 SYN 合并 | ACK 与 FIN 分开发送 |
| 特殊状态 | SYN_SENT、SYN_RCVD | TIME_WAIT、CLOSE_WAIT |
4. 从输入 URL 到页面展示的完整过程
这是一道经典的综合性面试题,考察候选人对网络协议、浏览器原理、操作系统和前端渲染的全链路理解。整个过程可分为以下几个阶段:
4.1 URL 解析
浏览器首先解析用户输入的 URL,判断是搜索关键词还是合法 URL。如果输入的是非 URL 文本,浏览器会使用默认搜索引擎进行搜索;如果是合法 URL,则拆分出协议(如https)、域名(如www.example.com)、端口(默认 443 或 80)、路径和查询参数等。
4.2 DNS 域名解析
浏览器按以下顺序查找域名的 IP 地址:浏览器 DNS 缓存→操作系统 hosts 文件→操作系统 DNS 缓存→本地 DNS 服务器。如果本地 DNS 服务器未命中,则进行递归查询,从根域名服务器逐级向下解析,最终获取目标 IP 地址。
DNS 解析优化手段包括:DNS 预解析(<link rel="dns-prefetch">)、CDN 就近解析、减少域名数量等。
4.3 建立 TCP 连接
获取到目标服务器的 IP 地址后,浏览器通过三次握手与服务器建立 TCP 连接。如果需要 HTTPS,则先通过 TCP 连接进行 SSL/TLS 四次握手:客户端发送 ClientHello → 服务端回复 ServerHello 和证书 → 客户端验证证书并交换密钥 → 双方确认加密通信开始。
4.4 发送 HTTP 请求
连接建立后,浏览器发送 HTTP 请求报文,包含请求行(方法、路径、协议版本)、请求头(Cookie、User-Agent、Accept 等)和请求体(如果是 POST/PUT 请求)。HTTP/2 和 HTTP/3 对这部分有进一步优化,如多路复用、头部压缩等。
4.5 服务器处理请求并响应
请求经过负载均衡、反向代理到达后端服务器。服务器解析请求,执行业务逻辑(查询数据库、调用微服务等),生成 HTTP 响应报文返回给浏览器。响应报文包含状态行(如200 OK、301 Moved Permanently)、响应头(Content-Type、Cache-Control、Set-Cookie 等)和响应体(HTML、JSON 等)。
4.6 浏览器解析与渲染
浏览器收到 HTML 后启动渲染流程:
- 构建 DOM 树:解析 HTML 字节流,构建文档对象模型树。
- 构建 CSSOM 树:解析 CSS 样式表,构建 CSS 对象模型树。
- 执行 JavaScript:遇到
<script>标签时暂停 DOM 解析,下载并执行 JS 代码(async和defer可改变执行时机)。JS 可能会修改 DOM 和 CSSOM。 - 构建渲染树:合并 DOM 树和 CSSOM 树,剔除不可见元素(如
display:none),生成渲染树。 - 布局:计算每个渲染树节点的几何位置和尺寸。
- 绘制:将各节点绘制到屏幕上,按图层分别进行光栅化,最终合成显示。
4.7 四次挥手断开连接
页面数据加载完毕后,如果使用 HTTP/1.0 的短连接,此时会进行四次挥手断连;HTTP/1.1 默认使用长连接(Connection: keep-alive),在一定时间内保持 TCP 连接以复用。
5. LRU 缓存淘汰算法
LRU(Least Recently Used,最近最少使用)是一种经典的缓存淘汰策略:当缓存空间满时,优先淘汰最近最少被访问的数据。其核心思想是认为近期被访问过的数据在未来也更有可能被访问,而长期未被访问的数据在未来被访问的概率较低。
5.1 核心实现:哈希表 + 双向链表
LRU 要求在O(1)时间复杂度内完成get和put操作。哈希表负责快速定位节点,双向链表负责维护访问顺序。每次访问或插入时将节点移到链表头部,淘汰时从链表尾部删除。
5.2 Java 实现示例
public class LRUCache { private Map<Integer, Node> map; private Node head, tail; private int capacity; static class Node { int key, value; Node prev, next; Node(int key, int value) { this.key = key; this.value = value; } } public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(); head = new Node(0, 0); tail = new Node(0, 0); head.next = tail; tail.prev = head; } public int get(int key) { Node node = map.get(key); if (node == null) return -1; moveToHead(node); return node.value; } public void put(int key, int value) { Node node = map.get(key); if (node != null) { node.value = value; moveToHead(node); return; } Node newNode = new Node(key, value); map.put(key, newNode); addToHead(newNode); if (map.size() > capacity) { Node removed = removeTail(); map.remove(removed.key); } } private void addToHead(Node node) { node.next = head.next; head.next.prev = node; head.next = node; node.prev = head; } private void removeNode(Node node) { node.prev.next = node.next; node.next.prev = node.prev; } private void moveToHead(Node node) { removeNode(node); addToHead(node); } private Node removeTail() { Node node = tail.prev; removeNode(node); return node; } }5.3 LRU 的变体与优化
- LRU-K:记录每个数据最近 K 次访问的时间戳,淘汰最久远的一次。相比基础 LRU,更能抵抗偶发性访问对缓存页面的污染。
- Two-Queue LRU:维护两个队列,数据先进入
FIFO队列,被再次访问时移入LRU队列,淘汰时优先淘汰FIFO队列。 - Redis 中的近似 LRU:Redis 默认采用近似 LRU 算法,随机采样若干个键,淘汰其中最久未访问的一个,既降低了实现复杂度,也在大多数场景下保持了较好的命中率。
- LRU 在操作系统中的应用:操作系统使用近似 LRU(如时钟算法)进行页面置换,通过硬件支持的引用位来近似追踪页面访问情况,避免维护完整链表的开销。
5.4 LRU 在浏览器缓存中的应用
浏览器的内存缓存(Memory Cache)在一定程度上遵循 LRU 策略。当浏览器缓存达到上限时,会优先淘汰最近最少使用的资源(如图片、CSS、JS 文件)。结合 HTTP 缓存头(Cache-Control、ETag 等),可以对资源进行更精确的缓存控制。前端开发中可以通过合理设置缓存策略、减少缓存体积,以及利用 Service Worker 进一步实现对缓存淘汰的精细化控制。
6. 总结
TCP 的三次握手和四次挥手奠定了互联网数据传输的可靠性基础,从 URL 到页面的渲染过程则串联起了域名解析、网络传输、浏览器内核三大体系。LRU 缓存策略在操作系统、数据库、Redis 以及浏览器缓存中都有着广泛应用。这三个知识点相互关联,共同构成了后端与前端工程师必备的基础能力。建议在实际面试准备中做到:能画流程图讲清握手挥手过程、能举出 TIME_WAIT 和 CLOSE_WAIT 的真实场景、能手写 LRU 的 O(1) Java 实现、能从 URL 出发串联完整的网络与渲染链路。