9.0 开篇:后端为什么绕不开网络和 IO
一起复盘一下:请求从浏览器到你的 Controller,要经过这些关卡——
DNS 找 IP → TCP 三次握手 → TLS 握手 → HTTP 解析
→ 内核把数据放进 socket 缓冲区 → 你的线程把数据读出来
→ 业务处理 → 写回 socket → 四次挥手
JVM 管的是”处理”,网络与 IO 管的是”进出”。很多线上问题(接口慢、连接数打满、CPU 不高但吞吐上不去)根子都在这一段。
学习地图:
分层(数据怎么封装) → TCP(怎么保证可靠) → HTTP(应用怎么说话)
→ IO 模型(内核怎么把数据交给你)→ 零拷贝(怎么少搬几次)
→ Netty(怎么把上面这些工程化)→ 排错(线上怎么查)
站内还有两篇更深入的专题,本文是面试速查版:
9.1 网络分层:寄一个快递
四层模型
| 层 | 协议 | 负责 | 快递类比 |
|---|---|---|---|
| 应用层 | HTTP、DNS、SSH、MQTT | 业务语义 | 你写的信的内容 |
| 传输层 | TCP、UDP、QUIC | 端到端、可靠/不可靠 | 快递单:收件人、电话、签收 |
| 网络层 | IP、ICMP | 寻址、路由 | 分拣中心:决定走哪条线路 |
| 链路层 | Ethernet、ARP、Wi-Fi | 相邻节点传输 | 运输车、飞机 |
封装:数据从应用层往下走,每层加一个头(就像每层快递套一个袋子),到对端再一层层拆开。这个动作叫封装与解封装。
[ Ethernet 头 ][ IP 头 ][ TCP 头 ][ HTTP 数据 ][ Ethernet 尾 ]
MTU 与 MSS
- MTU(Maximum Transmission Unit):链路层一帧最多 1500 字节(以太网)
- MSS(Maximum Segment Size):TCP 一个报文段最多多少数据 = MTU - IP 头 20 - TCP 头 20 = 1460 字节
超过 MTU 会被分片,任何一片丢了整个包都要重传,效率很差。所以 TCP 会按 MSS 切分,尽量不分片。
9.2 TCP 三次握手与四次挥手
三次握手:确认”双方都能说、都能听”
类比:打电话。 你:“喂,能听到吗?“(SYN) 对方:“能听到,你能听到我吗?“(SYN + ACK) 你:“能听到。“(ACK)
少一次会怎样?如果你只说了”喂”就挂断,对方不知道你能否听到他——双向通道只验证了一半。
客户端 服务端
| -------- SYN (seq=x) -------> | SYN_SENT → SYN_RCVD
| <--- SYN+ACK (seq=y,ack=x+1) -|
| -------- ACK (ack=y+1) -----> | ESTABLISHED
| ======== 开始传数据 ========== |
| 为什么是三次 | 解释 |
|---|---|
| 确认双方收发能力 | 两次只能确认客户端→服务端,服务端→客户端还没验证 |
| 同步初始序列号 | 双方都要告诉对方自己的起始序号 |
| 防止历史重复连接 | 老的 SYN 迟到,若只两次握手,服务端会白白建立一个无效连接 |
SYN 洪泛攻击:攻击者只发 SYN 不回 ACK,服务端半连接队列被占满。
| 防御 | 说明 |
|---|---|
| SYN Cookie | 不保存半连接状态,把信息编码进序列号;net.ipv4.tcp_syncookies=1 |
| 增大半连接队列 | tcp_max_syn_backlog |
| 缩短 SYN+ACK 重试 | tcp_synack_retries |
注意区分两个队列:半连接队列(收到 SYN 未完成握手)和全连接队列(已完成握手、等待应用
accept)。全连接队列满了,新连接会被拒绝或丢弃,对应参数somaxconn和listen()的 backlog。
四次挥手:为什么比握手多一次
TCP 是全双工的,两个方向要分别关闭。收到对方的 FIN 只代表”他不再发数据了”,你还可以继续发——这叫半关闭。
客户端 服务端
| -------- FIN (seq=u) --------> | FIN_WAIT_1 → CLOSE_WAIT
| <---------- ACK --------------| (我收到了,但可能还有数据要发)
| <--------- FIN (seq=v) -------| FIN_WAIT_2 → LAST_ACK
| ---------- ACK --------------> | TIME_WAIT → CLOSED
| 状态 | 出现在哪 | 含义 |
|---|---|---|
FIN_WAIT_1/2 | 主动关闭方 | 已发 FIN,等对方关 |
CLOSE_WAIT | 被动关闭方 | 收到 FIN,但应用还没 close() |
LAST_ACK | 被动关闭方 | 已发 FIN,等最后 ACK |
TIME_WAIT | 主动关闭方 | 等 2MSL 后彻底关闭 |
TIME_WAIT 为什么要等 2MSL
- 保证最后一个 ACK 能到对端:ACK 丢了,对方会重发 FIN,还能再回一次
- 让旧连接的残留报文在网络中消散,不会串到新连接里
Linux 上 MSL = 30 秒,所以 TIME_WAIT 持续 60 秒。
TIME_WAIT 太多怎么办:
net.ipv4.tcp_tw_reuse = 1 # 允许复用 TIME_WAIT 连接(仅客户端/出站连接)
net.ipv4.tcp_max_tw_buckets # 限制数量,超了直接清
net.ipv4.ip_local_port_range # 扩大可用端口范围
⚠️
tcp_tw_recycle在 Linux 4.12 已移除,网上老文章还在教,别用。它会让 NAT 后面的客户端连接异常。 更好的做法是让客户端主动关闭短连接、用连接池、或者用长连接。
CLOSE_WAIT 太多 = 代码有 bug
CLOSE_WAIT 表示对方已经关了,你的程序还没调 close()。
常见原因:连接泄漏(忘记 finally 关闭)、连接池借出未归还、下游超时后没清理、线程卡死无法执行到关闭逻辑。 看到服务器上大量 CLOSE_WAIT,不要调内核参数,去查代码。
9.3 可靠传输、拥塞控制与粘包
TCP 怎么保证不丢不重不乱
| 机制 | 作用 | 类比 |
|---|---|---|
| 序号 + 确认(ACK) | 每个字节编号,收到就确认 | 快递单号 + 签收 |
| 超时重传 | 一段时间没确认就重发 | 没签收就补发 |
| 快速重传 | 收到 3 个重复 ACK,立刻重传,不等超时 | 收件人说”前一件没到”,马上补 |
| 校验和 | 检测数据损坏 | 验货 |
| 滑动窗口 | 一次可以发多少,流量控制 | 收件人一次能收多少 |
| 拥塞控制 | 网络堵了就降速 | 高速堵车就减速 |
拥塞控制四阶段
慢启动 :窗口指数增长(1 → 2 → 4 → 8 …),直到阈值
拥塞避免 :过了阈值,窗口线性增长(每次 +1)
快重传 :收到 3 个重复 ACK,立即重传丢失段
快恢复 :不回到慢启动,窗口减半后继续线性增长
算法现状(2026):
| 算法 | 特点 | 现状 |
|---|---|---|
| CUBIC | 基于丢包,Linux 默认 | 绝大多数服务器 |
| BBR | 基于带宽和 RTT 建模,不靠丢包 | Google 主推,跨境/高丢包链路效果好 |
| BBRv3 | 改进公平性 | 新系统逐步采用 |
面试答法:默认 CUBIC,高丢包长肥管道(LFN)场景考虑 BBR。切换用
net.ipv4.tcp_congestion_control=bbr。
粘包与拆包
先说结论:TCP 是字节流,没有”消息边界”这个概念。 所谓粘包,不是 TCP 的 bug,是你把字节流当成了消息队列。
发送方发了三条消息: [A][B][C]
接收方可能收到: [AB][C] 或 [A][BC] 或 [ABC]
| 解决方案 | 做法 | 适用 |
|---|---|---|
| 固定长度 | 每条消息补齐到固定长度 | 简单、浪费空间 |
| 分隔符 | 用 \r\n 等特殊字符分隔 | 文本协议(HTTP、Redis) |
| 长度前缀 | 先读固定长度头,里面写着正文多长 | 最常用,二进制协议 |
| 自描述格式 | Protobuf 等自带长度信息 | RPC |
// Netty:长度字段解码器
pipeline.addLast(new LengthFieldBasedFrameDecoder(
1024 * 1024, // 最大帧长
0, // 长度字段偏移
4, // 长度字段本身占 4 字节
0, // 长度调整值
4 // 跳过 4 字节长度字段
));
UDP 不存在粘包:它保留消息边界,一次
send对应一次receive。代价是可能丢包、乱序、重复。
Nagle 算法与 40ms 延迟
Nagle:小包先攒着,等收到 ACK 或攒够一个 MSS 再发——减少网络上的小包。
延迟 ACK:接收方不立刻回 ACK,等一会儿(通常 40ms)看能不能捎带数据。
两个”好人”凑一起就出事:发送方等 ACK 才发,接收方等数据才回 ACK,互相等,平白多出 40ms 延迟。
socket.setTcpNoDelay(true); // 关闭 Nagle,实时性要求高的场景必开
游戏、IM、RPC 一般开
TCP_NODELAY;大文件传输可以保留 Nagle。
心跳:TCP keepalive 还是应用层心跳
| TCP keepalive | 应用层心跳 | |
|---|---|---|
| 默认时间 | 2 小时(tcp_keepalive_time) | 自定义,通常 30s~60s |
| 能否穿透代理 | 不能(代理可能不转发) | 能 |
| 能否探测业务假死 | 不能 | 能(应用还能回包才算活) |
生产环境用应用层心跳,TCP keepalive 只作为兜底。
9.4 HTTP 的演进:三次队头阻塞
HTTP/1.0 → 1.1
| 特性 | 1.0 | 1.1 |
|---|---|---|
| 连接 | 默认短连接 | 默认长连接(Connection: keep-alive) |
| 队头阻塞 | 有 | 有 + 管线化(实际很少用) |
| 缓存 | 简单 | Cache-Control、ETag、If-None-Match |
| 分块传输 | ❌ | ✅ Transfer-Encoding: chunked |
| Host 头 | 可选 | 必需(虚拟主机) |
队头阻塞:同一个连接上,前一个请求没响应完,后面的请求只能排队。
浏览器只能给同一域名开 6 个左右连接,于是大家发明了域名分片、雪碧图、资源合并——全是为了绕开这个问题。
HTTP/2
| 特性 | 说明 |
|---|---|
| 二进制分帧 | 把请求拆成 HEADERS / DATA 帧,不再是人类可读文本 |
| 多路复用 | 一个 TCP 连接上并发多个 Stream,解决应用层队头阻塞 |
| HPACK | 头部压缩,重复的 Header 只传索引 |
| Stream 优先级 | 声明资源重要性 |
| 服务端推送 | ❌ 已被浏览器移除(Chrome 106 起) |
但 HTTP/2 没解决 TCP 层的队头阻塞:所有 Stream 共用一条 TCP 连接,一旦丢包,TCP 要等重传,所有 Stream 一起卡住。这就是”第二次队头阻塞”。
HTTP/3 与 QUIC
| 特性 | 说明 |
|---|---|
| 传输层 | 跑在 UDP 上(QUIC) |
| 队头阻塞 | 流级别隔离,一个流丢包不影响其它流 |
| 握手 | 传输层与 TLS 1.3 合并,1-RTT;恢复连接可 0-RTT |
| 连接迁移 | 手机从 Wi-Fi 切 4G,连接不断(用 Connection ID 而不是四元组) |
| 加密 | 默认全加密(包括头部) |
代价:UDP 在部分企业网络被限速或封禁;服务端实现复杂度高。
HTTPS / TLS
核心思路:非对称加密换钥匙,对称加密传数据。
类比:先用保险箱(公钥)把一把只有双方知道的一次性钥匙送过去,之后就用这把钥匙快速通信。非对称安全但慢,对称快但需要先共享密钥。
| TLS 1.2 | TLS 1.3 | |
|---|---|---|
| 握手往返 | 2-RTT | 1-RTT |
| 恢复连接 | 1-RTT | 0-RTT |
| 密钥交换 | RSA / ECDHE | 只保留 ECDHE |
| 前向保密 | 可选 | 强制 |
| 加密套件 | 多 | 精简到 5 个 |
前向保密:私钥以后泄露了,以前抓的包也解不开——因为每次会话的密钥是临时生成的。
常见状态码
| 码 | 含义 | 易混点 |
|---|---|---|
| 200 / 201 / 204 | 成功 / 已创建 / 无内容 | 204 不返回 body |
| 301 / 302 / 307 / 308 | 永久 / 临时重定向 | 307/308 保持原方法,301/302 可能变 GET |
| 304 | 未修改 | 走协商缓存,不返回 body |
| 400 / 401 / 403 / 404 | 请求错 / 未认证 / 无权限 / 不存在 | 401 是”你是谁”,403 是”你不能” |
| 409 | 冲突 | 幂等键重复、乐观锁失败 |
| 429 | 限流 | 配合 Retry-After |
| 500 / 502 / 503 / 504 | 服务端错 / 网关错 / 不可用 / 网关超时 | 502 是网关连不上后端,504 是后端没按时回 |
长连接、SSE、WebSocket
| 轮询 | 长轮询 | SSE | WebSocket | |
|---|---|---|---|---|
| 方向 | 客户端拉 | 客户端拉 | 服务端推 | 双向 |
| 协议 | HTTP | HTTP | HTTP | 独立握手后升级 |
| 开销 | 大 | 中 | 小 | 小 |
| 适用 | 简单状态 | 兼容性要求高 | 通知、日志流、AI 流式输出 | IM、游戏、协同编辑 |
SSE 就是
text/event-stream,浏览器原生EventSource支持,AI 应用的流式输出基本都用它。
9.5 IO 模型:点外卖的五种姿势
先记住一个前提:一次网络读取分两个阶段——
1. 等数据就绪:数据从网卡 → 内核缓冲区
2. 拷贝数据:内核缓冲区 → 用户空间(你的程序)
类比:点外卖。
- 等餐做好(数据到内核)
- 把餐拿到手(拷贝到用户空间)
五种模型就是”等餐”和”取餐”的不同方式。
五种模型对比
| 模型 | 等数据 | 拷贝 | 类比 | Java |
|---|---|---|---|---|
| 阻塞 IO(BIO) | 一直等 | 自己搬 | 站柜台前干等,做好再端走 | InputStream.read() |
| 非阻塞 IO(NIO) | 反复问 | 自己搬 | 每隔一会儿问”好了吗” | SocketChannel.configureBlocking(false) |
| IO 多路复用 | 等叫号 | 自己搬 | 坐大厅等叫号屏 | Selector(epoll) |
| 信号驱动 IO | 等信号 | 自己搬 | 做好后打电话叫你 | 很少用 |
| 异步 IO(AIO) | 不用等 | 别人送 | 外卖送到桌上 | AsynchronousSocketChannel |
⚠️ 最容易考的一句话:前四种都是同步 IO(数据拷贝阶段要自己参与),只有 AIO 是真正的异步。
select / poll / epoll
都是”IO 多路复用”的具体实现,区别在于怎么知道谁就绪了。
| select | poll | epoll | |
|---|---|---|---|
| 最大连接数 | 1024(FD_SETSIZE) | 无上限 | 无上限 |
| 数据结构 | 位图 | 链表 | 红黑树 + 就绪链表 |
| 每次调用 | 把整个 fd 集合从用户态拷到内核态 | 同 select | 只注册一次,不用重复拷贝 |
| 就绪检查 | 遍历所有 fd,O(n) | O(n) | 回调触发,O(1) |
| 触发方式 | LT | LT | LT / ET |
| 适用 | 连接数少 | 连接数中等 | 高并发(Nginx、Redis、Netty) |
epoll 为什么快:
epoll_create → 内核里建一棵红黑树 + 一个就绪链表
epoll_ctl → 把 fd 挂到红黑树上,注册回调
epoll_wait → 有事件时,回调把 fd 放进就绪链表,直接返回链表
类比:select/poll 是每次挨个敲门问”有人吗”;epoll 是在每家门口装了个门铃,有人回来按铃,前台只需要看铃响了几个。
LT vs ET:
| 水平触发 LT | 边沿触发 ET | |
|---|---|---|
| 通知时机 | 只要缓冲区有数据就一直通知 | 只在状态变化时通知一次 |
| 编程难度 | 低 | 高(必须一次读完,读到 EAGAIN) |
| 默认 | ✅ | 需显式设置 |
| 风险 | 效率略低 | 没读完就再也不通知了 |
类比:LT 是”水没关就一直报警”;ET 是”水位第一次超过线时响一次”。ET 性能更好,但必须配合非阻塞 IO 循环读干净。
Java 三种 IO
| BIO | NIO | AIO | |
|---|---|---|---|
| 模型 | 阻塞 | 多路复用 | 异步 |
| 线程 | 一连接一线程 | 一个 Selector 管多连接 | 回调 |
| 编程难度 | 低 | 高 | 中 |
| 典型 | Socket、ServerSocket | Channel、Selector | AsynchronousSocketChannel |
| 现状 | 配合虚拟线程重新流行 | Netty 的基础 | Linux 上长期是 epoll 模拟,真正内核异步靠 io_uring |
9.6 零拷贝:少搬几次家
普通 read() + write() 有多浪费
磁盘/网卡 → 内核缓冲区 → 用户缓冲区 → socket 缓冲区 → 网卡
①DMA ②CPU ③CPU ④DMA
一次文件传输:4 次拷贝 + 4 次上下文切换,其中 2 次 CPU 拷贝完全没必要——数据只是路过用户空间。
类比:你要把仓库 A 的货搬到仓库 B,规定必须先搬到你家客厅再搬过去。零拷贝就是把这条规定取消。
三种优化
| 方案 | 拷贝次数 | 做法 | 谁在用 |
|---|---|---|---|
| mmap + write | 3 次 | 用户空间映射内核缓冲区,省掉一次 CPU 拷贝 | RocketMQ(mmap 写 CommitLog) |
| sendfile | 2 次 + 2 次切换 | 内核直接对接,数据不进用户空间 | Kafka(transferTo) |
| sendfile + DMA gather | 0 次 CPU 拷贝 | 只传描述符,DMA 按描述符拼数据 | 现代网卡 + Kafka |
// Java 里的零拷贝
FileChannel in = new FileInputStream("a.txt").getChannel();
SocketChannel out = SocketChannel.open(addr);
in.transferTo(0, in.size(), out); // 底层走 sendfile
注意:零拷贝不是”不拷贝”,而是不经过用户空间。DMA 拷贝和必要的协议头处理仍然存在。
Netty 的”零拷贝”
Netty 说的零拷贝是另一层意思:减少 JVM 堆内/堆外的数据复制。
| 手段 | 说明 |
|---|---|
DirectByteBuf | 直接用堆外内存,避免堆内 ↔ 堆外复制 |
CompositeByteBuf | 多个 Buffer 逻辑合并,不实际拷贝 |
slice() / duplicate() | 共享同一块内存,只改视图 |
FileRegion | 底层 FileChannel.transferTo |
9.7 Reactor 与 Netty
Reactor 三种模式
| 模式 | 结构 | 类比 |
|---|---|---|
| 单 Reactor 单线程 | 一个线程负责接收 + 处理 | 小饭馆:老板既点菜又炒菜 |
| 单 Reactor 多线程 | 一个线程收连接,业务扔线程池 | 老板点菜,厨师炒菜 |
| 主从 Reactor 多线程 | 主 Reactor 只接连接,从 Reactor 处理 IO | 前台迎宾 + 多个服务员 |
Netty 用的是主从 Reactor:
bossGroup接收连接,workerGroup处理读写。
Netty 核心组件
| 组件 | 作用 | 类比 |
|---|---|---|
| EventLoop | 一个线程 + 一个 Selector,绑定多个 Channel | 一个服务员管几桌 |
| Channel | 连接的抽象 | 一张桌子 |
| ChannelPipeline | 责任链,串起所有 Handler | 传菜流程 |
| ByteBuf | 增强版 ByteBuffer,读写指针分离、可池化 | 可重复使用的餐盘 |
| Bootstrap | 启动器 | 开店流程 |
Netty 为什么快
- 主从 Reactor + 多路复用:少量线程扛大量连接
- 池化 ByteBuf:减少 GC 和内存分配
- 堆外内存:减少一次复制
- 无锁串行化:一个 Channel 的所有事件都在同一个 EventLoop 线程里处理,天然无并发竞争
- 零拷贝:CompositeByteBuf、slice、FileRegion
- 可调背压:写缓冲高水位线(
WRITE_BUFFER_WATER_MARK)自动切换autoRead,防止内存被打爆
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4))
.addLast(new StringDecoder())
.addLast(new BusinessHandler());
}
});
关键设计:一个连接的全部 IO 事件由同一个 EventLoop 线程串行处理,业务 Handler 里不要做阻塞操作(数据库、HTTP 调用),否则会卡住这个线程上的所有连接。耗时逻辑扔到业务线程池。
2026 年的新选择:虚拟线程
Netty 的复杂度来自”用少量线程扛大量连接”。JDK 21 的虚拟线程改变了这个前提:
// 每个请求一个虚拟线程,代码是阻塞写法
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
server.handle((req, res) -> executor.submit(() -> {
var data = jdbc.query(sql); // 阻塞调用,但会释放载体线程
res.write(data);
}));
}
| Netty / 响应式 | 虚拟线程 | |
|---|---|---|
| 编程模型 | 回调 / 链式 / 异步 | 同步阻塞写法 |
| 调试难度 | 高(栈被切碎) | 低 |
| 生态 | 成熟 | 新,JDBC 等阻塞驱动直接可用 |
| 适用 | 超高并发、精细控制 | 大多数业务服务 |
结论:新项目如果不是网关/推送这类超高连接数场景,虚拟线程 + 阻塞式代码往往是更划算的选择。Netty 仍然不可替代(协议定制、极致性能、海量长连接)。
9.8 线上排错手册
问题一:连接超时 vs 读超时
| 超时 | 发生阶段 | 常见原因 |
|---|---|---|
| connect timeout | 三次握手 | 网络不通、防火墙、后端没监听、SYN 队列满 |
| read timeout | 握手成功后 | 后端处理慢、线程池满、数据库慢、死锁 |
| 连接池获取超时 | 从池里拿连接 | 池太小、连接泄漏、下游变慢 |
排查顺序:先确认是哪一种超时,别一上来就调大超时时间——那只是把问题藏起来。
问题二:端口耗尽
# 报错:Cannot assign requested address
ss -s # 看 timewait 数量
ss -tan state time-wait | wc -l
cat /proc/sys/net/ipv4/ip_local_port_range # 默认 32768 60999,约 2.8 万个
- 短连接 + 高 QPS → 本地端口被 TIME_WAIT 占满
- 解决:用连接池/长连接、
tcp_tw_reuse=1、扩大端口范围
问题三:文件句柄耗尽
ulimit -n # 进程可打开的文件数
lsof -p <pid> | wc -l # 当前打开数
cat /proc/<pid>/limits
报错通常是
Too many open files。每个 socket 都是一个 fd,连接数 + 文件数 + 管道都要算进去。
问题四:accept 队列溢出
netstat -s | grep -i listen # "listen queue of a socket overflowed"
ss -lnt # Send-Q 列是 backlog
cat /proc/sys/net/core/somaxconn
- 应用
accept太慢(比如在 accept 线程里做业务) backlog太小- 解决:加大
somaxconn、把业务处理移出 accept 线程
抓包三板斧
tcpdump -i eth0 -nn port 8080 -w /tmp/cap.pcap # 抓包
tcpdump -i eth0 -nn -A port 8080 # 直接看内容
# 用 Wireshark 分析:看三次握手、重传、RST、窗口大小
| 现象 | 可能原因 |
|---|---|
| 大量 重传 | 丢包、网卡/交换机问题、带宽打满 |
| RST | 端口没监听、连接被中间设备重置、服务端 backlog 满 |
| 窗口为 0 | 接收方处理不过来(应用读太慢) |
| 握手很慢 | 网络 RTT 高、SYN 队列满、DNS 慢 |
常用内核参数
# /etc/sysctl.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10240 65000
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_fin_timeout = 30
9.9 面试速查
| 问题 | 一句话答案 |
|---|---|
| TCP 为什么三次握手 | 确认双方收发能力 + 同步序列号 + 防止历史重复连接 |
| 为什么四次挥手 | 全双工,两个方向要分别关闭,中间有半关闭状态 |
| TIME_WAIT 为什么 2MSL | 保证最后 ACK 到达 + 让旧报文消散;Linux 上约 60 秒 |
| 大量 CLOSE_WAIT | 应用没调用 close,是代码 bug,不是内核参数问题 |
| 大量 TIME_WAIT | 短连接多、主动关闭方;用连接池或 tcp_tw_reuse |
| TCP 怎么保证可靠 | 序号确认、超时重传、快速重传、校验和、滑动窗口、拥塞控制 |
| 粘包怎么解决 | TCP 没有消息边界;用固定长度、分隔符或长度前缀 |
| Nagle 和延迟 ACK | 两者互相等待会产生约 40ms 延迟,实时场景开 TCP_NODELAY |
| HTTP/1.1 的队头阻塞 | 同一连接上前一个请求未完成,后面的排队 |
| HTTP/2 解决了吗 | 解决应用层,但 TCP 丢包会让所有 Stream 一起阻塞 |
| HTTP/3 怎么解决 | QUIC 跑在 UDP 上,流级隔离,1-RTT/0-RTT,支持连接迁移 |
| TLS 1.3 改进 | 1-RTT 握手、只保留 ECDHE、强制前向保密、0-RTT 恢复 |
| 502 和 504 区别 | 502 网关连不上后端,504 后端没按时响应 |
| 五种 IO 模型 | 阻塞、非阻塞、多路复用、信号驱动、异步 |
| 哪些是同步 IO | 前四种都是,只有 AIO 是异步 |
| select / poll / epoll | 1024 上限 / 无上限但遍历 / 红黑树 + 就绪链表 + 回调 |
| LT 和 ET | 水平触发一直通知,边沿触发只通知一次,ET 必须读干净 |
| 零拷贝是什么 | 数据不经过用户空间;sendfile 2 次拷贝,DMA gather 0 次 CPU 拷贝 |
| Netty 为什么快 | 主从 Reactor、池化堆外内存、无锁串行化、零拷贝、背压 |
| Netty 里能写阻塞代码吗 | 不能,会卡住 EventLoop 上的所有连接 |
| 虚拟线程和 Netty 怎么选 | 普通业务用虚拟线程更简单,超高并发/协议定制用 Netty |
| 端口耗尽怎么查 | ss -s 看 timewait、ip_local_port_range,改用长连接 |
到这里,Java 面试笔记的骨架就补齐了:基础 → 集合 → 多线程 → JVM → Redis → MySQL → Spring → 微服务 → MQ → 网络与 IO。