网络/操作系统 面试题


1. TCP 三次握手与四次挥手

题目: TCP 为什么是三次握手而不是两次或四次?四次挥手时为什么有 TIME_WAIT 状态?

追问1:大量 TIME_WAIT 状态连接出现在服务器上,是什么原因?如何优化? 追问2:TCP 的 KeepAlive 机制是什么?它和 HTTP 的 Keep-Alive 有区别吗?

💡 答案:

主问题: 三次握手的根本目标是:在不可靠的网络上建立可靠的连接,同时防止旧的连接请求(来自历史网络延迟)突然到达服务端导致错误。如果只有两次握手,客户端发送 SYN,服务端回 SYN+ACK 就认为连接建立——但此时客户端可能没收到 SYN+ACK(丢包),服务端已经为这个连接分配了资源。更关键的问题是:网络中的一个历史 SYN 包延迟到达服务端,服务端以为这是一个新连接请求,回复 SYN+ACK 后建立连接,客户端早已关闭不会有数据发送,服务端的资源就白白浪费了。三次握手中,服务端还需要等待客户端一个 ACK 才能确认建立连接——这个 ACK 可以确认客户端确实收到了 SYN+ACK 且愿意建立连接,同时能区分”这是一个活跃请求”和”这是一个历史延迟请求”。

四次挥手中的 TIME_WAIT 状态持续 2MSL(Maximum Segment Lifetime,通常是 60 秒),主要作用有两个:一是确保最后一个 ACK 能被对方收到——如果最后 ACK 丢失,对方会重发 FIN,TIME_WAIT 状态下还可以再次回复 ACK;二是”等待网络中所有属于这个连接的包都消失”——防止新建立的连接(相同的四元组)收到旧连接的延迟包造成混淆。

追问1: 大量 TIME_WAIT 状态的连接通常出现在”主动关闭连接”的一方。如果是 Web 服务器主动关闭(比如使用了短连接,服务器每次都主动断开 TCP),就会积累大量 TIME_WAIT,耗尽端口号或系统资源。主要优化方案:一是改用长连接(HTTP Keep-Alive),复用 TCP 连接避免频繁断开;二是将主动关闭权交给客户端(比如 Nginx 中 keepalive_timeout 设短一些让客户端先关);三是调整内核参数——net.ipv4.tcp_tw_reuse 允许多个 TIME_WAIT 连接重用(只对客户端有效),tcp_tw_recycle(在新版本 Linux 中已废弃,因为对 NAT 环境不兼容),tcp_max_tw_buckets 限制 TIME_WAIT 最大数量,多余的连接直接清理掉。

追问2: TCP KeepAlive 是传输层的保活机制——它定期发送一个空的 ACK 探测报文,确认对端是否仍然可达。如果对端无响应,KeepAlive 重试几次后 OS 认为连接断开,通知应用层。HTTP Keep-Alive 是应用层的连接复用机制——它表示一个 TCP 连接可以同时承载多次 HTTP 请求-响应,而不是一个请求就关闭一个连接。两者的层级和目的完全不同,TCP KeepAlive 感知”物理链路是否畅通”;HTTP Keep-Alive 是”业务层要不要复用这个连接”减少握手开销。在实际工程中 TCP KeepAlive 默认间隔很长(7200 秒),通常不适合直接用于快速故障检测,需要调整 tcp_keepalive_time 等参数,或者在应用层做心跳(如 WebSocket ping/pong)。

📌 易错点 / 加分项:

  • TIME_WAIT 只出现在”主动关闭方”,是被动关闭方没有这个状态
  • CLOSE_WAIT 大量出现是一种典型问题——被动关闭方收到了 FIN 但应用层没关连接,导致泄漏
  • tcp_tw_recycle 在 Linux 4.12 已正式废除——面试时说用了它反而不好

2. HTTP 协议演进与性能优化

