Netty RPC 底层硬核拆解:从 NIO 到 Socket 到粘包半包,一个下午全打通
从 BIO 阻塞模型开始,一路推到 Netty 的 Pipeline、Dubbo 的 BusinessHandler、RPC 的完整链路。TCP 粘包半包、NIO 为什么不是异步、AIO 为什么没人用,全用我自己的思路串了一遍。
Netty RPC 底层硬核拆解:从 NIO 到 Socket 到粘包半包,一个下午全打通
前言
今天下午聊了近一个小时,从 NIO 聊到 Netty,从粘包半包聊到 RPC,从 IM 聊到 BusinessHandler。
这篇文章不按教科书结构来,就按照我一步步问、一步步懂的思路写,方便以后复习。
一、没有 RPC 会怎样?
两个服务之间通讯。订单服务查用户服务。
方案 A(没有 RPC):
// OrderService 里
String json = restTemplate.getForObject("http://user-service/getUser?id=1", String.class);
User user = JSON.parse(json);
三个问题:
- 手动拼 URL、手动序列化反序列化
- HTTP 协议太重(请求头几百字节,数据才几个字节)
- 服务换地址得改代码
方案 B(RPC):
User user = userService.findById(1); // 像调本地方法一样调远程服务
RPC 的目标:让远程调用像本地调用一样简单。
二、Nacos / OpenFeign / Netty 各管哪块?
这是我当时问的问题——这三者不是一回事。
| 组件 | 管什么 |
|---|---|
| Nacos / Zookeeper | 服务注册发现——"UserService 在哪台机器上?" |
| OpenFeign | 声明式 HTTP 调用——封装了 URL 拼装 + JSON 序列化 |
| Netty | 底层通信——"数据怎么从 A 机器传到 B 机器?" |
OpenFeign 还是走 HTTP + Tomcat 那一套。Dubbo 用 Netty 走自定义 TCP 协议。
三、BIO vs NIO vs AIO
这是今天最核心的地基。
BIO:同步阻塞
serverSocket.accept(); // 没有连接就卡在这
inputStream.read(); // 没有数据就卡在这
一个连接一个线程。1000 个连接 = 1000 个线程,大部分在空等。
NIO:同步非阻塞
selector.select(1000); // 等 1 秒,没有数据就返回
一个线程注册 1000 个 Channel 到 Selector。有数据来了操作系统唤醒线程处理。没有数据时不占 CPU,睡眠等待。
注意:NIO 是同步的——你要主动去调 selector.select() 告诉操作系统"有数据了吗?"
它不是异步。异步是操作系统主动通知你。
AIO:异步非阻塞
channel.read(buffer, attachment, new CompletionHandler() {
void completed(result) {
// 操作系统主动通知你:数据读好了
}
});
但 AIO 在 Linux 上实现不成熟,Netty 官方推荐用 NIO 而不是 AIO。Windows 的 IOCP 是真正的异步,但服务端用 Linux 多。
我当时的理解过程
我以为 NIO 是"轮询查看 Channel,有就处理,没有就返回 null 继续轮询"
实际上 NIO 不是 while(true) 忙等。它调 selector.select() 时线程是休眠的,CPU 不空转。
那 Spring 里那个 while(true) 是什么?是 Tomcat 的 Acceptor:
// Tomcat Acceptor
while (true) {
SocketChannel socket = serverSocketChannel.accept(); // 阻塞的!
// 没连接时线程挂在这,不占 CPU
}
区别: Tomcat 的 accept() 是阻塞的,没连接就挂起。NIO 的 selector.select() 可以设置超时,没事件就超时返回。
四、TCP 粘包半包
这是 Netty 要解决的核心问题之一。
TCP 是流式协议
TCP 不保证每次 recv 刚好拿到一次 send 的数据。
粘包(合并):
send("Hello") send("World")
↓ ↓
TCP 合并了 → recv("HelloWorld")
半包(拆开):
send("HelloWorld")
↓
TCP 拆开了 → recv("Hel") recv("loWorld")
为什么会有粘包?TCP 的 Nagle 算法,两次 send 间隔短就合并发,提升效率。
为什么会有半包?MTU(以太网 1500 字节),减去 IP 头 20 + TCP 头 20,每个报文最多带 1460 字节。超过就拆。
三种边界方案
| 方案 | 原理 | 优缺点 |
|---|---|---|
| 固定长度 | 每个请求固定 N 字节,不够补位 | 浪费带宽 |
| 特殊分隔符 | 用 \n 或 \r\n 切分 | 正文里不能出现 |
| 长度字段 | 前 N 字节存正文长度,后面是正文 | 灵活、常用 |
Dubbo 用长度字段法 = 每个包先看前 4 字节拿到长度,再读正文。
五、Netty Pipeline
Netty 解决粘包半包的方式就是一行代码:
new LengthFieldBasedFrameDecoder(1024, 0, 4);
// 从第 0 字节开始读 4 字节作为长度,最大帧 1024 字节
管线结构
TCP 数据进来
↓
FrameDecoder(切帧)—— 把流切成完整的数据包
↓
XXXDecoder(解码)—— 字节 → Java 对象
↓
BusinessHandler(业务处理)—— 调你的 Service
↓
返回结果
各层职责
| 层 | 输入 | 输出 | 干了什么 |
|---|---|---|---|
| FrameDecoder | TCP 字节流 | 完整的数据帧 | 粘包半包处理 |
| 解码器 | 二进制帧 | String / 对象 | 协议解析 |
| BusinessHandler | 解析好的请求对象 | 响应结果 | 调你的 Service |
BusinessHandler 到底是什么?
这句话我问了很久才搞懂。用真实场景:
你的 EmpDemo 项目,没有 Netty 时,Tomcat 管线长这样:
TCP 数据 → Tomcat 切帧 → 解析 HTTP → DispatcherServlet → 你的 Controller
Controller 就是你的 BusinessHandler。
Dubbo RPC 用 Netty 时:
TCP 数据 → Netty 切帧 → Dubbo 协议解码 → Dubbo 内部 → 你的 Service
Dubbo 内部的这段代码,就是 BusinessHandler:
// 这就是 BusinessHandler 核心
public void handle(DubboRequest request) {
String interfaceName = request.getInterfaceName(); // "UserService"
String methodName = request.getMethodName(); // "findById"
Object[] args = request.getArgs(); // [1]
Object serviceImpl = serviceBeans.get(interfaceName); // 找到你写的实现
Method method = serviceImpl.getClass().getMethod(methodName);
Object result = method.invoke(serviceImpl, args); // 调你的代码
ctx.writeAndFlush(result); // 写回
}
BusinessHandler = 调你 Service 的那段胶水代码。 等价于 Spring MVC 的 Controller。
六、Socket
任何两个服务通讯都可以用 Socket。
服务端:
ServerSocket server = new ServerSocket(8080);
Socket client = server.accept(); // 等客户端连上来
client.getInputStream().read(); // 读客户端发的数据
client.getOutputStream().write("响应".getBytes()); // 写回去
客户端:
Socket socket = new Socket("localhost", 8080); // 连服务端
socket.getOutputStream().write("请求".getBytes()); // 发数据
socket.getInputStream().read(); // 读服务端响应
连上之后两边对称:InputStream 读,OutputStream 写。
七、Netty + Socket = 传统 IM
IM 和 HTTP 的本质区别:
HTTP:客户端请求 → 服务端响应 → 断开
IM: 客户端连接 → 一直保持 → 服务端随时推消息
IM 架构:
服务端(Netty 维护百万个长连接)
┌────────────┐
│ 用户A的连接 │
│ 用户B的连接 │
│ 用户C的连接 │
│ ... │
└────────────┘
A 给 B 发消息:
1. A 通过自己的连接发"给 B:你好"
2. 服务端找到 B 的连接
3. 推给 B
4. B 收到
QQ、微信早期版本、WhatsApp 都是这个模式。
Netty 在这里就是帮管那些百万个长连接的——心跳检测、粘包半包、断线重连,一行配置搞定。
八、一条完整链路串联
把我学的所有东西串起来:
Dubbo(RPC 框架)
│
用 Netty 做通信
│
Netty 封装了 Java NIO
│
NIO 是一个线程管多个 Channel
│
Channel 是 Socket 的封装
│
Socket = TCP 连接的抽象
│
TCP 有粘包半包问题
│
Netty 用 LengthFieldBasedFrameDecoder 解决
│
解码后的数据交给 BusinessHandler 调 Service
后记
这篇没有一行 Netty 的编程代码,因为今天学的就是这些底层概念。理解了这些,再看 Netty 的源码和 Dubbo 的源码就不会一头雾水了——知道它在解决什么问题,才看得懂代码为什么那样写。