跳到正文
Joeplover
后端开发·2026-07-10·约 6 分钟阅读

Netty RPC 底层硬核拆解:从 NIO 到 Socket 到粘包半包,一个下午全打通

从 BIO 阻塞模型开始,一路推到 Netty 的 Pipeline、Dubbo 的 BusinessHandler、RPC 的完整链路。TCP 粘包半包、NIO 为什么不是异步、AIO 为什么没人用,全用我自己的思路串了一遍。

Network server with fiber optic cables

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);

三个问题:

  1. 手动拼 URL、手动序列化反序列化
  2. HTTP 协议太重(请求头几百字节,数据才几个字节)
  3. 服务换地址得改代码

方案 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
     ↓
返回结果

各层职责

层输入输出干了什么
FrameDecoderTCP 字节流完整的数据帧粘包半包处理
解码器二进制帧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 的源码就不会一头雾水了——知道它在解决什么问题,才看得懂代码为什么那样写。