题目: 从 HTTP/1.1 到 HTTP/2 再到 HTTP/3,每一代协议解决了什么核心问题?

追问1:HTTP/2 的多路复用解决了 HTTP/1.1 的什么痛点?它又是如何引入”队头阻塞”的? 追问2:HTTP/3 为什么把传输层从 TCP 换成了 QUIC?QUIC 的 0-RTT 连接建立是怎么做到安全的?

💡 答案:

主问题: HTTP/1.1 的核心痛点是”一个 TCP 连接同一时间只能处理一个请求”(队头阻塞),浏览器为了并发加载页面资源必须开 6-8 个 TCP 连接——但这增加了连接建立成本、也容易造成网络拥塞。HTTP/2 引入了多路复用——一个 TCP 连接上同时跑多个 Stream,每个 Stream 可以独立传输请求和响应,互不影响。解决了应用层的队头阻塞,大量减少了连接数。HTTP/2 还引入了头部压缩(HPACK)和服务端推送。但 HTTP/2 引出了新的问题:因为所有 Stream 跑在一个 TCP 连接上,如果 TCP 中某一个包丢失,整个 TCP 连接的所有 Stream 都被阻塞等待重传——这就是 TCP 层的队头阻塞,HTTP/2 无法解决这个问题。HTTP/3 彻底放弃了 TCP,改用 QUIC 协议(基于 UDP),每个 Stream 独立拥塞控制——单个 Stream 丢包只影响该 Stream,不影响其他 Stream 的数据传输。

追问1: HTTP/1.1 的队头阻塞是串行请求的问题:浏览器请求 A → 服务器响应 A → 浏览器请求 B → 服务器响应 B,A 没处理完 B 就得排队。Pipelining 试图让浏览器一口气发多个请求然后服务器按序响应,但实现复杂和中间件兼容性差导致几乎没有实际部署。HTTP/2 的帧(Frame)设计解决了这个问题:一个 TCP 连接包含多个 Stream,每个 Stream 由多个 Frame 组成(DATA 帧、HEADERS 帧)。发送方可以将不同 Stream 的 Frame 交错发送,接收方根据 Frame 头中的 Stream ID 组装回各自的请求/响应。所以 HTTP/2 中多个请求的 Frame 可以在同一个 TCP 连接上交错传输,不再需要等 A 完成再发 B。但又产生了 TCP 层的队头阻塞——底层 TCP 的滑动窗口中有任何一个包丢失,窗口内所有后续数据都无法被应用层读取。

追问2: QUIC 解决的核心问题是 TCP 层的队头阻塞。QUIC 基于 UDP,在网络层之上实现了 Stream 独立传输——每个字节流有个 Stream ID,不同 Stream 的数据在 UDP 中独立成包传输,某个 Stream 的包丢失只阻塞这个 Stream,不影响其他 Stream 的数据到达。0-RTT 连接建立是 QUIC 的另一个杀手特性:客户端和服务端在首次连接时交换密钥并缓存(客户端保存服务端的密钥配置),之后重新连接时可在第一条 SYN 包中就带上业务数据,省去了一次 RTT。0-RTT 的安全保障是通过”客户端在生成的会话票证中加密存储上次协商的密钥参数”来实现——服务端验证通过后即可解密 0-RTT 数据。但 0-RTT 有”重放攻击”的风险——攻击者截获 0-RTT 包重放,服务端会重复处理。防御措施是:幂等请求允许 0-RTT,非幂等请求(转账、扣费)必须等握手完成。

📌 易错点 / 加分项:

  • HTTP/3 基于 QUIC,QUIC 基于 UDP——但不是简单的 UDP,QUIC 重新实现了类似 TCP 的可靠传输+拥塞控制在用户空间
  • HTTP/2 的服务端推送并没有被主流浏览器完全启用(因为浏览器缓存的 hit 率等现实问题)
  • H3 的普及现状:Google、YouTube、Cloudflare 等已经全面支持,但企业内网还很少