💡 为什么需要网络模型
Java 程序需要通过网络对外提供服务。当多个客户端同时连接时,服务端如何处理这些连接并高效读写数据,就是网络 IO 模型要解决的问题。
直观理解
如果把服务端比作饭店:
- BIO(老式小饭店):一个服务员服务一桌客人,客人不走服务员不离开。客人多了就不断招服务员(线程),成本极高
- NIO(网红大食堂):一个服务员(Selector)站在门口看,哪桌客人招手了(有数据到达),就叫对应的厨师(处理线程)过去
- Netty(智能食堂管理系统):现成的成熟架构,包含了排队、叫号、传菜、结账的整套解决方案,你只需要关注炒菜就行
📐 BIO 阻塞 IO
原理
Java IO 模型
Java 网络 IO 模型演变:从 BIO 到 NIO 再到多路复用,以及 Kafka、Netty 等中间件中的实践印证。
一、为什么需要网络模型
用生活化例子+分层拆解讲明白,先抛开专业名词,搞懂为什么需要网络模型,再对应 BIO/NIO/Netty。
一、先搞懂:Java 程序为什么要「网络」?
你写的 Java 程序分两类:
- 本地程序:只读写本机文件、计算,不和别的机器通信,不需要网络。
- 网络程序:两台/多台机器互相发数据(客户端 ↔ 服务端)
例:浏览器访问网站、APP调接口、生产者发消息给 Kafka、Redis 客户端连服务端。
只要是跨机器通信,就必须走 Socket 网络通信,而「网络模型」就是:
Java 代码如何高效管理海量客户端连接、收发数据的一套设计方案。
二、最通俗比喻:把服务端比作「饭店」
把 服务端程序 = 饭店,客户端连接 = 前来吃饭的客人,帮你彻底理解。
场景1:BIO 阻塞IO(老式小饭店)
规则:来一个客人,专门配一个服务员全程陪着
- 客人进门(建立TCP连接)→ 分配专属服务员(新建线程)
- 客人点菜、等菜、慢慢吃(读写数据)→ 这个服务员全程站着等,啥也干不了(线程阻塞)
- 客人离开(连接断开)→ 服务员下岗(线程销毁)
问题在哪?
- 店里客人少(几十个):没问题,体验还行。
- 一下子来几百、几千客人:服务员不够用,雇人就要加成本(线程暴涨、内存CPU拉满)。
- 服务员大部分时间都在傻等,闲着不干活,资源严重浪费。
✅ 对应技术特点:
BIO = 一连接一线程,连接一多就崩,只适合低并发。
场景2:NIO 非阻塞多路复用(网红大食堂)
老板换了玩法:不用一对一服务员,只用几个「前台引导员」统一接待
- 所有客人(连接)都在大厅等候区,统一由少数几个引导员(线程)看管。
- 引导员不会盯着某一个客人傻等,而是不停扫视全场:
- 哪个客人举手要加菜(有读/写事件)→ 上前服务;
- 客人安静坐着没动作 → 完全不管,继续看下一位。
- 一个引导员,可以同时照看几百上千个客人。
核心优势
- 极少的人手(线程),服务海量客人(连接)
- 没人等待、没人空闲,资源利用率拉满
✅ 对应技术特点:
NIO = 少量线程管理大量连接,非阻塞、轮询就绪事件,天生扛高并发。
Kafka、Redis 这种要对接上万客户端的中间件,必须用这套模式。
场景3:Netty(现成的「智能食堂管理系统」)
原生 NIO 就像:老板自己手写一套接待规则、流程、话术。
- 规则复杂、细节多、容易出错(对应原生 NIO API 繁琐、坑多)。
Netty 就是:市面上卖好的一整套标准化食堂管理系统
- 已经帮你把接待流程、排队、上菜、异常处理全部封装好;
- 老板直接拿来用,不用自己从零造轮子。
✅ 对应技术特点:
Netty 底层还是 NIO 思想,只是封装简化、屏蔽底层坑,普通开发做网络程序首选。
三、回到你的问题:Java 为什么要搞「网络模型」?
总结成 3 句大白话:
- 只要做网络服务(对外提供接口、接收连接),就必须管理客户端连接和数据读写。
- 不同的管理方式,就是不同的网络模型(BIO / NIO)。
- 设计网络模型的唯一目标:
用最少的系统资源(线程、内存、CPU),支撑尽可能多的客户端并发连接。
四、结合你之前学的 Kafka,再串一遍
- Kafka 是消息服务,每时每刻都有成千上万生产者、消费者连接上来。
- 如果用 BIO:每来一个客户端就开一个线程,线程数爆炸,服务器直接卡死。
- 所以必须用 NIO 非阻塞多路复用:少量线程管理上万连接。
- Kafka 团队追求极致性能,自己手写原生 NIO(不套 Netty 框架),做定制优化。
五、补充两个关键概念(为什么会有「阻塞」?)
1. 什么是阻塞?
线程执行读写网络数据时,如果对方还没发数据,线程就卡在原地不动,干等。
- BIO:全程阻塞,线程被占用。
- NIO:设置为非阻塞,没数据就立刻返回,线程去处理别的连接。
2. 什么是多路复用(Selector)?
就是上面食堂例子里的「统一扫视全场」:
一个线程监听一堆连接,只处理有数据往来的连接,忽略空闲连接。
这是 NIO 能支撑高并发的核心。
六、极简总结(记这一段就够)
- 网络模型 = Java 服务端管理客户端连接、收发数据的设计方案。
- BIO:一人服务一个客户,简单但扛不住高并发。
- NIO:几个人看管海量客户,高并发必备,原生代码复杂。
- Netty:封装好的 NIO 框架,简化开发,不用重复造轮子。
- Kafka 这类中间件,为了性能,选择手写原生 NIO 网络模型。
二、BIO 与 NIO 详解
结合 Java 网络编程基础,通俗讲清 BIO、NIO、Netty,再结合 Kafka 场景对比,附区别、适用场景、优缺点。
一、基础概念:IO 模型(针对网络通信)
IO 全称 Input/Output,这里特指网络IO(客户端 ↔ 服务端数据读写),Java 里分三大经典模型:BIO、NIO、AIO,日常重点看前两者。
1. BIO 阻塞IO(Blocking IO)
1. 原理
阻塞是核心:线程在执行读写/等待连接时,会卡住不动,直到操作完成。
经典模型:一连接一线程
- 服务端主线程
ServerSocket监听端口,等待客户端连接; - 一旦有客户端连上,新开一个独立线程专门处理该连接;
- 这个子线程调用
read()读数据时,如果客户端没发数据,线程一直阻塞等待,啥也干不了; - 连接断开,对应线程销毁。
2. 流程图
主线程:监听端口 → 收到连接 → 新建子线程
子线程:阻塞读数据 → 处理业务 → 阻塞写数据
3. 优缺点
- 优点:代码简单、上手容易
- 缺点:
- 连接数越多,线程越多,线程爆炸,内存、CPU 开销巨大;
- 线程频繁创建/销毁,开销高;
- 线程阻塞,资源利用率极低。
4. 适用场景
低并发、短连接场景,传统老旧系统、单机连接数几十/几百以内。
Kafka、Nginx、微服务网关等高并发组件绝对不会用 BIO。
2. NIO 非阻塞IO(Non-blocking IO)
Java 1.4 推出,全称 New IO / Non-blocking IO,Kafka 原生使用的模型。
1. 核心思想
单线程管理大量连接 + 非阻塞 + 多路复用(Selector 选择器)
不再是「一连接一线程」,而是一个线程通过 Selector 轮询监听成百上千个连接。
关键组件(Java NIO 三大核心)
- Channel(通道):替代传统 Socket,数据读写载体,双向通信;
- Buffer(缓冲区):所有数据必须先读到缓冲区,再处理;
- Selector(选择器/多路复用器):NIO 灵魂。
- 一个 Selector 注册多个 Channel;
- 线程只轮询 Selector,只处理有事件(读/写/连接)就绪的 Channel;
- 无事件的连接,线程完全不搭理,不会阻塞。
2. 工作流程(结合 Kafka Processor 理解)
- 把所有客户端连接的 Channel 注册到同一个 Selector;
- 线程调用
select()轮询,该方法阻塞,直到有 Channel 产生事件(有数据可读、可写、新连接); - 遍历就绪的 Channel,执行读/写操作;
- 读写操作本身非阻塞:没有数据就立刻返回,不会卡住线程。
3. 优缺点
- 优点:
- 少量线程支撑海量连接,线程数和连接数解耦;
- 非阻塞 + 多路复用,CPU/内存利用率极高,天然适配高并发;
- 缺点:
- Java 原生 NIO API 非常繁琐、底层、坑多(空轮询、粘包拆包、协议解析都要自己写);
- 编程复杂度极高,手写生产级 NIO 成本大。
4. 适用场景
高并发、长连接,中间件标配:Kafka、RocketMQ、Redis、ZooKeeper 等。
补充:Kafka 没有用第三方框架,基于 Java 原生 NIO 自研网络层,也就是上一节讲的 SocketServer + Acceptor/Processor/Handler 架构。
3. Netty
1. 定义
Netty 是基于 Java 原生 NIO 封装的高性能网络框架。
简单理解:
Java 原生 NIO 是「毛坯房」,API 难用、坑多;
Netty 是「精装修成品房」,在 NIO 基础上做了封装、优化、组件封装,开箱即用。
2. 核心定位
底层依旧是 NIO 多路复用模型,和 Kafka 网络模型底层原理一致,只是:
- Kafka:手写原生 NIO
- Netty:封装好的 NIO 框架
3. Netty 解决了原生 NIO 的痛点
- 修复 Java NIO 经典 Bug(Selector 空轮询);
- 内置粘包/拆包处理器、编解码器,不用自己处理 TCP 流问题;
- 封装线程模型(Reactor 模型)、连接管理、心跳检测、重连;
- 统一 API,代码简洁,生产稳定性经过大规模验证。
4. 经典线程模型(Reactor)
Netty 默认 主从 Reactor 多线程模型,和 Kafka 三层架构思想高度相似:
- Boss 线程组:对应 Kafka Acceptor,专门接收新连接;
- Worker 线程组:对应 Kafka Processor,处理网络读写、协议解析;
- 业务线程池:对应 Kafka Handler,执行业务逻辑。
5. 适用场景
现在主流互联网中间件、网关、RPC 框架几乎都在用 Netty:
- RPC:Dubbo、gRPC(Java 版)
- 网关:Spring Cloud Gateway
- 消息队列:RocketMQ
- 大数据:Elasticsearch、Flink
- 游戏服务器、物联网长连接服务
二、三大模型横向对比(重点记忆)
| 模型 | 阻塞特性 | 线程模型 | 核心特点 | 编程难度 | 典型应用 |
| BIO | 完全阻塞 | 一连接一线程 | 简单、并发弱、连接上限低 | 低 | 老旧系统、低并发服务 |
| 原生NIO | 非阻塞 + 多路复用 | 单/多线程管理海量连接 | 高并发、高性能、底层能力强 | 极高 | Kafka(自研NIO) |
| Netty | 基于NIO封装,非阻塞 | 标准Reactor线程池 | NIO能力 + 易用、稳定、功能全 | 低 | Dubbo、Gateway、RocketMQ |
三、结合 Kafka 再复盘(串联之前知识点)
- 为什么 Kafka 不用 BIO?
Kafka 面向百万级消息吞吐、海量客户端连接,BIO 一连接一线程会直接线程溢出,完全扛不住。 - 为什么 Kafka 不用 Netty,选择手写原生 NIO?
- 历史原因:Kafka 诞生较早,早期 Netty 生态不成熟;
- 极致性能:自研 NIO 可以深度定制,去掉 Netty 多余封装、冗余逻辑,压榨硬件极限;
- 架构适配:为自身
Acceptor + Processor + Handler三层架构、自定义二进制协议做专属优化。
- 三者关系总结
- BIO:过时模型,高并发场景直接淘汰;
- NIO:是底层技术规范(非阻塞+多路复用),是高并发网络编程的基础;
- Netty:NIO 的工业级封装框架,普通开发首选,不用重复造轮子;
- Kafka:吃透原生 NIO,自己造轮子,追求极致性能。
四、补充两个高频面试点
- TCP 粘包/拆包
TCP 是流式协议,没有边界,会出现“多条消息粘在一起”或“一条消息被拆分”。
- 原生 NIO:需要开发者手动写逻辑处理,非常麻烦;
- Netty:内置多种拆包器(定长、分隔符、长度域),开箱即用;
- Kafka:自定义二进制协议,自带消息长度字段,自己处理边界。
- Reactor 模型不管是 Kafka 自研 NIO,还是 Netty,底层都遵循 Reactor 反应器模式:
由专门线程监听事件,事件触发后分发给对应处理器执行。
这也是高并发网络组件的通用设计思想。
极简背诵版
- BIO:阻塞,一连接一线程,并发差,老旧技术;
- NIO:Java 非阻塞多路复用IO,高并发底层基础,原生API复杂;
- Netty:封装好的 NIO 框架,简化开发,主流中间件最爱;
- Kafka:基于原生Java NIO自研网络层,不使用Netty,追求极致性能。
三、两种模型运行形态差异
一、核心结论
- BIO 中的 ServerSocket只要不执行
close()、进程不退出、端口不被释放,ServerSocket会持续监听端口,循环调用accept()一直接收新客户端连接,不断生成新Socket。 - NIO 中的 ServerSocketChannel逻辑一模一样:
只要不调用close()、通道不关闭,它就一直绑定在指定端口上,持续监听入站连接请求;每收到一个 TCP 连接,accept()就生成一个新SocketChannel。
简言之:监听对象不关闭,监听就永不停止,会一直处理新连接。
二、两种模型运行形态差异
1. BIO 形态(阻塞式)
// 伪代码
ServerSocket ss = new ServerSocket(9092);
while(true) { // 死循环
Socket socket = ss.accept(); // 阻塞等待连接
// 新开线程处理当前 socket 读写
new Thread(() -> handleData(socket)).start();
}
// 只要代码没走到 ss.close(),循环永远执行,持续接连接
accept()是阻塞方法:没连接就卡住,有连接就立即返回并继续下一轮循环。- 直到调用
ss.close()或进程终止,监听才停止。
2. NIO 形态(非阻塞 + Selector)
// 伪代码
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(9092);
ssc.configureBlocking(false); // 设为非阻塞
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true) { // 事件轮询死循环
selector.select();
// 遍历就绪事件
for(SelectionKey key : selector.selectedKeys()){
if(key.isAcceptable()){
SocketChannel sc = ssc.accept(); // 拿到新连接通道
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
}
}
// 不执行 ssc.close(),端口持续监听,不断接收新连接
- 依靠
Selector循环轮询连接事件,ServerSocketChannel常驻监听。 - 只有主动
close()、销毁通道,才会停止接收请求。
三、结合 Kafka 场景印证
Kafka 的 Acceptor 线程就是一个无限循环:
- 内部
ServerSocketChannel绑定 9092 端口,全程不关闭; - 循环检测新连接事件,不断
accept()生成SocketChannel; - 只要 Kafka 服务进程运行、监听通道不关闭,就永久接收生产者、消费者的连接请求;
- 只有停止 Kafka 服务、手动关闭通道,监听才终止。
四、补充几个边界场景
- 端口占用问题
ServerSocket/ServerSocketChannel一旦绑定端口并开始监听,其他进程无法再监听同一个端口;只有它close()或进程退出,端口才会释放。 - 异常中断如果进程崩溃、网络异常、系统强制杀进程,监听会被动终止,等价于隐式
close()。 - 已建立的连接不受影响
监听通道关闭后:
- 不再接收新连接;
- 已经建立好的 Socket/SocketChannel 仍可正常收发数据,直到主动断开。
五、最终一句话总结
在 ServerSocket / ServerSocketChannel 调用 close() 之前,或是服务进程未停止时,会持续监听端口、一直接收客户端连接请求,不断生成对应的通信通道,这是网络服务端的标准运行逻辑。
关联文档
- 20-areas/cloud-native/kafka/Kafka 网络模型 — Kafka 中的 NIO 实践
- 20-areas/cloud-native/network/protocol/Socket 工作原理 — Socket 底层基础
- JVM 内存结构 — JVM 运行时数据区