网络高频面试题
Summary
把网络面试题拆成 6 轮对话 完成,每轮聚焦一个主题,避免一次性信息过载。每道题按统一结构讲:一句话结论、核心原理、图示/流程、常见追问、面试表达。
Dialogue Plan
- 第一轮:TCP/UDP 核心
- TCP 和 UDP 区别
- TCP 为什么可靠
- 三次握手、四次挥手
- TIME_WAIT、CLOSE_WAIT
- 粘包/拆包
- 第二轮:TCP 进阶
- 滑动窗口
- 拥塞控制
- 半连接队列/全连接队列
- SYN Flood
- TCP 参数调优思路
- 第三轮:HTTP/HTTPS
- HTTP 和 HTTPS 区别
- HTTPS 握手
- 对称加密、非对称加密、证书
- HTTP 1.0/1.1/2/3 区别
- GET/POST、状态码、长连接
- 第四轮:DNS/CDN/代理/网关
- DNS 解析过程
- DNS 劫持、污染
- CDN 原理和回源
- 正向代理、反向代理
- 网关、负载均衡、反向代理区别
- 第五轮:Socket、IO 模型、Netty
- Socket 是什么
- BIO/NIO/AIO
- select/poll/epoll
- Reactor 模型
- Netty 为什么高性能
- 零拷贝和网络 IO 的关系
- 第六轮:网络排查与架构场景
- 接口超时怎么排查
- TIME_WAIT/CLOSE_WAIT 过多怎么处理
- QPS 上不去的网络原因
- 浏览器输入 URL 到页面展示
- 高并发网关设计
- 百万 WebSocket 连接设计
Answer Format
每一轮都采用这个格式:
- 先给面试结论:30 秒内能说清楚。
- 再讲底层机制:说明为什么是这样。
- 配图或流程图:用文本图画清链路。
- 补充常见追问:面试官可能继续问什么。
- 给标准表达:可以直接背诵但不生硬。
Acceptance Criteria
完成后你会得到一套完整网络面试资料,覆盖:
- 八股基础题
- 原理深入题
- 架构场景题
- 线上排查题
- 高级工程表达方式
Assumptions
默认面向 Java 后端 / 高级后端 / 架构岗 面试准备,回答深度按中高级标准组织。每轮内容控制在一个对话内,讲透但不堆太多无关细节。
第一轮:TCP/UDP 核心。这一轮重点是网络面试里命中率最高的一组题。
1. TCP 和 UDP 区别
面试结论:
TCP 是面向连接、可靠、有序、面向字节流的传输协议;UDP 是无连接、不保证可靠、不保证顺序、面向报文的传输协议。TCP 适合可靠传输场景,UDP 适合低延迟、可容忍丢包的场景。
核心对比:
| 对比项 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 可靠传输 | 不保证可靠 |
| 顺序 | 保证有序 | 不保证有序 |
| 数据边界 | 字节流,无消息边界 | 报文,有消息边界 |
| 开销 | 较大 | 较小 |
| 速度 | 相对慢 | 相对快 |
| 典型场景 | HTTP、HTTPS、MySQL、Redis | DNS、直播、游戏、语音视频 |
图示:
1 | TCP: 客户端 ── 建立连接 ──> 服务端 客户端 ── 可靠传输 ──> 服务端 客户端 ── 断开连接 ──> 服务端 UDP: 客户端 ── 直接发包 ──> 服务端 不关心连接、不关心是否一定送达 |
常见追问:UDP 一定不可靠吗?
不是。UDP 协议层不保证可靠,但应用层可以自己实现可靠性,比如:
1 | 序列号 ACK 确认 超时重传 滑动窗口 丢包检测 |
比如 QUIC 就是基于 UDP 实现了可靠传输、拥塞控制和加密能力。
标准表达:
TCP 把可靠性交给传输层,应用开发简单,但协议开销更大;UDP 把可靠性选择权交给应用层,延迟低、灵活性高,所以适合实时通信、音视频、游戏、DNS 这类场景。
2. TCP 为什么可靠?
面试结论:
TCP 的可靠性来自序列号、确认应答、超时重传、滑动窗口、流量控制、拥塞控制和校验和。
核心机制:
1 | 1. 序列号:保证数据可以按顺序重组 2. ACK 确认:接收方告诉发送方收到哪些数据 3. 超时重传:数据丢了会重新发送 4. 滑动窗口:提高传输效率 5. 流量控制:防止把接收方打爆 6. 拥塞控制:防止把网络打爆 7. 校验和:检测数据是否损坏 |
图示:
1 | 发送方 接收方 Seq=100, Len=50 ────────────> <──────────── ACK=150 含义:100 到 149 的数据我收到了,下次从 150 开始发 |
常见追问:ACK=150 是什么意思?
表示接收方已经收到 150 之前的所有字节,下一次希望收到从 150 开始的数据。
标准表达:
TCP 的可靠不是靠单一机制,而是一组机制配合实现的。序列号解决顺序问题,ACK 和重传解决丢包问题,滑动窗口提升吞吐,流量控制保护接收端,拥塞控制保护网络。
3. TCP 三次握手
面试结论:
三次握手的核心目的是确认客户端和服务端的发送、接收能力都正常,并同步双方的初始序列号。
流程图:
1 | 客户端 服务端 SYN, seq=x ────────────────> 服务端知道:客户端能发 <─────────────── SYN+ACK, seq=y, ack=x+1 客户端知道:服务端能收、能发 ACK, ack=y+1 ────────────────> 服务端知道:客户端能收 |
为什么不是两次?
如果只有两次,服务端无法确认客户端是否具备接收能力。
更关键的是,历史失效连接请求可能导致服务端误建立连接。
1 | 旧 SYN 延迟到达服务端 服务端回复 SYN+ACK 如果两次就建立连接,服务端会创建一个无效连接 |
第三次 ACK 可以让客户端确认这个连接是否真实有效。
标准表达:
三次握手不是为了形式上多一次,而是为了让双方都确认彼此的收发能力,并交换初始序列号。两次握手服务端无法确认客户端的接收能力,也更容易受到历史重复 SYN 的影响。
4. TCP 四次挥手
面试结论:
四次挥手是因为 TCP 是全双工协议,客户端和服务端两个方向的数据通道需要分别关闭。
流程图:
1 | 主动关闭方 被动关闭方 FIN ────────────────> <─────────────── ACK <─────────────── FIN ACK ────────────────> |
为什么握手是三次,挥手是四次?
因为连接建立时,服务端可以把 SYN 和 ACK 合并发送。
断开连接时,被动关闭方收到 FIN 后,可能还有数据没发完,所以通常先回 ACK,等数据发送完后再发 FIN。
1 | 建立连接:SYN + ACK 可以合并 断开连接:ACK 和 FIN 通常不能立刻合并 |
标准表达:
TCP 是双向通信,关闭一个方向不代表另一个方向也关闭。主动方发 FIN 只是表示自己不再发送数据,但仍然可以接收数据;被动方处理完剩余数据后,再发送 FIN,所以通常需要四次挥手。
5. TIME_WAIT 是什么?为什么需要?
面试结论:
TIME_WAIT 是主动关闭方在发送最后一个 ACK 后进入的等待状态,主要作用是保证最后一个 ACK 能被对方收到,并让旧连接中的报文在网络中自然过期。
流程位置:
1 | 主动关闭方发送最后一个 ACK | v 进入 TIME_WAIT 等待 2MSL | v 真正关闭连接 |
为什么要等 2MSL?
MSL 是报文最大生存时间。
等待 2MSL 有两个目的:
1 | 1. 如果最后一个 ACK 丢了,对方会重发 FIN,主动关闭方可以再次 ACK 2. 防止旧连接残留报文影响后续相同四元组的新连接 |
四元组是:
1 | 源 IP + 源端口 + 目标 IP + 目标端口 |
TIME_WAIT 过多怎么办?
常见处理:
1 | 1. 客户端侧 TIME_WAIT 多通常正常 2. 服务端避免主动关闭连接 3. 开启连接复用、长连接 4. 调整内核参数,但要谨慎 5. 排查是否短连接过多 |
标准表达:
TIME_WAIT 不是异常,它是 TCP 为可靠关闭连接设计的保护机制。它的核心价值是处理最后 ACK 丢失和旧报文残留问题。真正要关注的是为什么出现大量短连接,而不是简单粗暴地消灭 TIME_WAIT。
6. CLOSE_WAIT 是什么?为什么危险?
面试结论:
CLOSE_WAIT 表示对端已经关闭连接,本端也收到了 FIN 并回复了 ACK,但本端应用程序还没有主动 close 连接。
状态图:
1 | 对端发送 FIN | v 本端内核回复 ACK | v 连接进入 CLOSE_WAIT | v 等待应用程序调用 close() |
CLOSE_WAIT 多说明什么?
通常说明应用程序有问题:
1 | 1. socket 没有关闭 2. 连接池连接泄漏 3. IO 线程阻塞,没走到 close 4. 代码异常分支没有释放连接 5. 第三方客户端使用不当 |
TIME_WAIT 和 CLOSE_WAIT 区别:
| 状态 | 常见位置 | 原因 | 是否危险 |
|---|---|---|---|
| TIME_WAIT | 主动关闭方 | TCP 正常保护机制 | 通常不危险 |
| CLOSE_WAIT | 被动关闭方 | 应用未关闭连接 | 危险,常表示泄漏 |
标准表达:
TIME_WAIT 多不一定是问题,但 CLOSE_WAIT 多通常是应用层 bug。它说明对方已经关闭连接,本机也知道该关闭了,但应用程序没有调用 close,长期堆积会耗尽文件描述符和连接资源。
7. TCP 粘包和拆包
面试结论:
TCP 是面向字节流的协议,没有消息边界,所以应用层发送的多条消息,接收方可能一次读到多条,也可能一条消息被分多次读到,这就是粘包和拆包。
图示:
应用层发送:
1 | 消息1: Hello 消息2: World |
接收方可能读到:
1 | 情况一:HelloWorld 粘包 情况二:Hel / loWorld 拆包 情况三:Hello / World 正常 |
为什么 UDP 没有这个问题?
UDP 是面向报文的:
1 | 发送一次 UDP 报文,接收方按报文接收 |
TCP 是字节流:
1 | TCP 只保证字节顺序,不保证应用层消息边界 |
怎么解决?
应用层协议自己定义边界:
1 | 1. 固定长度 2. 分隔符,例如 \n 3. 消息头 + 消息体,头里放 length 4. TLV 格式:Type + Length + Value |
最常见的是:
1 | Length + Body |
例如:
1 | [消息长度 4 字节][消息内容 N 字节] |
标准表达:
粘包不是 TCP 的 bug,而是 TCP 字节流模型的自然结果。TCP 只负责可靠有序地传输字节,不理解应用层消息边界,所以需要应用层协议自己做拆包,比如固定长度、分隔符或者 length-based framing。
第一轮总结
这一轮你可以抓住这几句主线:
1 | TCP 和 UDP 的区别:可靠连接 vs 轻量无连接 TCP 可靠性:序列号、ACK、重传、窗口、拥塞控制 三次握手:确认双方收发能力,交换初始序列号 四次挥手:TCP 全双工,两个方向分别关闭 TIME_WAIT:主动关闭方的保护状态 CLOSE_WAIT:应用没 close,通常是 bug 粘包拆包:TCP 是字节流,没有消息边界 |
面试官如果继续追问,通常会往这几个方向打:
1 | 为什么三次不是两次? TIME_WAIT 为什么是 2MSL? CLOSE_WAIT 多怎么排查? 粘包怎么设计协议解决? TCP 为什么可靠但仍然可能传输慢? |
下一轮建议继续讲:TCP 进阶:滑动窗口、拥塞控制、半连接队列、SYN Flood、TCP 参数调优。
15:17
继续,下面是 第二轮:TCP 进阶。这一轮偏原理和线上问题,架构岗、高级后端很爱追问。
1. TCP 滑动窗口是什么?
面试结论:
滑动窗口是 TCP 用来提升传输效率和做流量控制的机制。它允许发送方在没有收到 ACK 前连续发送多段数据,而不是发一段等一段。
如果没有滑动窗口:
1 | 发送 1 个包 -> 等 ACK -> 再发下一个包 效率很低 |
有滑动窗口后:
1 | 发送方可以连续发送窗口范围内的数据 接收方通过 ACK 告诉发送方哪些数据已收到 窗口继续向前滑动 |
图示:
1 | 发送缓冲区: [已确认][已发送未确认][可发送][不可发送] ↑ ↑ 窗口左边界 窗口右边界 |
窗口滑动过程:
1 | 发送方发送:1 2 3 4 5 接收方 ACK=4 表示: 1、2、3 已经收到 下一次希望收到 4 窗口向右移动 |
它解决什么问题?
主要两个:
1 | 1. 提高吞吐量:不用每发一个包都等 ACK 2. 流量控制:根据接收方处理能力控制发送速度 |
常见追问:滑动窗口和流量控制什么关系?
TCP 头里有一个 Window Size 字段,接收方通过它告诉发送方:
1 | 我现在还能接收多少数据 |
如果接收方处理不过来,就把窗口调小;如果缓冲区满了,甚至可以通知窗口为 0。
标准表达:
滑动窗口让 TCP 可以批量发送数据,提高链路利用率;同时接收方通过通告窗口控制发送方速度,避免发送太快导致接收缓冲区被打满。
2. TCP 拥塞控制是什么?
面试结论:
流量控制是保护接收方,拥塞控制是保护网络。拥塞控制通过动态调整拥塞窗口,避免发送方把网络链路打爆。
TCP 发送量受两个窗口共同限制:
1 | 实际可发送数据量 = min(接收窗口 rwnd, 拥塞窗口 cwnd) |
其中:
1 | rwnd:接收方告诉我的能力 cwnd:发送方根据网络拥塞情况估算的能力 |
拥塞控制四个核心阶段:
1 | 慢启动 拥塞避免 快速重传 快速恢复 |
慢启动:
刚开始不知道网络承载能力,所以从小窗口开始指数增长。
1 | cwnd: 1 -> 2 -> 4 -> 8 -> 16 |
虽然叫慢启动,但增长其实是指数级,只是起点小。
拥塞避免:
当 cwnd 达到阈值 ssthresh 后,不再指数增长,而是线性增长。
1 | cwnd: 16 -> 17 -> 18 -> 19 |
快速重传:
如果发送方连续收到 3 个重复 ACK,认为某个包丢了,不等超时,立即重传。
1 | 接收方收到 1、2、4、5、6 发现 3 没到 持续 ACK=3 发送方收到 3 个重复 ACK 立即重传 3 |
快速恢复:
发生快速重传后,不直接把窗口降到 1,而是降低到一定值继续传输,避免吞吐量骤降。
标准表达:
TCP 拥塞控制的核心是根据丢包、延迟、ACK 等信号判断网络是否拥塞,然后调整拥塞窗口。慢启动负责探测带宽,拥塞避免负责平稳增长,快速重传和快速恢复负责在丢包时尽快恢复传输。
3. 流量控制和拥塞控制区别
这个特别高频,别混。
| 对比项 | 流量控制 | 拥塞控制 |
|---|---|---|
| 保护对象 | 接收方 | 网络链路 |
| 核心窗口 | 接收窗口 rwnd | 拥塞窗口 cwnd |
| 控制依据 | 接收缓冲区剩余大小 | 网络拥塞情况 |
| 目的 | 防止接收方处理不过来 | 防止网络被打爆 |
图示:
1 | 流量控制: 发送方 =======> 接收方 接收方说:慢点,我缓冲区快满了 拥塞控制: 发送方 === 网络链路 ===> 接收方 网络说:慢点,中间链路堵了 |
标准表达:
流量控制是端到端的接收能力控制,拥塞控制是整个网络链路的承载能力控制。TCP 最终发送多少数据,要同时受 rwnd 和 cwnd 限制。
4. 半连接队列和全连接队列
面试结论:
TCP 服务端维护两个队列:半连接队列保存收到 SYN 但还没完成三次握手的连接;全连接队列保存已经完成三次握手、等待应用 accept 的连接。
三次握手和队列关系:
1 | 客户端 服务端 SYN ─────────────> 放入半连接队列 SYN Queue SYN+ACK <───────────── ACK ─────────────> 从半连接队列移到全连接队列 Accept Queue 应用 accept() 从全连接队列取出连接 |
半连接队列满会怎样?
新来的 SYN 可能被丢弃,客户端连接超时或重试。
全连接队列满会怎样?
握手已经完成,但应用来不及 accept(),新连接可能被丢弃或重置,表现为连接失败、超时、服务端连接建立慢。
常见原因:
1 | 1. 服务端 accept 太慢 2. 应用线程阻塞 3. 瞬时连接洪峰 4. backlog 配置太小 5. SYN Flood 攻击 |
标准表达:
半连接队列对应三次握手未完成的连接,全连接队列对应握手完成但还没被应用 accept 的连接。线上连接失败不一定是网络问题,也可能是这两个队列被打满。
5. SYN Flood 是什么?怎么防?
面试结论:
SYN Flood 是攻击者大量发送 SYN 包但不完成三次握手,导致服务端半连接队列被占满,正常用户无法建立连接。
攻击过程:
1 | 攻击者 -> SYN -> 服务端 服务端 -> SYN+ACK -> 攻击者 攻击者不回 ACK 半连接一直占着队列 |
图示:
1 | 正常连接: SYN -> SYN+ACK -> ACK -> 建立连接 SYN Flood: SYN -> SYN+ACK -> 不回 ACK SYN -> SYN+ACK -> 不回 ACK SYN -> SYN+ACK -> 不回 ACK 半连接队列被占满 |
解决方案:
1 | 1. 开启 SYN Cookies 2. 调大半连接队列 3. 缩短 SYN+ACK 重试次数 4. 接入高防、WAF、DDoS 清洗 5. 限制单 IP 连接速率 6. 使用负载均衡分摊连接压力 |
SYN Cookies 是什么?
正常情况下服务端收到 SYN 后要保存半连接状态。
SYN Cookies 的思路是:
1 | 服务端不立即保存连接状态 而是把连接信息编码进 SYN+ACK 的序列号里 等客户端真的回 ACK 后,再还原连接 |
这样可以减少半连接队列被打爆的风险。
标准表达:
SYN Flood 利用的是 TCP 三次握手过程中服务端需要保存半连接状态这一点。防护核心是减少半连接状态占用,比如开启 SYN Cookies,同时结合队列参数调优和入口层限流清洗。
6. TCP 参数调优思路
面试一般不会要求背具体值,更看你有没有思路。
核心方向:
1 | 1. 连接队列 2. TIME_WAIT 处理 3. 端口范围 4. KeepAlive 5. 缓冲区大小 6. 重传和超时参数 |
常见参数:
1 | net.core.somaxconn 全连接队列上限 net.ipv4.tcp_max_syn_backlog 半连接队列上限 net.ipv4.tcp_syncookies 是否开启 SYN Cookies net.ipv4.ip_local_port_range 本地临时端口范围 net.ipv4.tcp_tw_reuse 是否允许复用 TIME_WAIT 连接 net.ipv4.tcp_fin_timeout FIN_WAIT 状态超时时间 net.ipv4.tcp_keepalive_time TCP KeepAlive 探测时间 |
注意点:
不要一上来就说“调内核参数”。高级一点的表达是:
先定位瓶颈,再决定是否调参。
排查顺序:
1 | 1. 看连接状态分布 2. 看半连接/全连接队列是否溢出 3. 看应用 accept 是否及时 4. 看是否短连接过多 5. 看是否有异常客户端或攻击流量 6. 最后再调整内核参数和应用 backlog |
常用命令:
1 | ss -ant netstat -s ss -lnt cat /proc/sys/net/core/somaxconn cat /proc/sys/net/ipv4/tcp_max_syn_backlog |
标准表达:
TCP 调优不能脱离现象。比如连接建立慢,要看半连接和全连接队列;TIME_WAIT 多,要看是否短连接过多;CLOSE_WAIT 多,要查应用是否关闭连接。内核参数是手段,不是第一反应。
第二轮总结
这一轮你抓住这些主线就够硬了:
1 | 滑动窗口:提高吞吐 + 流量控制 流量控制:保护接收方,看 rwnd 拥塞控制:保护网络,看 cwnd 最终发送窗口:min(rwnd, cwnd) 半连接队列:收到 SYN,未完成握手 全连接队列:完成握手,等待 accept SYN Flood:占满半连接队列 TCP 调优:先定位瓶颈,再调参数 |
面试官常见追问:
1 | 滑动窗口和拥塞窗口有什么区别? 慢启动为什么不是线性增长? 为什么收到 3 个重复 ACK 才快速重传? 半连接队列和全连接队列分别在哪一步? SYN Cookies 为什么能防 SYN Flood? 连接数上不去你怎么排查? |
下一轮继续讲:HTTP/HTTPS:HTTPS 握手、证书、HTTP/1.1/2/3、GET/POST、状态码、长连接。
15:32
继续深入回答
15:39
1. HTTP 和 HTTPS 区别
面试结论:
HTTP 是明文传输协议,HTTPS 是在 HTTP 和 TCP 之间加入 TLS/SSL 安全层,解决数据加密、身份认证和完整性校验问题。
核心区别:
| 对比项 | HTTP | HTTPS |
|---|---|---|
| 安全性 | 明文传输 | 加密传输 |
| 默认端口 | 80 | 443 |
| 证书 | 不需要 | 需要 CA 证书 |
| 身份认证 | 无 | 可验证服务端身份 |
| 数据完整性 | 弱 | 防篡改 |
| 性能 | 握手简单 | 多 TLS 握手和加解密开销 |
HTTPS 解决的三个问题:
1 | 1. 加密:防止内容被窃听 2. 认证:确认访问的是真实服务器 3. 完整性:防止数据被篡改 |
标准表达:
HTTPS 不是一种新的应用层协议,它本质上还是 HTTP,只是在 HTTP 和 TCP 之间增加 TLS 层。TLS 提供加密、证书认证和完整性校验,所以可以防窃听、防冒充、防篡改。
2. HTTPS 为什么既用非对称加密又用对称加密?
面试结论:
非对称加密用于身份认证和密钥协商,对称加密用于后续数据传输。因为非对称加密安全但慢,对称加密快但需要先安全交换密钥。
两者区别:
| 类型 | 特点 | 用途 |
|---|---|---|
| 对称加密 | 同一个密钥加密解密,速度快 | 加密业务数据 |
| 非对称加密 | 公钥加密、私钥解密,速度慢 | 身份认证、交换密钥 |
| 哈希/摘要 | 不可逆,验证完整性 | 防篡改 |
直观流程:
1 | 非对称加密: 客户端用服务器公钥安全协商出会话密钥 对称加密: 双方使用会话密钥加密后续 HTTP 数据 |
为什么不全程用非对称加密?
因为太慢,计算成本高,不适合大量业务数据传输。
为什么不直接用对称加密?
因为一开始客户端和服务端没有共享密钥,密钥怎么安全传过去是问题。
标准表达:
HTTPS 是组合拳。非对称加密解决“如何安全地协商密钥”和“如何证明服务器身份”,对称加密解决“高效传输大量数据”。这就是安全性和性能的平衡。
3. HTTPS TLS 握手过程
这里要区分 TLS 1.2 和 TLS 1.3。面试大多数讲 TLS 1.2 足够,再补一句 TLS 1.3 优化。
TLS 1.2 简化流程:
1 | 客户端 服务端 ClientHello 支持的 TLS 版本、随机数、加密套件 ───────> ServerHello 选择 TLS 版本、加密套件、随机数 Certificate 证书 <──────────── ServerKeyExchange ServerHelloDone 校验证书 生成 Pre-Master Secret 用服务端公钥加密后发送 ────────────> ClientKeyExchange ChangeCipherSpec Finished ────────────> <──────────── ChangeCipherSpec Finished 开始加密传输 HTTP 数据 |
更好记的版本:
1 | 1. 客户端发 ClientHello:我支持这些算法,这是我的随机数 2. 服务端发 ServerHello:我选这个算法,这是我的随机数,这是我的证书 3. 客户端校验证书 4. 双方协商出会话密钥 5. 后续用会话密钥对称加密通信 |
关键点:三个随机数
TLS 1.2 中会话密钥通常由这些材料生成:
1 | 客户端随机数 服务端随机数 Pre-Master Secret |
双方基于这些信息计算出相同的会话密钥。
证书校验做什么?
客户端校验证书时会看:
1 | 1. 证书是否过期 2. 域名是否匹配 3. 证书链是否可信 4. 是否被吊销 5. 签名是否合法 |
标准表达:
HTTPS 握手阶段主要做两件事:第一,客户端通过证书验证服务端身份;第二,双方协商出后续对称加密使用的会话密钥。握手完成后,HTTP 数据才开始通过对称加密传输。
4. HTTPS 能防中间人攻击吗?
面试结论:
正常证书校验完整的 HTTPS 可以防中间人攻击,关键在于客户端必须验证服务端证书是否合法。
中间人攻击大概是:
1 | 客户端 <----> 中间人 <----> 服务端 |
如果没有证书验证,中间人可以把自己的公钥发给客户端:
1 | 客户端以为这是服务端公钥 实际加密数据都被中间人解开了 |
证书的作用是证明:
1 | 这个公钥确实属于目标域名 并且由可信 CA 签发 |
标准表达:
HTTPS 防中间人攻击的关键不是“用了加密”这么简单,而是证书体系保证了公钥和域名之间的可信绑定。如果客户端跳过证书校验,HTTPS 的安全性会大幅下降。
5. HTTP 1.0、1.1、2.0、3.0 区别
面试结论:
HTTP 的演进主线是:从短连接到长连接,从文本协议到二进制分帧,从串行请求到多路复用,再到基于 QUIC 解决 TCP 队头阻塞。
HTTP/1.0:
1 | 1. 默认短连接 2. 每个请求通常新建一个 TCP 连接 3. 性能差,连接成本高 |
HTTP/1.1:
1 | 1. 默认长连接 Keep-Alive 2. 支持管道化 Pipeline,但实际使用少 3. 支持 Host 头,方便虚拟主机 4. 支持断点续传、缓存控制增强 |
问题:
1 | 同一个 TCP 连接上,请求响应基本还是有顺序约束 容易出现队头阻塞 |
HTTP/2:
1 | 1. 二进制分帧 2. 多路复用 3. Header 压缩 HPACK 4. 支持服务端推送,实际使用减少 |
HTTP/2 多路复用:
1 | 一个 TCP 连接里可以并发多个 Stream 不同请求响应被拆成 Frame 交错传输 |
图示:
1 | HTTP/1.1: 请求1 -> 响应1 -> 请求2 -> 响应2 HTTP/2: 连接中同时传: Stream1 Frame Stream2 Frame Stream3 Frame |
但 HTTP/2 仍然有 TCP 层队头阻塞:
1 | 如果 TCP 丢了一个包 后面的所有 Stream 都要等这个包重传 |
HTTP/3:
1 | 1. 基于 QUIC 2. QUIC 基于 UDP 3. 内置 TLS 1.3 4. 减少连接建立延迟 5. 解决 TCP 层队头阻塞 6. 支持连接迁移 |
HTTP/3 为什么用 UDP?
不是因为 UDP 天然可靠,而是 QUIC 在 UDP 之上自己实现了:
1 | 可靠传输 拥塞控制 流量控制 多路复用 TLS 1.3 连接迁移 |
标准表达:
HTTP/1.1 通过长连接减少建连成本,HTTP/2 通过二进制分帧和多路复用提升并发能力,但仍受 TCP 队头阻塞影响。HTTP/3 基于 QUIC,把可靠传输和 TLS 放到 UDP 之上,解决 TCP 层队头阻塞,并支持更快握手和连接迁移。
6. HTTP/2 多路复用是什么?
面试结论:
HTTP/2 多路复用是指在一个 TCP 连接上并发传输多个请求和响应,每个请求对应一个 Stream,数据被拆成 Frame 后交错发送。
结构关系:
1 | TCP Connection ├── Stream 1 │ ├── Frame │ └── Frame ├── Stream 2 │ ├── Frame │ └── Frame └── Stream 3 ├── Frame └── Frame |
解决了什么?
HTTP/1.1 下浏览器通常要开多个 TCP 连接来并发请求:
1 | 连接1 请求 CSS 连接2 请求 JS 连接3 请求图片 |
HTTP/2 可以一个连接承载多个请求:
1 | 一个 TCP 连接 多个 Stream 并发传输 |
注意:
HTTP/2 解决的是 HTTP 应用层队头阻塞,但没有彻底解决 TCP 层队头阻塞。
标准表达:
HTTP/2 的多路复用让多个请求复用一个 TCP 连接,减少连接数量和握手开销。但底层还是 TCP,只要 TCP 某个包丢失,后续字节流仍要等待重传,所以 HTTP/2 仍存在 TCP 层队头阻塞。
7. GET 和 POST 区别
面试结论:
GET 通常用于获取资源,参数放在 URL 中,语义上应该是安全且幂等的;POST 通常用于提交数据,参数放在请求体中,语义上可能改变服务端状态。
核心区别:
| 对比项 | GET | POST |
|---|---|---|
| 语义 | 获取资源 | 提交数据 |
| 参数位置 | URL Query | Body |
| 幂等性 | 应该幂等 | 不一定幂等 |
| 安全性语义 | 不应修改资源 | 可能修改资源 |
| 缓存 | 更容易被缓存 | 默认不易缓存 |
| 长度限制 | 受 URL 长度限制 | 理论上更大,受服务端配置限制 |
注意纠偏:
不要说:
1 | GET 一定没有 body POST 一定更安全 POST 一定不会被缓存 |
更准确是:
1 | GET 可以有 body,但不推荐,很多实现不兼容 POST 参数在 body,不代表安全,HTTP 明文下一样能被抓包 POST 也可以通过响应头控制缓存 |
标准表达:
GET 和 POST 最大区别不是参数放哪里,而是 HTTP 语义不同。GET 用于获取资源,应该安全幂等;POST 用于提交处理,可能产生副作用。安全传输要靠 HTTPS,不是靠 POST。
8. 常见 HTTP 状态码
面试结论:
HTTP 状态码按首位分五类:1xx 信息,2xx 成功,3xx 重定向,4xx 客户端错误,5xx 服务端错误。
高频状态码:
1 | 200 OK:请求成功 201 Created:资源创建成功 204 No Content:成功但无响应体 301 Moved Permanently:永久重定向 302 Found:临时重定向 304 Not Modified:缓存命中,资源未修改 400 Bad Request:请求参数错误 401 Unauthorized:未认证 403 Forbidden:无权限 404 Not Found:资源不存在 405 Method Not Allowed:方法不支持 409 Conflict:资源冲突 429 Too Many Requests:请求过多,被限流 500 Internal Server Error:服务端内部错误 502 Bad Gateway:网关收到上游异常响应 503 Service Unavailable:服务不可用 504 Gateway Timeout:网关等待上游超时 |
301 和 302 区别:
1 | 301:永久重定向,浏览器和搜索引擎可能缓存 302:临时重定向,适合短链、临时跳转、登录跳转 |
502 和 504 区别:
1 | 502:网关连到了上游,但上游返回了无效响应或异常断开 504:网关等上游响应超时 |
标准表达:
状态码不是只看数字,要结合调用链看。比如 502/504 往往出现在网关或 Nginx 层,502 更偏上游异常响应,504 更偏上游超时。
9. HTTP 长连接和短连接
面试结论:
短连接是每次请求都新建 TCP 连接;长连接是多个 HTTP 请求复用同一个 TCP 连接,减少握手和挥手成本。
短连接:
1 | TCP 建连 -> HTTP 请求 -> HTTP 响应 -> TCP 断开 TCP 建连 -> HTTP 请求 -> HTTP 响应 -> TCP 断开 |
长连接:
1 | TCP 建连 -> HTTP 请求1/响应1 -> HTTP 请求2/响应2 -> HTTP 请求3/响应3 TCP 断开 |
HTTP/1.1 默认长连接:
1 | Connection: keep-alive |
关闭则是:
1 | Connection: close |
长连接的好处:
1 | 1. 减少 TCP 握手开销 2. 减少 TIME_WAIT 3. 降低延迟 4. 提高吞吐 |
长连接的风险:
1 | 1. 空闲连接占资源 2. 需要合理 idle timeout 3. 连接池配置不当会打爆下游 4. 服务端需要处理连接泄漏 |
标准表达:
长连接通过复用 TCP 连接降低建连成本,非常适合频繁请求场景。但它不是越多越好,需要配合连接池、空闲超时、最大连接数和服务端容量一起设计。
第三轮总结
这一轮主线可以这样记:
1 | HTTPS = HTTP + TLS TLS 解决加密、认证、完整性 非对称加密用于协商密钥和认证 对称加密用于高效传输数据 证书保证公钥属于目标域名 HTTP/1.1 长连接 HTTP/2 二进制分帧 + 多路复用 HTTP/3 QUIC + UDP + TLS1.3 GET/POST 主要区别是语义 状态码要结合网关和上游链路理解 |
面试官常见追问:
1 | HTTPS 为什么安全? 证书怎么防中间人攻击? HTTP/2 为什么还有队头阻塞? HTTP/3 为什么用 UDP? GET 和 POST 本质区别是什么? 502 和 504 怎么排查? 长连接会带来什么问题? |
下一轮继续讲:DNS/CDN/代理/网关:DNS 解析、CDN 回源、正向代理、反向代理、网关和负载均衡区别。
15:39
继续深入回答
15:49
继续,下面是 第四轮:DNS/CDN/代理/网关深入版。这一轮更偏架构链路,面试官经常用它判断你是否理解“一个请求进系统前发生了什么”。
1. DNS 解析过程
面试结论:
DNS 的作用是把域名解析成 IP 地址。完整解析过程通常是:浏览器缓存、本机缓存、hosts、递归 DNS、根域名服务器、顶级域名服务器、权威 DNS,最后拿到目标 IP。
解析流程:
1 | 用户访问:www.example.com | v 浏览器 DNS 缓存 | v 操作系统 DNS 缓存 / hosts | v 本地 DNS 递归解析器 | v 根 DNS 服务器:去问 .com | v .com 顶级域名服务器:去问 example.com | v example.com 权威 DNS:返回 www.example.com 的 IP | v 客户端拿到 IP,开始建立 TCP/QUIC 连接 |
递归查询和迭代查询区别:
1 | 递归查询: 客户端问本地 DNS,本地 DNS 负责查到底,然后把结果返回客户端。 迭代查询: DNS 服务器不会直接给最终答案,而是告诉你下一步该问谁。 |
图示:
1 | 客户端 -> 本地 DNS:帮我查 www.example.com 本地 DNS -> 根 DNS:.com 在哪? 本地 DNS -> .com DNS:example.com 在哪? 本地 DNS -> 权威 DNS:www 的 IP 是多少? 本地 DNS -> 客户端:结果是 x.x.x.x |
标准表达:
DNS 不是一次请求就直接找到 IP,通常客户端把解析任务交给本地递归 DNS,本地 DNS 再通过根域、顶级域和权威 DNS 逐级查询。为了性能,各层都会有缓存,缓存时间由 TTL 控制。
2. DNS 缓存和 TTL
面试结论:
DNS TTL 控制解析结果可以缓存多久。TTL 越长,解析性能越好,但切流和故障恢复慢;TTL 越短,调度更灵活,但 DNS 查询压力更大。
缓存位置:
1 | 浏览器缓存 操作系统缓存 本地 DNS 缓存 运营商 DNS 缓存 递归 DNS 缓存 |
TTL 的权衡:
| TTL | 优点 | 缺点 |
|---|---|---|
| 长 TTL | 解析快,DNS 压力小 | IP 变更生效慢 |
| 短 TTL | 切流快,容灾灵活 | DNS 压力大,解析延迟增加 |
面试表达:
如果是稳定业务,可以设置相对长的 TTL;如果要做故障切换、灰度流量或多地域调度,TTL 通常会设置短一些,但要接受更高 DNS 解析压力。
3. DNS 劫持和 DNS 污染
面试结论:
DNS 劫持是解析结果被恶意或异常篡改;DNS 污染是客户端收到错误的 DNS 响应,导致域名解析到错误 IP 或无法访问。
DNS 劫持:
1 | 用户访问 www.a.com 正常应该返回:1.1.1.1 被劫持后返回:2.2.2.2 |
可能后果:
1 | 跳广告页面 跳钓鱼网站 访问错误服务 业务不可用 |
常见防护:
1 | 1. 使用 HTTPS,避免被跳转后伪装服务 2. 使用 HTTPDNS 3. 使用 DoH / DoT 4. 客户端内置域名解析兜底 5. 多 DNS 解析源校验 |
HTTPDNS 是什么?
HTTPDNS 是绕过传统本地 DNS,通过 HTTP/HTTPS 请求服务商的 DNS 解析接口。
1 | 传统 DNS: 客户端 -> 运营商 DNS -> 解析 IP HTTPDNS: 客户端 -> HTTPDNS 服务 -> 返回 IP |
优点:
1 | 减少运营商 DNS 劫持 调度更精准 移动端常用 |
标准表达:
DNS 劫持的核心风险是域名到 IP 的映射被篡改。HTTPS 可以防止内容被伪造,但不能阻止 DNS 解析到错误 IP;HTTPDNS、DoH、DoT 可以降低传统 DNS 链路被污染或劫持的风险。
4. CDN 工作原理
面试结论:
CDN 的核心是把内容缓存到离用户更近的边缘节点,通过 DNS 调度让用户访问最近或最优的节点,从而降低延迟、减轻源站压力。
访问链路:
1 | 用户访问 static.example.com | v DNS 解析到 CDN 调度系统 | v 返回较优 CDN 边缘节点 IP | v 用户访问边缘节点 | v 边缘节点有缓存:直接返回 边缘节点无缓存:回源拉取,再缓存后返回 |
图示:
1 | 用户 | | 访问图片 v CDN 边缘节点 | \ | \ 缓存命中:直接返回 | \ 缓存未命中 \ v 源站服务器 |
CDN 适合什么?
1 | 图片 视频 CSS/JS 安装包 静态 HTML 部分可缓存接口 |
不适合什么?
1 | 强动态、强用户个性化内容 频繁变化且不可缓存的数据 涉及敏感权限且缓存策略复杂的接口 |
标准表达:
CDN 本质是边缘缓存加智能调度。它不是简单代理,而是通过 DNS 或 Anycast 把用户调度到合适节点,节点命中缓存就直接返回,未命中再回源。
5. CDN 回源是什么?
面试结论:
回源是 CDN 边缘节点没有命中缓存,或者缓存过期时,向源站请求最新内容的过程。
流程:
1 | 用户请求 CDN | v CDN 查缓存 | |-- 命中:直接返回 | |-- 未命中/过期:请求源站 | v 源站返回内容 | v CDN 缓存内容并返回用户 |
回源过多的风险:
1 | 1. 源站压力变大 2. 接口延迟升高 3. CDN 命中率下降 4. 极端情况下打挂源站 |
如何优化回源?
1 | 1. 设置合理 Cache-Control 2. 静态资源文件名带 hash 3. 提高热点资源缓存时间 4. 预热 CDN 缓存 5. 对大文件做分片缓存 6. 防止缓存击穿和回源风暴 |
缓存刷新和缓存预热区别:
1 | 刷新: 让 CDN 删除旧缓存,下次请求重新回源。 预热: 提前让 CDN 节点拉取资源,用户访问时直接命中。 |
标准表达:
CDN 回源是正常机制,但回源率过高会把压力重新打回源站。生产上要关注缓存命中率、回源带宽、回源 QPS,热点资源通常要做预热和长缓存。
6. 正向代理和反向代理区别
面试结论:
正向代理代理客户端,服务端不知道真实客户端是谁;反向代理代理服务端,客户端不知道真实后端是谁。
正向代理:
1 | 客户端 -> 正向代理 -> 目标服务器 |
典型场景:
1 | 访问外部网络 客户端隐藏真实 IP 公司内网统一出口 科学上网类代理 |
站在服务端视角:
1 | 它看到的是代理服务器 IP |
反向代理:
1 | 客户端 -> 反向代理 -> 后端服务 |
典型场景:
1 | Nginx API Gateway 负载均衡 TLS 终止 统一鉴权 限流 灰度发布 |
站在客户端视角:
1 | 它只知道访问代理,不知道后面有多少服务 |
图示:
1 | 正向代理:替客户端出门 客户端们 -> 代理 -> 外部网站 反向代理:替服务端接客 用户 -> 代理 -> 后端服务集群 |
标准表达:
判断正向还是反向,关键看代理站在哪一边。正向代理隐藏客户端,反向代理隐藏服务端。Nginx 在后端系统入口通常就是反向代理。
7. 网关、负载均衡、反向代理区别
这题面试特别爱问,因为很多人会混。
面试结论:
反向代理强调请求转发,负载均衡强调流量分摊,网关强调统一入口和治理能力。三者能力有重叠,但关注点不同。
对比表:
| 类型 | 核心职责 | 常见能力 |
|---|---|---|
| 反向代理 | 代理后端服务,转发请求 | 转发、TLS、缓存、压缩 |
| 负载均衡 | 把请求分发到多个实例 | 轮询、权重、健康检查 |
| API 网关 | 微服务统一入口和治理 | 鉴权、限流、路由、灰度、审计 |
图示:
1 | 客户端 | v API 网关 | 鉴权 / 限流 / 路由 / 灰度 v 负载均衡 | 按策略选择实例 v 服务实例 A/B/C |
实际部署中可能合并:
1 | Nginx 既可以做反向代理,也可以做负载均衡 Spring Cloud Gateway 主要做 API 网关 LVS/ELB/NLB 更偏四层负载均衡 |
四层负载均衡和七层负载均衡:
1 | 四层:基于 IP + 端口,工作在 TCP/UDP 层 七层:基于 HTTP 协议内容,能识别 URL、Header、Cookie |
四层例子:
1 | LVS 云厂商 NLB F5 四层 |
七层例子:
1 | Nginx Envoy Spring Cloud Gateway Kong 云厂商 ALB |
标准表达:
反向代理是请求入口代理,负载均衡是多实例分发策略,网关是面向服务治理的统一入口。实际系统里这些能力可能由同一个组件承载,但架构职责上要区分清楚。
8. CDN 和反向代理有什么区别?
面试结论:
CDN 更强调全球/区域边缘缓存和就近访问,反向代理更强调源站入口转发和后端治理。
| 对比项 | CDN | 反向代理 |
|---|---|---|
| 部署位置 | 边缘节点,靠近用户 | 源站入口,靠近后端 |
| 核心目标 | 加速访问,减少源站流量 | 请求转发,统一入口 |
| 典型内容 | 静态资源、大文件、视频 | API、动态请求、服务入口 |
| 调度方式 | DNS/Anycast 调度 | upstream/路由规则 |
| 是否缓存 | 强缓存能力 | 可缓存但不是核心 |
链路:
1 | 用户 | v CDN 边缘节点 | v 源站反向代理 Nginx / 网关 | v 业务服务 |
标准表达:
CDN 和反向代理都可能转发请求,但 CDN 的核心是边缘缓存和就近调度,反向代理的核心是源站入口代理和后端服务治理。一个完整系统里通常两者都会存在。
9. 一次请求经过 DNS、CDN、网关的完整链路
这是场景题,可以这样串起来。
1 | 1. 用户访问 https://www.example.com/api/order 2. 浏览器查询 DNS 缓存 3. 缓存未命中,请求本地 DNS 4. DNS 返回 CDN 或负载均衡入口 IP 5. 客户端与入口建立 TCP/TLS 或 QUIC 连接 6. 如果是静态资源,CDN 命中后直接返回 7. 如果是动态接口,CDN/边缘节点回源 8. 请求进入源站反向代理或 API 网关 9. 网关做鉴权、限流、路由、灰度 10. 负载均衡选择具体服务实例 11. 服务处理请求,返回响应 12. 响应沿原链路返回客户端 |
图示:
1 | 浏览器 | | DNS 解析 v CDN / 边缘入口 | | 动态请求回源 v 源站反向代理 / API 网关 | | 路由、鉴权、限流 v 负载均衡 | v 业务服务集群 |
标准表达:
一个请求进入后端系统前,通常先经历 DNS 解析和全局流量调度;静态内容可能在 CDN 边缘直接结束,动态请求才会回源进入网关。网关负责治理和路由,负载均衡负责选择具体实例,最终请求才到业务服务。
10. 线上访问慢,DNS/CDN/网关层怎么排查?
面试结论:
访问慢要先分段定位:DNS 慢、建连慢、TLS 慢、CDN 回源慢、网关慢,还是后端服务慢。
排查维度:
1 | DNS 阶段: 域名解析是否慢,是否解析到异常 IP 连接阶段: TCP 握手是否慢,丢包率是否高 TLS 阶段: 证书、握手耗时是否异常 CDN 阶段: 是否命中缓存,回源是否过多 网关阶段: 限流、鉴权、路由、上游超时是否异常 后端阶段: 业务服务、DB、缓存是否慢 |
常用命令:
1 | nslookup www.example.com dig www.example.com curl -w "@curl-format.txt" -o /dev/null -s https://www.example.com traceroute www.example.com mtr www.example.com tcpdump |
curl 关键耗时:
1 | time_namelookup:DNS 解析耗时 time_connect:TCP 连接耗时 time_appconnect:TLS 握手耗时 time_starttransfer:首字节时间 time_total:总耗时 |
标准表达:
我不会直接猜是后端慢,而是先用 curl 分解 DNS、TCP、TLS、首字节和总耗时。如果 DNS 慢查解析链路,如果连接慢查网络和丢包,如果首字节慢再看 CDN 回源、网关和后端服务。
第四轮总结
这一轮的核心主线:
1 | DNS:域名到 IP 的解析系统 TTL:缓存效率和切流速度的权衡 DNS 劫持:解析结果被篡改 CDN:边缘缓存 + 就近调度 回源:CDN 未命中后访问源站 正向代理:代理客户端 反向代理:代理服务端 负载均衡:把流量分发到多个实例 网关:统一入口 + 服务治理 |
面试高分表达可以压成一句:
DNS 负责找到入口,CDN 负责让用户就近访问并缓存内容,反向代理和负载均衡负责把请求送到合适的后端实例,API 网关则在入口层做鉴权、限流、路由、灰度和审计。线上排查时要按 DNS、连接、TLS、CDN、网关、后端逐段拆解,而不是凭感觉定位。
下一轮继续讲:Socket、IO 模型、select/poll/epoll、Reactor、Netty 高性能、零拷贝与网络 IO。
15:49
继续回答
16:00
继续,下面是 第五轮:Socket、IO 模型、Netty 深入版。这一轮是 Java 后端面试的硬菜,讲清楚它,你在网络编程这块就有“真的上过生产”的味道。
1. Socket 是什么?
面试结论:
Socket 是应用程序使用网络能力的编程接口,本质上是操作系统提供的一组抽象,用来表示一个网络连接或通信端点。
对于 TCP,一个连接由四元组唯一标识:
1 | 源 IP + 源端口 + 目标 IP + 目标端口 |
服务端典型流程:
1 | socket() bind() listen() accept() read()/write() close() |
客户端典型流程:
1 | socket() connect() write()/read() close() |
图示:
1 | 客户端应用 服务端应用 | | socket socket connect ───── TCP 握手 ─────> listen/accept write ───── 数据传输 ─────> read read <──── 响应数据 ────── write |
标准表达:
Socket 不是协议,而是操作系统暴露给应用层的网络编程接口。应用通过 Socket 读写数据,真正的 TCP/IP 协议栈处理在内核中完成。
2. BIO、NIO、AIO 区别
面试结论:
BIO 是同步阻塞 IO,NIO 是同步非阻塞 IO,AIO 是异步 IO。Java 后端高并发网络编程主要使用 NIO/Reactor 模型。
BIO:Blocking IO
1 | 一个连接一个线程 read 没数据时线程阻塞 |
图示:
1 | 连接1 -> 线程1 连接2 -> 线程2 连接3 -> 线程3 |
问题:
1 | 连接数多时线程数量膨胀 线程上下文切换开销大 内存占用高 |
适合:
1 | 连接数少、逻辑简单、并发不高 |
NIO:Non-blocking IO
1 | 一个线程可以管理多个连接 通过 Selector 监听多个 Channel 的事件 |
图示:
1 | Selector / | \ Channel Channel Channel |
事件:
1 | OP_ACCEPT OP_CONNECT OP_READ OP_WRITE |
优点:
1 | 少量线程处理大量连接 适合高并发网络服务 |
AIO:Asynchronous IO
1 | 应用发起 IO 操作后立即返回 IO 完成后由系统通知回调 |
图示:
1 | 应用提交读请求 | v 立即返回 | v 内核完成 IO 后回调通知 |
标准表达:
BIO 的问题是连接和线程强绑定,高并发下线程资源会成为瓶颈。NIO 通过非阻塞 Channel 和 Selector,让一个线程监听多个连接事件。AIO 更进一步,应用不等待 IO 完成,而是由系统完成后通知,但 Java 服务端主流还是 Netty 这类 NIO/Reactor 框架。
3. 同步、异步、阻塞、非阻塞怎么区分?
这个非常容易绕,面试官喜欢钓鱼。
面试结论:
同步/异步关注的是调用结果由谁来通知,阻塞/非阻塞关注的是线程在等待结果时是否挂起。
阻塞/非阻塞:调用线程是否等待
1 | 阻塞: 调用 read,没有数据,线程挂起等待。 非阻塞: 调用 read,没有数据,立即返回 EAGAIN。 |
同步/异步:结果获取方式
1 | 同步: 调用方主动等待或轮询结果。 异步: 调用方发起请求后返回,完成后由系统或回调通知。 |
组合理解:
| 类型 | 含义 |
|---|---|
| 同步阻塞 | 调用后一直等,线程挂起 |
| 同步非阻塞 | 调用后立即返回,但要自己轮询 |
| IO 多路复用 | 一个线程阻塞在 selector/epoll 上,管理多个 fd |
| 异步非阻塞 | 发起后返回,完成后回调通知 |
标准表达:
阻塞和非阻塞描述线程等待时的状态,同步和异步描述结果通知机制。NIO 虽然叫非阻塞 IO,但 Selector 线程本身可以阻塞在 select/epoll_wait 上,所以它通常属于 IO 多路复用模型。
4. select、poll、epoll 区别
面试结论:
select、poll、epoll 都是 IO 多路复用机制。select 和 poll 每次都要传入并遍历 fd 集合;epoll 把 fd 注册到内核,只返回就绪事件,更适合高并发连接场景。
select:
1 | 1. 使用 bitmap 表示 fd 集合 2. fd 数量有限制 3. 每次调用都要把 fd 集合从用户态拷贝到内核态 4. 返回后还要遍历所有 fd 找就绪事件 |
poll:
1 | 1. 使用 pollfd 数组/链表 2. 没有 select 那种固定 fd 数量限制 3. 仍然每次传入全部 fd 4. 仍然要线性遍历 |
epoll:
1 | 1. epoll_create 创建 epoll 实例 2. epoll_ctl 注册 fd 和事件 3. epoll_wait 等待就绪事件 4. 内核维护 fd 集合 5. 只返回活跃 fd |
图示:
1 | select/poll: 应用每次提交所有 fd -> 内核扫描 -> 应用再扫描 epoll: 应用先注册 fd -> 内核维护集合 事件就绪 -> epoll_wait 只返回就绪 fd |
标准表达:
select 和 poll 的主要问题是重复拷贝和全量扫描,连接数越多成本越高。epoll 通过事件注册和就绪队列避免每次全量传递和遍历,因此适合大量连接但活跃连接较少的典型互联网场景。
5. epoll 的 LT 和 ET
面试结论:
LT 是水平触发,只要缓冲区还有数据就会持续通知;ET 是边缘触发,只有状态变化时通知一次,性能更高但更容易写错。
LT:Level Triggered
1 | 只要 socket 接收缓冲区还有数据 epoll_wait 就会反复返回这个事件 |
优点:
1 | 编程简单 不容易漏读 |
ET:Edge Triggered
1 | 只有从无数据变成有数据时通知一次 如果这次没读完,后面可能不再通知 |
所以 ET 必须:
1 | 1. fd 设置非阻塞 2. 循环 read,直到 EAGAIN |
伪代码:
1 | while (true) { int n = read(fd, buffer); if (n > 0) { // process } else if (n == -1 && errno == EAGAIN) { break; } else { close(fd); break; } } |
标准表达:
LT 更稳,ET 更高效但要求一次把数据读到 EAGAIN。ET 如果只读一部分就返回,后续可能收不到通知,连接就会假死,所以必须配合非阻塞 IO。
6. Reactor 模型是什么?
面试结论:
Reactor 是一种事件驱动模型,由 Reactor 线程监听 IO 事件,再把事件分发给对应 Handler 处理。Netty 的核心就是 Reactor 模型的工程化实现。
基本结构:
1 | Reactor | | 监听事件 v Acceptor / Handler | v 业务处理 |
单 Reactor 单线程:
1 | 一个线程负责: accept read decode process encode write |
图示:
1 | Reactor 线程 ├── accept 新连接 ├── read 数据 ├── 业务处理 └── write 响应 |
问题:
1 | 所有事情一个线程做,业务稍重就阻塞 IO |
单 Reactor 多线程:
1 | Reactor 线程负责 IO 事件 业务处理丢给线程池 |
图示:
1 | Reactor 线程 ├── accept ├── read/write └── 分发业务任务 -> Worker 线程池 |
问题:
1 | 单个 Reactor 仍可能成为瓶颈 |
主从 Reactor 多线程:
1 | Boss Reactor:负责 accept 新连接 Worker Reactor:负责已连接 socket 的 read/write 业务线程池:负责耗时业务 |
图示:
1 | 客户端连接 | v Boss Reactor | | accept v Worker Reactor 1/2/3 | | read/write v 业务线程池 |
标准表达:
Reactor 模型的核心是事件驱动和 IO 线程复用。主 Reactor 负责接收连接,从 Reactor 负责读写事件,业务逻辑交给业务线程池,避免 IO 线程被耗时任务阻塞。
7. Netty 为什么性能高?
面试结论:
Netty 性能高不是靠某一个点,而是 NIO 多路复用、主从 Reactor 线程模型、零拷贝思想、内存池、Pipeline 编解码机制和减少锁竞争等工程优化共同作用。
核心原因:
1 | 1. 基于 NIO / epoll,少量线程支撑大量连接 2. 主从 Reactor 模型,accept 和 read/write 分离 3. ChannelPipeline 责任链,编解码和业务处理清晰 4. ByteBuf 比 Java 原生 ByteBuffer 更易用、更高效 5. PooledByteBufAllocator 内存池减少频繁分配和 GC 6. Direct Buffer 减少堆内和堆外拷贝 7. 支持零拷贝组合 Buffer,减少内存复制 8. EventLoop 单线程串行处理 Channel 事件,减少锁竞争 |
Netty 线程模型:
1 | BossGroup | | accept v WorkerGroup | | read/write/decode/encode v 业务 Handler / 业务线程池 |
图示:
1 | 客户端 | v Boss EventLoopGroup | v Worker EventLoopGroup | v ChannelPipeline ├── Decoder ├── Encoder ├── BusinessHandler └── ExceptionHandler |
关键提醒:
Netty IO 线程不能做耗时业务。
如果在 ChannelHandler 里执行慢 SQL、远程调用、大计算:
1 | IO 线程被阻塞 同一个 EventLoop 上的其他连接都受影响 |
正确做法:
1 | IO 线程负责收发和轻量编解码 耗时业务投递到业务线程池 |
标准表达:
Netty 的高性能来自事件驱动和工程细节。它用 EventLoop 绑定 Channel,避免大量线程切换;用 Pipeline 组织处理链;用 ByteBuf 和内存池减少内存分配;同时基于 epoll/kqueue 等机制提升 IO 多路复用效率。但使用时要避免在 IO 线程里做阻塞业务。
8. Netty 的零拷贝体现在哪里?
面试结论:
Netty 的零拷贝更多是用户态层面的减少内存复制,不完全等同于操作系统 sendfile 的零拷贝。
Netty 常见零拷贝点:
1 | 1. CompositeByteBuf 多个 ByteBuf 逻辑合并,避免真实复制 2. slice() 切片共享原始内存,不复制数据 3. duplicate() 共享同一块内存,只复制读写指针等元数据 4. FileRegion / DefaultFileRegion 底层可使用 transferTo/sendfile 发送文件 |
示例:CompositeByteBuf
传统合并:
1 | header buffer + body buffer 需要复制到一个新 buffer |
CompositeByteBuf:
1 | 逻辑上看是一个 buffer 底层仍引用多个 buffer 避免复制 |
图示:
1 | CompositeByteBuf ├── ByteBuf header └── ByteBuf body |
标准表达:
Netty 的零拷贝包含两层含义:一是用户态减少 ByteBuf 之间的内存复制,比如 slice、duplicate、CompositeByteBuf;二是文件传输时通过 FileRegion 利用操作系统 transferTo/sendfile,减少内核态和用户态之间的数据拷贝。
9. 零拷贝和网络 IO 的关系
面试结论:
网络 IO 里的零拷贝主要用于文件直接发送到网络,核心是避免数据从内核 Page Cache 拷贝到用户态,再从用户态拷贝回 Socket Buffer。
传统方式:
1 | 磁盘 -> Page Cache -> 用户 buffer -> Socket Buffer -> 网卡 |
sendfile:
1 | 磁盘 -> Page Cache -> Socket Buffer -> 网卡 |
带 DMA Scatter-Gather 的 sendfile:
1 | 磁盘 -> Page Cache Page Cache -> 网卡 Socket Buffer 只保存描述符 |
适合场景:
1 | 静态文件服务器 Nginx Kafka 大文件下载 视频分发 |
不适合场景:
1 | 数据需要加密 数据需要压缩 数据需要修改 数据需要业务解析 |
标准表达:
零拷贝并不是没有任何数据移动,而是减少 CPU 参与的冗余内存拷贝。它适合文件内容无需用户态加工、直接发往网络的场景;如果数据必须经过业务处理,就很难完全零拷贝。
10. Java NIO 核心组件
面试结论:
Java NIO 的核心是 Channel、Buffer、Selector。Channel 表示连接或文件通道,Buffer 是数据容器,Selector 用来监听多个 Channel 的事件。
三件套:
1 | Channel:数据通道 Buffer:读写缓冲区 Selector:多路复用器 |
NIO 读数据:
1 | SocketChannel -> Buffer -> 应用处理 |
NIO 写数据:
1 | 应用数据 -> Buffer -> SocketChannel |
Selector 事件循环:
1 | while (true) { selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); for (SelectionKey key : keys) { if (key.isAcceptable()) { // accept } else if (key.isReadable()) { // read } else if (key.isWritable()) { // write } } keys.clear(); } |
标准表达:
Java NIO 通过 Selector 让一个线程管理多个 Channel 的事件,避免 BIO 一个连接一个线程的问题。但原生 NIO 编程复杂,容易出现空轮询、半包处理、连接管理等问题,所以生产上通常使用 Netty。
第五轮总结
这一轮核心主线:
1 | Socket:操作系统网络编程接口 BIO:一个连接一个线程,阻塞 NIO:一个线程管理多个连接,同步非阻塞 AIO:异步完成通知 select/poll:全量传递、全量扫描 epoll:注册事件,只返回就绪 fd LT:有数据就反复通知 ET:状态变化通知一次,必须读到 EAGAIN Reactor:事件驱动 + 事件分发 Netty:NIO + Reactor + ByteBuf + 内存池 + Pipeline 零拷贝:减少 CPU 冗余拷贝 |
面试高分表达:
高并发网络编程的核心不是无限加线程,而是用 IO 多路复用让少量线程管理大量连接,用 Reactor 模型分发事件,用业务线程池隔离耗时逻辑。Netty 在这个基础上补齐了编解码、内存管理、线程模型和零拷贝等工程能力,所以成为 Java 网络编程的主流框架。
下一轮就是最后一轮:网络排查与架构场景:接口超时、TIME_WAIT/CLOSE_WAIT、QPS 上不去、浏览器输入 URL、网关设计、百万 WebSocket。
16:00
继续回答
16:12
继续,下面是 第六轮:网络排查与架构场景深入版。这一轮很像真正的高级后端/架构岗面试:不只问你懂不懂协议,还问你线上怎么定位、系统怎么设计。
1. 线上接口超时怎么排查?
面试结论:
接口超时要按链路分段排查:客户端、DNS、网络连接、TLS、网关、应用服务、缓存、数据库、第三方依赖,先定位慢在哪一段,再深入。
完整链路:
1 | 客户端 | DNS 解析 | TCP 建连 / TLS 握手 | CDN / LB / 网关 | 应用服务 | 缓存 / DB / MQ / 第三方接口 |
排查步骤:
1 | 1. 确认是个别用户慢,还是全局慢 2. 看监控:P99、P95、错误率、超时率 3. 用 curl 拆分 DNS、建连、TLS、首字节耗时 4. 查网关日志:是否路由慢、限流、上游超时 5. 查应用日志:接口耗时、线程池、GC、锁等待 6. 查依赖:Redis、MySQL、MQ、第三方接口 7. 查网络:丢包、重传、连接数、带宽 |
curl 分段耗时:
1 | curl -w " dns: %{time_namelookup} connect: %{time_connect} tls: %{time_appconnect} ttfb: %{time_starttransfer} total: %{time_total} " -o /dev/null -s https://example.com/api |
含义:
1 | time_namelookup:DNS 解析耗时 time_connect:TCP 建连耗时 time_appconnect:TLS 握手耗时 time_starttransfer:首字节时间 time_total:总耗时 |
标准表达:
我不会一上来猜数据库慢,而是先把请求链路拆开,用指标和日志判断慢在哪一段。DNS 慢查解析,connect 慢查网络和服务端连接队列,ttfb 慢查网关、应用和下游依赖。
2. TIME_WAIT 过多怎么处理?
面试结论:
TIME_WAIT 是主动关闭连接的一方进入的正常状态。TIME_WAIT 多不一定是问题,重点是判断是否有大量短连接或服务端主动关闭连接。
常见原因:
1 | 1. 短连接太多 2. 服务端主动关闭连接 3. HTTP Keep-Alive 没生效 4. 连接池配置不合理 5. 客户端频繁创建连接 |
排查命令:
1 | ss -ant | awk '{print $1}' | sort | uniq -c ss -ant state time-wait | wc -l |
解决思路:
1 | 1. 开启 HTTP Keep-Alive 2. 使用连接池复用连接 3. 服务端尽量不要主动关闭连接 4. 调整客户端超时和连接池大小 5. 合理调整内核参数 |
谨慎调参:
1 | net.ipv4.tcp_tw_reuse net.ipv4.ip_local_port_range net.ipv4.tcp_fin_timeout |
标准表达:
TIME_WAIT 的根因通常是连接复用不足或短连接过多。处理时优先从长连接、连接池和关闭方入手,内核参数只能辅助,不能掩盖应用层连接管理问题。
3. CLOSE_WAIT 过多怎么处理?
面试结论:
CLOSE_WAIT 多通常是应用层没有正确关闭连接,比 TIME_WAIT 更值得警惕。
状态含义:
1 | 对端已经发 FIN 本机内核已经回 ACK 但本机应用还没调用 close() |
常见原因:
1 | 1. 代码没有关闭 socket/response/body 2. 连接池泄漏 3. 异常分支没有释放资源 4. 线程阻塞,无法执行 close 5. 第三方 SDK 使用不当 |
排查命令:
1 | ss -ant state close-wait lsof -p <pid> ls /proc/<pid>/fd | wc -l |
Java 排查:
1 | jstack <pid> jcmd <pid> Thread.print |
重点看:
1 | IO 线程是否阻塞 连接池是否耗尽 是否大量线程卡在 read/write 是否有资源未关闭 |
标准表达:
CLOSE_WAIT 多基本可以先怀疑应用没有释放连接。排查重点是代码 finally/try-with-resources、HTTP Client 响应体是否关闭、连接池是否泄漏,以及线程是否阻塞在某个慢依赖上。
4. QPS 上不去可能有哪些网络原因?
面试结论:
QPS 上不去不一定是业务代码慢,也可能是连接队列、端口耗尽、带宽、丢包重传、连接池、网关限流、内核参数等网络层因素导致。
常见网络原因:
1 | 1. TCP 连接建立慢 2. 半连接队列或全连接队列满 3. 客户端临时端口耗尽 4. TIME_WAIT 过多 5. 带宽打满 6. 丢包导致重传 7. 网关或 LB 限流 8. 连接池太小 9. DNS 解析慢 10. TLS 握手成本高 |
排查方向:
1 | ss -s ss -ant netstat -s sar -n DEV 1 sar -n TCP,ETCP 1 iostat -x 1 top pidstat -w 1 |
重点看:
1 | retransmission:TCP 重传 listen queue overflow:监听队列溢出 established:连接是否足够 time_wait:短连接是否多 bandwidth:网卡是否打满 |
优化方向:
1 | 1. 使用长连接和连接池 2. 调大 backlog 和连接队列 3. 优化 TLS:会话复用、HTTP/2、硬件加速 4. 扩容网关和服务实例 5. 降低丢包,优化网络路径 6. 增大客户端临时端口范围 7. 使用多端口或多源 IP |
标准表达:
QPS 上不去时我会同时看应用吞吐和网络指标。如果 CPU 没满但 QPS 上不去,要重点看连接队列、连接池、带宽、重传、端口耗尽和网关限制。
5. 浏览器输入 URL 到页面展示发生了什么?
面试结论:
这个过程包括 URL 解析、DNS 查询、建立连接、TLS 握手、发送 HTTP 请求、服务端处理、浏览器解析渲染资源。
完整流程:
1 | 1. 浏览器解析 URL 2. 查浏览器缓存、DNS 缓存、hosts 3. DNS 解析域名得到 IP 4. 建立 TCP 连接,HTTPS 还要 TLS 握手 5. 浏览器发送 HTTP 请求 6. 请求经过 CDN、负载均衡、网关 7. 服务端处理请求,返回 HTML 8. 浏览器解析 HTML 9. 加载 CSS、JS、图片等资源 10. 构建 DOM 树和 CSSOM 树 11. 生成 Render Tree 12. Layout 布局 13. Paint 绘制 14. Composite 合成展示 |
网络侧图示:
1 | 浏览器 | DNS | TCP/TLS | CDN/LB/Gateway | 应用服务 | DB/Cache |
浏览器渲染侧:
1 | HTML -> DOM CSS -> CSSOM DOM + CSSOM -> Render Tree Render Tree -> Layout -> Paint -> Composite |
标准表达:
这题要分两半讲:网络链路负责把资源拿回来,包括 DNS、TCP/TLS、HTTP、CDN、网关、服务端;浏览器渲染链路负责把 HTML、CSS、JS 解析成页面,包括 DOM、CSSOM、布局、绘制和合成。
6. 如何设计高并发 API 网关?
面试结论:
高并发网关要做到无状态、异步非阻塞、可水平扩展,并具备路由、鉴权、限流、熔断、灰度、观测和高可用能力。
核心能力:
1 | 1. 路由转发 2. 统一鉴权 3. 限流防刷 4. 熔断降级 5. 负载均衡 6. 灰度发布 7. 协议转换 8. 日志审计 9. 监控追踪 |
架构图:
1 | 客户端 | v DNS / CDN / LB | v API Gateway 集群 | | 鉴权 / 限流 / 路由 / 灰度 v 后端服务集群 |
高并发设计点:
1 | 1. 网关无状态,方便水平扩容 2. 使用异步非阻塞 IO,比如 Netty / Nginx / Envoy 3. 路由规则本地缓存,避免每次查 DB 4. 限流用本地限流 + 分布式限流组合 5. 鉴权结果短时缓存 6. 配置中心动态下发规则 7. 熔断隔离慢服务 8. 全链路 trace 和指标监控 |
限流策略:
1 | 单机限流:令牌桶、漏桶、滑动窗口 分布式限流:Redis、网关集群配额、Sentinel |
标准表达:
网关是系统入口,核心原则是自己不能成为瓶颈。设计上要无状态、可水平扩展,IO 模型要异步非阻塞,配置和路由要本地缓存,慢服务要熔断隔离,限流鉴权这类公共能力要前置但不能拖慢主链路。
7. 如何设计百万 WebSocket 连接服务?
面试结论:
百万 WebSocket 连接的核心不是单机扛百万,而是连接层、消息层、状态层分离,通过多节点长连接网关、连接注册中心、消息路由和水平扩展实现。
整体架构:
1 | 客户端 | v 接入层 LB | v WebSocket Gateway 集群 | | 连接注册 v 连接状态存储 Redis / etcd | v 消息路由服务 | v 业务服务 / MQ |
关键设计点:
1 | 1. 长连接网关只维护连接,不做重业务 2. 每个连接绑定到某个网关节点 3. 用户 ID -> 网关节点 映射存储到 Redis 4. 业务服务发送消息时先查用户在哪个节点 5. 消息投递到对应网关节点 6. 网关通过本地连接推送给客户端 |
连接映射:
1 | userId -> gatewayNodeId -> channelId |
消息流:
1 | 业务服务 | | 发送给 userId=1001 v 消息路由 | | 查询 userId 所在网关 v Gateway-3 | v 用户 WebSocket 连接 |
难点:
1 | 1. 连接保活:心跳、超时检测 2. 断线重连:重连退避、防止雪崩 3. 消息可靠性:ACK、重试、离线消息 4. 水平扩容:连接重新分布 5. 单机 fd 限制、内存、CPU 6. 网关节点宕机后的连接清理 7. 跨机房推送和就近接入 |
单机优化:
1 | 1. 使用 epoll/kqueue 2. 调大文件描述符限制 3. 调整 TCP keepalive 4. 控制每连接内存占用 5. 避免每连接一个线程 6. 使用 Netty 等事件驱动框架 |
标准表达:
百万 WebSocket 的核心是连接治理。接入网关负责维持长连接,业务服务不直接持有连接;用户到连接节点的映射放在共享状态中;消息通过路由服务投递到对应网关。系统要重点处理心跳、断线重连、离线消息、节点故障和水平扩容。
8. 网络抖动和丢包怎么定位?
面试结论:
网络抖动要从客户端、服务端、中间链路三侧定位,看延迟、丢包、重传、带宽和路由路径变化。
常用工具:
1 | ping mtr traceroute tcpdump ss sar -n TCP,ETCP 1 iftop |
关注指标:
1 | RTT 延迟 packet loss 丢包率 TCP retrans 重传 带宽利用率 连接数 网卡错误包 |
排查思路:
1 | 1. ping 看基本连通性和 RTT 2. mtr 看哪一跳开始丢包或延迟升高 3. tcpdump 抓包看重传、乱序、RST 4. sar 看 TCP 重传和网卡吞吐 5. 对比同机房、跨机房、不同运营商 |
标准表达:
网络问题不能只看应用日志,要结合链路工具和 TCP 指标。尤其是接口偶发慢,常见原因是丢包重传、跨运营商链路波动、网关到上游连接池异常或带宽打满。
9. 502、503、504 怎么排查?
面试结论:
502 偏上游响应异常,503 偏服务不可用或过载,504 偏网关等待上游超时。
| 状态码 | 含义 | 常见原因 |
|---|---|---|
| 502 | Bad Gateway | 上游连接被拒绝、异常断开、返回非法响应 |
| 503 | Service Unavailable | 服务过载、维护、限流、无可用实例 |
| 504 | Gateway Timeout | 网关等待上游超时 |
排查方向:
1 | 502: 上游服务是否挂了 端口是否监听 连接是否被 reset 协议是否匹配 503: 服务实例是否健康 是否被限流 连接池是否耗尽 网关是否无可用 upstream 504: 上游接口是否慢 网关超时配置是否过短 DB/Redis/第三方依赖是否慢 |
标准表达:
502、503、504 都常出现在代理或网关层。排查时要先看网关日志里的 upstream 状态,再看服务注册、健康检查、连接池和应用耗时,不能只盯业务服务日志。
10. 网络面试最终串讲模板
如果面试官让你整体讲网络请求链路,可以这样答:
一个请求首先经过 DNS 解析拿到入口 IP,然后建立 TCP 连接;如果是 HTTPS,还要做 TLS 握手,完成身份认证和密钥协商。请求进入服务端后,可能先经过 CDN、负载均衡和 API 网关,网关做鉴权、限流、路由和灰度,再转发到具体服务实例。服务内部可能访问 Redis、MySQL、MQ 或第三方接口,最后响应沿原链路返回。
如果线上出现慢请求,我会按阶段拆解:DNS、建连、TLS、网关、应用、缓存、数据库、第三方依赖。网络层重点关注连接队列、TIME_WAIT、CLOSE_WAIT、TCP 重传、带宽、连接池和网关超时。架构上则通过长连接、连接池、异步非阻塞、限流熔断、缓存和水平扩展提升吞吐和稳定性。
第六轮总结
这一轮你要记住:
1 | 接口超时:分段定位,不要猜 TIME_WAIT:主动关闭方,短连接多 CLOSE_WAIT:应用没 close,通常是 bug QPS 上不去:看连接队列、重传、带宽、连接池 URL 到页面:网络链路 + 浏览器渲染链路 高并发网关:无状态、异步非阻塞、限流熔断 百万 WebSocket:连接层和业务层解耦 502/503/504:网关视角定位上游问题 |
到这里,6 轮网络高频面试题就完整闭环了。你后面可以继续按“单题深入”来问,比如:HTTPS 握手继续深挖、epoll 底层红黑树和就绪队列、百万 WebSocket 详细设计、接口超时排查实战版。
16:12
epoll 底层红黑树和就绪队列、百万 WebSocket 详细设计详细说下
16:18
下面分两块讲:epoll 底层红黑树和就绪队列、百万 WebSocket 详细设计。这两个都很适合中高级面试,回答时要既能讲机制,也能讲工程落地。
一、epoll 底层:红黑树 + 就绪队列
先给面试结论:
epoll 的核心数据结构可以理解为一棵红黑树和一个就绪队列。红黑树保存所有被监听的 fd,方便增删改查;就绪队列保存已经发生事件的 fd。epoll_wait 不需要像 select/poll 一样全量扫描 fd,只需要从就绪队列里取事件。
1. select/poll 的问题
select/poll 的典型问题是:
1 | 每次调用都要传入所有 fd 内核每次都要遍历所有 fd 用户态返回后还要再遍历一遍找哪些 fd 就绪 |
假设监听 10 万连接,但只有 100 个连接有数据:
1 | select/poll:仍然要扫描 10 万个 fd epoll:只返回这 100 个活跃 fd |
所以 epoll 更适合:
1 | 大量连接,少量活跃 |
比如网关、IM、WebSocket、长连接服务。
2. epoll 三个核心 API
1 | int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
分别对应:
1 | epoll_create:创建 epoll 实例 epoll_ctl:添加、修改、删除监听 fd epoll_wait:等待就绪事件 |
3. 红黑树保存监听 fd
当调用:
1 | epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event); |
内核会把这个 fd 注册到 epoll 实例中。
可以抽象理解为:
1 | epoll 实例 | v 红黑树 ├── fd=3 ├── fd=7 ├── fd=11 ├── fd=20 └── fd=100 |
红黑树的作用:
1 | 1. 保存所有被监听的 fd 2. 支持快速查找 fd 是否已经注册 3. 支持快速添加、删除、修改监听事件 |
复杂度大致是:
1 | epoll_ctl add/mod/del:O(logN) |
这里的 N 是监听 fd 数量。
为什么用红黑树?
因为 epoll 需要频繁做:
1 | 添加 fd 删除 fd 修改 fd 关注事件 查找 fd 是否存在 |
红黑树是平衡二叉搜索树,能保证较稳定的查找和更新性能。
面试可以说:
红黑树不是为了 epoll_wait 快,而是为了 epoll_ctl 管理监听 fd 快。
这个点很加分。
4. 就绪队列保存活跃 fd
当某个 socket 有数据可读、可写或者发生异常时,内核会通过回调机制把对应事件加入就绪队列。
抽象结构:
1 | epoll 实例 | ├── 红黑树:保存所有监听 fd | └── 就绪队列:保存已经就绪的 fd ├── fd=7 可读 ├── fd=20 可读 └── fd=31 可写 |
当应用调用:
1 | epoll_wait(epfd, events, maxevents, timeout); |
epoll 不需要遍历整棵红黑树,而是直接检查就绪队列:
1 | 就绪队列为空:阻塞等待 就绪队列非空:拷贝就绪事件到用户态并返回 |
所以 epoll_wait 的复杂度更接近:
1 | O(就绪事件数量) |
而不是:
1 | O(监听 fd 总数) |
5. 事件是怎么进就绪队列的?
当你用 epoll_ctl 注册 fd 时,内核会把当前 epoll 实例挂到这个 socket 的等待队列上。
可以理解为:
1 | socket fd | └── 等待队列 wait queue | └── epoll 回调函数 |
当网卡收到数据,内核协议栈把数据放入 socket 接收缓冲区后,会唤醒等待队列。
然后 epoll 的回调被触发:
1 | socket 收到数据 | v 触发等待队列回调 | v 把 fd 对应事件加入 epoll 就绪队列 | v 唤醒阻塞在 epoll_wait 的线程 |
图示:
1 | 网卡收到数据 | v 内核协议栈处理 | v 数据进入 socket 接收缓冲区 | v 触发 socket wait queue | v epoll 回调把事件放入 ready list | v epoll_wait 返回 |
6. 红黑树和就绪队列整体图
1 | epoll instance | ┌───────────┴───────────┐ v v 红黑树 RB-Tree 就绪队列 Ready List 保存监听 fd 保存活跃事件 | | fd 3,7,11,20... fd 7, fd 20 | | epoll_ctl 管理 epoll_wait 返回 |
一句话:
1 | 红黑树管“我关心谁” 就绪队列管“谁已经有事了” |
这个表达挺好记,也挺像面试官爱听的人话版。
7. epoll 为什么比 select/poll 高效?
不是所有情况下都高效,准确说:
在大量连接、少量活跃的场景下,epoll 更高效。
原因:
1 | 1. fd 集合只注册一次,不用每次重复从用户态传到内核态 2. 内核维护监听集合,避免重复构建 3. 事件就绪后进入就绪队列,不需要全量扫描 4. epoll_wait 只返回活跃 fd |
但如果所有 fd 都很活跃:
1 | 监听 10 万个 fd 每次都有 10 万个 fd 就绪 |
epoll 优势会下降,因为最终还是要处理所有活跃事件。
8. LT 和 ET 在就绪队列上的差异
LT:水平触发
只要 socket 缓冲区还有数据,就会持续通知。
1 | fd 有数据 epoll_wait 返回 应用只读了一半 下次 epoll_wait 还会返回 |
ET:边缘触发
只有状态变化时通知一次。
1 | fd 从无数据 -> 有数据 epoll_wait 返回一次 应用只读了一半 如果后面没有新数据到来,可能不再通知 |
所以 ET 要求:
1 | 非阻塞 fd 循环 read 到 EAGAIN |
面试表达:
LT 是提醒你“还有数据没处理”,ET 是提醒你“状态刚发生变化”。ET 性能更好,但要求应用一次性读到 EAGAIN,否则可能漏事件。
9. epoll 常见面试追问
追问 1:epoll_wait 是不是 O(1)?
更严谨地说:
epoll_wait 不需要遍历所有监听 fd,它的成本主要和就绪事件数量有关。所以不能简单说永远 O(1),更准确是接近 O(ready events)。
追问 2:epoll 一定比 select 快吗?
不一定。
如果 fd 很少,select/poll 简单直接,差距不明显。
如果大量连接但活跃连接少,epoll 优势明显。
追问 3:epoll 的红黑树是干什么的?
红黑树保存所有被监听的 fd,用于 epoll_ctl 的增删改查;真正让 epoll_wait 高效的是就绪队列。
二、百万 WebSocket 详细设计
先给面试结论:
百万 WebSocket 系统的核心不是让业务服务直接维护百万连接,而是把连接接入层、消息路由层、业务处理层解耦。连接网关负责维持长连接,业务服务负责产生消息,消息路由负责找到用户所在连接节点并投递。
1. 需求假设
面试时先讲假设,很专业。
假设场景:
1 | 在线用户:100 万 消息类型:系统通知、私信、状态推送 连接方式:WebSocket 消息可靠性:至少一次送达 离线消息:需要保存一段时间 客户端:Web / App 部署:多节点、可水平扩展 |
核心指标:
1 | 连接数 消息 QPS 消息延迟 在线状态准确性 节点故障恢复 扩容能力 |
2. 总体架构
1 | 客户端 | v 接入层 LB | v WebSocket Gateway 集群 | | 连接注册 / 心跳维护 v 连接状态存储 Redis / etcd | v 消息路由服务 | v MQ | v 业务服务 |
更清晰的消息链路:
1 | 业务服务 | | 产生消息 v MQ | v 消息路由服务 | | 查询 userId 在哪个 Gateway v WebSocket Gateway | v 客户端 |
3. 核心模块拆分
WebSocket Gateway
职责:
1 | 1. 接收 WebSocket 连接 2. 维护 Channel 3. 心跳检测 4. 连接鉴权 5. 用户上线/下线注册 6. 向客户端推送消息 |
不做:
1 | 复杂业务逻辑 重 SQL 大计算 复杂规则判断 |
消息路由服务
职责:
1 | 1. 根据 userId 查连接位置 2. 将消息投递到对应 Gateway 3. 处理用户多端在线 4. 处理离线用户 5. 做消息重试和失败记录 |
业务服务
职责:
1 | 1. 产生业务消息 2. 写消息表或投递 MQ 3. 不直接关心连接在哪台机器 |
连接状态存储
保存:
1 | userId -> gatewayNodeId -> channelId |
如果支持多端:
1 | userId -> [ {deviceId, gatewayNodeId, channelId, loginTime} ] |
4. 为什么不要业务服务直接维护 WebSocket?
因为会导致:
1 | 1. 业务服务和连接状态强耦合 2. 服务扩容缩容困难 3. 某个业务重启会断开大量连接 4. 推送能力无法复用 5. 消息路由复杂 |
更合理的是:
1 | 连接层稳定 业务层灵活 中间用 MQ 和路由解耦 |
面试表达:
长连接是基础设施能力,不应该绑死在某个业务服务里。接入层维护连接,业务层只生产消息,通过消息路由投递到具体连接节点。
5. 用户连接注册流程
流程:
1 | 1. 客户端携带 token 发起 WebSocket 连接 2. Gateway 校验 token 3. 校验通过后建立连接 4. Gateway 生成 channelId 5. 写入连接状态存储 6. 启动心跳检测 |
图示:
1 | 客户端 | | WebSocket + token v Gateway-1 | | 校验 token v Redis: user:1001 -> Gateway-1, channel-abc |
状态结构:
1 | { "userId": "1001", "gatewayNodeId": "gw-1", "channelId": "ch-abc", "deviceId": "ios-001", "lastHeartbeat": 1710000000 } |
6. 消息推送流程
单用户推送:
1 | 1. 业务服务产生消息 2. 消息写入 MQ 3. 路由服务消费消息 4. 查询 userId 对应 Gateway 5. 调用对应 Gateway 内部推送接口 6. Gateway 根据 channelId 找到本地连接 7. 发送 WebSocket 消息 8. 客户端 ACK 9. 更新消息状态 |
图示:
1 | 业务服务 | v MQ | v 消息路由 | | userId=1001 在 gw-1 v Gateway-1 | | channelId=ch-abc v 客户端 |
群发/广播:
1 | 业务服务 -> MQ -> 广播任务 | v 拆分用户分片 | v 多个路由 worker 并行推送 |
不要直接一个请求循环推百万用户。那是事故预告片,不太想上映。
7. 连接状态如何存储?
可选方案:
Redis
优点:
1 | 性能高 实现简单 适合 userId 到节点映射 支持 TTL |
缺点:
1 | 强一致性一般 节点故障清理要处理 |
推荐:
1 | Redis Cluster + TTL + 心跳续期 |
Key 设计:
1 | ws:user:{userId} -> device/session list ws:node:{nodeId}:users -> userId set ws:session:{sessionId} -> connection metadata |
etcd / Consul
适合:
1 | 节点注册 配置发现 少量强一致元数据 |
不适合:
1 | 高频用户连接状态读写 |
所以常见组合:
1 | Redis 存用户连接映射 etcd/注册中心存 Gateway 节点列表 |
8. 心跳和连接保活
为什么需要心跳?
1 | 1. 检测客户端是否还在线 2. 清理异常断开的连接 3. 防止 NAT/代理层空闲连接被断开 4. 维持连接状态准确性 |
心跳设计:
1 | 客户端每 30s 发送 ping 服务端收到后返回 pong 服务端记录 lastHeartbeat 超过 90s 未收到心跳,关闭连接并清理状态 |
图示:
1 | 客户端 -- ping --> Gateway 客户端 <-- pong -- Gateway lastHeartbeat 更新时间 |
注意:
1 | 心跳间隔不能太短,否则百万连接会产生巨大心跳流量 |
比如 100 万连接,30 秒一次心跳:
1 | 1000000 / 30 ≈ 33333 次心跳/秒 |
这还只是心跳,别小看它,心跳也会敲 CPU 的门。
9. 消息可靠性设计
WebSocket 本身只是连接通道,不等于业务消息可靠。
如果要求可靠送达,需要应用层 ACK。
消息状态:
1 | CREATED SENT ACKED FAILED EXPIRED |
推送流程:
1 | 1. 服务端生成 messageId 2. 推送消息给客户端 3. 客户端处理后回 ACK(messageId) 4. 服务端收到 ACK 标记成功 5. 超时未 ACK 则重试 6. 多次失败进入离线消息或失败队列 |
图示:
1 | Gateway -> messageId=123 -> Client Gateway <- ACK 123 <- Client |
至少一次投递会带来重复消息,所以客户端要幂等:
1 | 根据 messageId 去重 |
面试表达:
WebSocket 保证连接上能发数据,但不保证业务语义上的可靠送达。要实现可靠推送,需要 messageId、ACK、超时重试、离线存储和客户端幂等。
10. 离线消息设计
如果用户不在线:
1 | 1. 消息写入离线消息表/Redis/MQ 2. 用户上线后拉取未读消息 3. 推送后等待 ACK 4. ACK 后标记已读或已送达 |
存储选型:
1 | 短期通知:Redis / MQ 延迟重试 重要消息:MySQL / Cassandra / HBase 大规模 IM:专门消息存储,按会话分片 |
简化表结构:
1 | message_id user_id content status created_at expire_at |
注意:
1 | 离线消息不能无限存 需要 TTL、分页拉取、过期清理 |
11. Gateway 单机容量怎么估算?
百万连接不是先喊口号,要估算资源。
每连接消耗:
1 | fd 内存:Channel 对象、Buffer、用户态连接上下文 心跳处理成本 TLS 状态 内核 socket buffer |
假设一个 Gateway 保守承载 5 万连接:
1 | 100 万连接 / 5 万 = 20 台 再加冗余,部署 30 台左右 |
如果单机 10 万连接:
1 | 100 万 / 10 万 = 10 台 加冗余,15 台左右 |
要关注:
1 | ulimit -n net.core.somaxconn net.ipv4.ip_local_port_range net.ipv4.tcp_keepalive_time net.ipv4.tcp_mem net.ipv4.tcp_rmem net.ipv4.tcp_wmem |
单机优化:
1 | 1. 使用 Netty epoll native transport 2. 调大 fd 限制 3. 控制每连接 buffer 大小 4. 避免给每连接分配大对象 5. 使用堆外内存池 6. 减少心跳频率 7. 避免 IO 线程阻塞 |
12. LB 怎么设计?
WebSocket 是长连接,LB 要注意:
1 | 1. 支持 WebSocket 升级 2. 长连接超时时间足够长 3. 健康检查准确 4. 尽量连接均衡 5. 节点下线要优雅摘流 |
负载策略:
1 | 最少连接数 一致性哈希 加权轮询 |
更推荐:
1 | 接入层按最少连接数或负载指标分配 |
因为 WebSocket 连接持续时间长,普通轮询可能不够均匀。
节点下线流程:
1 | 1. 从 LB 摘除节点 2. 不再接收新连接 3. 通知客户端重连或等待连接自然断开 4. 清理连接状态 5. 完成下线 |
13. Gateway 宕机怎么处理?
问题:
1 | Gateway 宕机后,本机连接全部断开 Redis 里可能还残留 userId -> gateway 映射 业务消息可能继续路由到故障节点 |
解决:
1 | 1. Gateway 定期上报节点心跳 2. 路由服务感知节点下线 3. 清理该节点关联的连接映射 4. 客户端自动重连到其他节点 5. 未 ACK 消息重新投递 |
节点状态:
1 | gw-1 alive gw-2 alive gw-3 dead |
清理方式:
1 | 通过 ws:node:{nodeId}:users 找到该节点用户 批量删除或标记连接失效 |
客户端重连策略:
1 | 指数退避 + 随机抖动 |
避免所有客户端同时重连把系统打爆。
14. 扩容缩容怎么做?
扩容:
1 | 1. 新 Gateway 注册到服务发现 2. LB 开始分配新连接 3. 老连接不迁移 4. 新连接逐渐分布到新节点 |
WebSocket 长连接不会自动迁移,所以扩容见效主要靠:
1 | 新连接 断线重连 主动重平衡 |
缩容:
1 | 1. 节点摘流 2. 停止接收新连接 3. 给客户端发送 reconnect 指令 4. 客户端带退避重连 5. 清理节点连接状态 6. 关闭节点 |
不要直接 kill 节点,否则会造成重连风暴。
15. 多端登录怎么处理?
如果一个用户多端在线:
1 | userId=1001 ├── iPhone -> gw-1 -> ch-a ├── Web -> gw-3 -> ch-b └── iPad -> gw-2 -> ch-c |
策略可选:
1 | 1. 全端推送 2. 只推最近活跃端 3. 按设备类型推 4. 互踢:新登录踢旧登录 |
面试中说清楚:
这个取决于业务语义。IM 通常多端同步,风控登录可能只保留一个端。
16. 安全设计
WebSocket 建连要做鉴权:
1 | 1. token 校验 2. token 过期处理 3. 连接频率限制 4. 单用户最大连接数 5. 单 IP 最大连接数 6. 消息大小限制 7. 心跳异常踢下线 8. 防止恶意空连接占资源 |
连接建立:
1 | wss://example.com/ws?token=xxx |
更推荐:
1 | Header / Cookie 携带 token |
生产上使用:
1 | WSS = WebSocket over TLS |
17. 监控指标
必须监控:
1 | 当前连接数 新建连接速率 断开连接速率 心跳 QPS 消息推送 QPS 推送成功率 ACK 延迟 离线消息堆积 Gateway CPU/内存/fd EventLoop 延迟 MQ 堆积 Redis 延迟 重连次数 节点连接分布 |
核心告警:
1 | 连接数突降 重连率突增 ACK 延迟升高 MQ 消费延迟 Redis 超时 单节点连接倾斜 fd 使用率过高 |
18. 百万 WebSocket 面试标准回答
可以直接这样讲:
百万 WebSocket 不能让业务服务直接维护连接,我会把系统拆成连接接入层、消息路由层和业务层。WebSocket Gateway 只负责长连接、鉴权、心跳和推送;业务服务产生消息后写 MQ;消息路由服务根据 userId 到 Gateway 的映射找到连接所在节点,再把消息投递到对应 Gateway。
连接状态可以放 Redis,结构类似 userId 到 gatewayNodeId、channelId、deviceId 的映射,并用 TTL 和心跳续期保证状态不过期。Gateway 宕机时,通过节点心跳发现故障,清理该节点连接映射,客户端使用指数退避重连。消息可靠性上,用 messageId、客户端 ACK、超时重试、离线消息和客户端幂等保证至少一次送达。
单机容量要根据 fd、内存、socket buffer、心跳 QPS 和 EventLoop 延迟评估,通常不会追求单机扛百万,而是多节点水平扩展。LB 要支持 WebSocket 长连接,节点下线要先摘流再让客户端重连,避免重连风暴。监控上重点看连接数、重连率、ACK 延迟、MQ 堆积、Redis 延迟和节点连接分布。
最后一句压轴:
这类系统的关键不是“能连上”,而是连接状态可治理、消息可路由、故障可恢复、扩容不雪崩。
16:18
websocket + netty + gateway 怎么落地
16:32
可以落地成一套 “边缘 API Gateway 负责入口治理,Netty WebSocket Gateway 负责长连接接入,业务服务负责消息生产” 的架构。重点先说清楚:这里有两个 gateway,别混成一坨。
1 | API Gateway:HTTP 入口治理,鉴权、限流、路由、灰度 WebSocket Gateway:长连接接入,维护 Channel、心跳、推送 |
整体架构
1 | 客户端 | | HTTPS / WSS v LB / Nginx / API Gateway | | /api/** -> HTTP 微服务 | /ws -> Netty WebSocket Gateway v Netty WebSocket Gateway 集群 | | 注册连接状态 v Redis Cluster | | 消息异步解耦 v MQ:Kafka / RocketMQ / RabbitMQ | v 业务服务 |
更完整一点:
1 | 客户端 | v SLB / Nginx | ├── HTTP 请求 -> API Gateway -> 业务服务 | └── WebSocket 连接 -> Netty WS Gateway | ├── Redis:userId -> nodeId/channelId ├── MQ:消费业务推送消息 └── RPC/HTTP:内部推送接口 |
1. 网关怎么分工?
API Gateway 做什么?
适合做 HTTP 请求治理:
1 | 登录鉴权 接口限流 路由转发 灰度发布 统一日志 请求签名校验 HTTP API 聚合 |
常见组件:
1 | Spring Cloud Gateway Nginx Kong Envoy APISIX |
Netty WebSocket Gateway 做什么?
只做长连接相关事情:
1 | WebSocket 握手 连接鉴权 Channel 维护 心跳检测 用户上线/下线 消息推送 连接状态注册 |
不建议在 Netty Gateway 里做:
1 | 复杂业务计算 慢 SQL 远程接口编排 大对象处理 复杂规则判断 |
一句话:
API Gateway 管 HTTP 流量治理,Netty WebSocket Gateway 管长连接生命周期。业务逻辑不要塞进长连接网关里。
2. WebSocket 建连流程
客户端连接:
1 | wss://example.com/ws?token=xxx |
建议生产上用:
1 | wss://example.com/ws Header/Cookie 携带 token |
建连流程:
1 | 1. 客户端发起 WSS 连接 2. LB/Nginx 转发到某个 Netty WS Gateway 3. Netty 完成 HTTP Upgrade 4. Gateway 校验 token 5. 解析 userId、deviceId、tenantId 6. 绑定 Channel 和用户上下文 7. 写入 Redis 连接映射 8. 返回连接成功消息 9. 开始心跳 |
图示:
1 | Client | | WebSocket Upgrade + token v Netty Gateway | | 校验 token v Redis: ws:user:{userId}:{deviceId} = nodeId + channelId |
连接状态可以存:
1 | { "userId": "1001", "deviceId": "ios-001", "nodeId": "ws-gw-03", "channelId": "ch-8899", "connectedAt": 1710000000, "lastHeartbeat": 1710000030 } |
3. Netty 服务端 Pipeline 怎么设计?
典型 Pipeline:
1 | pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true)); pipeline.addLast(new IdleStateHandler(90, 0, 0)); pipeline.addLast(new AuthHandshakeHandler()); pipeline.addLast(new WebSocketFrameDecoder()); pipeline.addLast(new BusinessMessageHandler()); pipeline.addLast(new ExceptionHandler()); |
更准确的职责:
1 | HttpServerCodec: HTTP 编解码,处理 WebSocket 握手前的 HTTP 请求 HttpObjectAggregator: 聚合 HTTP 请求,方便鉴权 WebSocketServerProtocolHandler: 处理 WebSocket Upgrade、Ping/Pong、Close IdleStateHandler: 检测读空闲,触发心跳超时 AuthHandshakeHandler: 握手阶段鉴权,绑定用户信息 WebSocketFrameDecoder: 解析 Text/Binary Frame BusinessMessageHandler: 处理上行消息、ACK、订阅等轻量逻辑 ExceptionHandler: 异常关闭和资源清理 |
注意:
鉴权最好在握手阶段完成,不要等连接建立后才发现 token 不合法。
4. Channel 怎么管理?
本机内存维护 Channel:
1 | channelId -> Channel userId -> channelId list channelId -> session context |
例如:
1 | ConcurrentHashMap<String, Channel> channelMap; ConcurrentHashMap<Long, Set<String>> userChannelMap; ConcurrentHashMap<String, SessionContext> sessionMap; |
Redis 维护全局连接位置:
1 | ws:user:{userId} -> sessions ws:session:{sessionId} -> metadata ws:node:{nodeId}:sessions -> sessionId set |
单端在线:
1 | ws:user:1001 = ws-gw-03:ch-8899 |
多端在线:
1 | ws:user:1001 = [ ios -> ws-gw-03:ch-a, web -> ws-gw-01:ch-b ] |
为什么本地和 Redis 都要有?
1 | 本地 Map:真正发消息要找到 Channel Redis:其他服务要知道用户在哪个 Gateway |
5. 消息下发怎么走?
有两种落地方式。
方式一:MQ 广播到所有 Gateway,Gateway 本地判断
1 | 业务服务 -> MQ Topic -> 所有 Gateway 消费 Gateway 判断目标用户是否在本机 在本机就推送,不在就忽略 |
优点:
1 | 实现简单 不需要单独路由服务 |
缺点:
1 | Gateway 多时消息放大严重 每个节点都收到无关消息 不适合大规模精准推送 |
适合:
1 | 小规模系统 广播类消息 早期版本 |
方式二:消息路由服务精准投递,推荐
1 | 业务服务 -> MQ -> Push Router -> 查 Redis -> 指定 Gateway -> Client |
流程:
1 | 1. 业务服务产生 PushMessage 2. 写入 MQ 3. Push Router 消费 4. 查 Redis:userId 在哪个 nodeId 5. 调用目标 Gateway 内部接口 6. Gateway 本地根据 channelId 推送 7. 客户端 ACK |
图示:
1 | 业务服务 | v MQ | v Push Router | | 查 Redis: userId -> ws-gw-03 v ws-gw-03 | v Client |
推荐生产使用第二种。
6. Gateway 之间怎么通信?
Push Router 找到目标 Gateway 后,需要把消息发到那台机器。
可选方式:
1 | 1. HTTP/gRPC 调用 Gateway 内部推送接口 2. 每个 Gateway 订阅自己的 MQ Topic/Queue 3. Redis Stream / PubSub |
推荐:
1 | 中等规模:HTTP/gRPC 内部接口 大规模:按 nodeId 分片 MQ Queue |
比如:
1 | push.queue.ws-gw-01 push.queue.ws-gw-02 push.queue.ws-gw-03 |
Push Router 投递到对应节点队列:
1 | userId=1001 -> ws-gw-03 -> push.queue.ws-gw-03 |
Gateway 只消费自己的队列。
7. 上行消息怎么处理?
客户端发来的消息不要在 Gateway 里重业务处理。
流程:
1 | Client -> Netty Gateway -> MQ/RPC -> 业务服务 |
例如客户端发送聊天消息:
1 | { "type": "CHAT_SEND", "requestId": "req-123", "conversationId": "c-100", "content": "hello" } |
Gateway 做轻量校验:
1 | 连接是否登录 消息大小是否合法 频率是否超限 协议格式是否正确 |
然后投递:
1 | Gateway -> MQ -> Chat Service |
业务服务处理完成后,再走下发链路推给接收方。
8. 心跳怎么落地?
建议:
1 | 客户端每 30s 发送 ping 服务端 90s 未收到任何读事件则断开 Redis session TTL 设置为 120s 每次心跳续期 |
Netty:
1 | pipeline.addLast(new IdleStateHandler(90, 0, 0)); |
处理:
1 | 读空闲事件触发 | v 服务端发送 ping 或直接关闭 | v 如果仍无响应,关闭 Channel | v 清理本地 Map 和 Redis |
心跳包:
1 | { "type": "PING", "ts": 1710000000 } |
响应:
1 | { "type": "PONG", "ts": 1710000000 } |
注意百万连接下:
1 | 100 万连接 / 30 秒 = 3.3 万心跳 QPS |
所以心跳处理必须轻,不能每次都查数据库。小小心跳,敲起来也挺吵的。
9. ACK 和可靠投递怎么落地?
下发消息格式:
1 | { "type": "PUSH", "messageId": "msg-10001", "bizType": "ORDER_STATUS", "payload": {} } |
客户端收到后回:
1 | { "type": "ACK", "messageId": "msg-10001" } |
服务端处理:
1 | 收到 ACK | v 更新消息状态为 ACKED |
如果超时未 ACK:
1 | 1. 重试 N 次 2. 仍失败则写离线消息 3. 用户下次上线拉取 |
可靠性语义建议说清楚:
1 | 至少一次送达 客户端按 messageId 幂等去重 |
不要轻易承诺“刚好一次”,面试里容易被追着打。
10. 限流和保护怎么做?
连接层:
1 | 单 IP 最大连接数 单用户最大连接数 连接建立速率限制 握手失败次数限制 |
消息层:
1 | 单连接上行 QPS 单用户发送频率 单消息大小 单 Gateway 推送队列长度 |
资源保护:
1 | 超过最大连接数拒绝新连接 发送队列积压过多断开慢客户端 消息体过大直接关闭 恶意连接加入黑名单 |
慢客户端问题:
1 | 如果客户端网络很差,服务端发送缓冲区持续堆积 会拖垮 Gateway 内存 |
处理:
1 | 1. 每个 Channel 设置待发送队列上限 2. 超过阈值丢弃低优先级消息 3. 严重时断开连接 |
11. 部署落地形态**
推荐部署:
1 | Nginx / SLB | v Netty WS Gateway Deployment | v Redis Cluster | v MQ Cluster | v Push Router / Business Services |
Gateway 节点启动时:
1 | 1. 生成 nodeId 2. 注册到服务发现 3. 定时上报心跳 4. 暴露内部推送接口 |
节点下线:
1 | 1. 从 LB 摘流 2. 停止接收新连接 3. 通知客户端重连或等待自然断开 4. 清理 Redis 中 nodeId 对应 sessions 5. 关闭进程 |
K8s 场景注意:
1 | preStop hook 做优雅下线 readinessProbe 控制摘流 terminationGracePeriodSeconds 给足 |
__END__