在 Spring Boot 微服务架构中,RPC 远程调用通常被理解为带服务发现与负载均衡的完整框架,但真正的通信核心并不复杂:客户端将方法调用信息封装成二进制数据,服务端解析后反射执行并返回结果。Netty 作为高性能网络框架,天然适合做这件事。本文会从零开始实现一个自定义 RPC 框架,重点说明通信协议、编解码、请求关联和 Spring Boot 启动方式,让读者既能理解原理,也能获得一套可继续扩展的代码骨架。

一、为什么用 Netty 替代传统 HTTP 调用
很多内部服务之间的调用使用 HTTP 加 JSON,优点是调试方便、跨语言友好,但每次请求通常需要重新建立连接或依赖线程池管理短连接,并且 JSON 文本序列化会产生较多字节。Netty 基于 NIO 多路复用,可以用更少的线程支持大量长连接,配合自定义二进制协议,可显著降低序列化体积和 CPU 占用。更重要的是,Netty 把连接、编解码、业务处理拆成独立的 ChannelHandler,开发者可以按需插入心跳、限流、告警等逻辑,而不用修改 HTTP 容器配置。
在 Spring Boot 项目中引入 Netty 并不需要替换 Web 容器,只需要额外监听一个 TCP 端口。RPC 服务端启动时通过 ServerBootstrap 绑定端口,客户端通过 Bootstrap 连接服务端。Netty 的 bossGroup 负责 accept 连接,workerGroup 负责后续读写,默认线程数可以为 CPU 核数的 2 倍,但内部 RPC 场景可根据 IO 密集程度调整。下面是一个典型的服务端启动代码:
@Component
public class RpcServer {
private final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
public void start(int port) throws InterruptedException {
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(65535, 0, 4, 0, 4))
.addLast(new RpcDecoder())
.addLast(new RpcEncoder())
.addLast(new RpcServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.TCP_NODELAY, true);
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
二、消息协议与编解码器怎么设计
自定义 RPC 协议至少要回答两个问题:如何切分一条完整消息,以及如何把 Java 对象转成字节。TCP 是字节流,多个请求包粘在一起或一个请求被拆成多个包是常见现象。Netty 提供的 LengthFieldBasedFrameDecoder 可以根据报文头中的长度字段自动切分数据帧,代码里配置为前 4 字节表示后续长度,适合大多数自定义协议。
协议头可以设计为 16 字节固定长度:魔数、版本、消息类型、序列化类型、请求 ID、正文长度。魔数用于快速识别非法请求,版本用于后续协议升级,请求 ID 用于关联客户端与响应。正文序列化建议从 JSON 起步,后续替换为 Kryo 或 Protobuf,编解码器只需根据 serializationType 选择对应实现。
下面先定义消息结构:
public class RpcMessage {
private short magic = 0x4E52;
private byte version = 1;
private byte messageType;
private byte serializationType;
private long requestId;
private int length;
private Object data;
// getter / setter 省略
}
解码流程通常为:读取 16 字节头,校验魔数,读取长度后检查可读字节是否足够,不够则重置读索引等待更多数据,足够则读取 body 并反序列化。这个模式可以避免半包读到不完整消息导致解析错误。
public class RpcDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 16) {
return;
}
in.markReaderIndex();
short magic = in.readShort();
if (magic != 0x4E52) {
ctx.close();
return;
}
byte version = in.readByte();
byte messageType = in.readByte();
byte serializationType = in.readByte();
long requestId = in.readLong();
int length = in.readInt();
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
}
byte[] body = new byte[length];
in.readBytes(body);
Object data = Serializer.deserialize(body, serializationType);
RpcMessage message = new RpcMessage();
message.setMagic(magic);
message.setVersion(version);
message.setMessageType(messageType);
message.setSerializationType(serializationType);
message.setRequestId(requestId);
message.setLength(length);
message.setData(data);
out.add(message);
}
}
编码器相对简单,先写消息头,再写序列化后的 body。需要注意 writeInt(length) 和实际字节数保持一致,并且每个 write 操作后正确释放或复用 ByteBuf。
三、服务注册与客户端动态代理
服务端需要维护一个服务名到实例的映射表。通过 Spring 的 BeanPostProcessor 扫描带有 @RpcService 注解的 Bean,启动时自动注册,这样新增服务只需要在实现类上添加注解,不用手工写 Map。收到请求后,处理器根据 RpcRequest 中的 serviceName 找到实例,再通过反射调用方法。反射调用是同步的,建议将业务处理提交到独立线程池,避免阻塞 Netty 的 IO 线程。
客户端动态代理是 RPC 框架最常用的入口。JDK 的 Proxy 可以在运行时生成接口实现,当业务代码调用接口方法时,代理对象把方法名、参数类型、参数值封装成 RpcRequest,通过 Netty 通道发送给服务端。每个请求带唯一 requestId,客户端维护一个 ConcurrentHashMap 保存 requestId 到 CompletableFuture 的映射。服务端响应后,客户端 handler 根据 requestId 找到对应 Future 并 complete,这样代理方法就能返回结果。相比同步阻塞等待,这种异步关联方式可以避免线程被大量挂起。
public class RpcServerHandler extends SimpleChannelInboundHandler<RpcMessage> {
private final Map<String, Object> serviceMap;
public RpcServerHandler(Map<String, Object> serviceMap) {
this.serviceMap = serviceMap;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcMessage msg) throws Exception {
RpcRequest request = (RpcRequest) msg.getData();
Object service = serviceMap.get(request.getServiceName());
Method method = service.getClass().getMethod(request.getMethodName(), request.getParameterTypes());
Object result = method.invoke(service, request.getParameters());
RpcResponse response = new RpcResponse();
response.setRequestId(request.getRequestId());
response.setResult(result);
RpcMessage responseMsg = new RpcMessage();
responseMsg.setMessageType((byte) 2);
responseMsg.setRequestId(request.getRequestId());
responseMsg.setData(response);
ctx.writeAndFlush(responseMsg);
}
}
客户端代理示例:
public class RpcClientProxy implements InvocationHandler {
private final RpcClient client;
public RpcClientProxy(RpcClient client) {
this.client = client;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RpcRequest request = new RpcRequest();
request.setServiceName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameters(args);
return client.send(request).get(5, TimeUnit.SECONDS);
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> clazz) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, this);
}
}
四、Spring Boot 集成与实际调用验证
集成到 Spring Boot 时,可以用一个配置类创建 RpcServer 和 RpcClient 的 Bean,并在启动后开启服务端监听。服务端一般放在独立的线程中启动,避免阻塞 Spring Boot 主线程;客户端则可以在首次调用时建立连接,也可以使用连接池管理多个通道。为了简化,本文的客户端使用单个连接,通过长连接连续发送请求。
验证时定义两个接口:一个返回固定字符串,另一个接收参数并返回计算结果。服务端注册好实现类,客户端注入代理对象后直接调用。观察日志可以看到请求 ID、方法名和响应时间。如果出现调用超时,优先检查 LengthFieldBasedFrameDecoder 的长度偏移是否与编码器一致;如果出现响应错乱,则要检查 requestId 是否在编解码时正确传输。这些细节看似简单,但实际排查时往往最耗时。
@Configuration
public class RpcAutoConfiguration {
@Bean(destroyMethod = "stop")
public RpcServer rpcServer() {
return new RpcServer();
}
}
五、性能优化与常见问题
Netty RPC 的性能优化主要从几方面入手。第一,启用 TCP_NODELAY,关闭 Nagle 算法,降低小包延迟。第二,合理设置 workerGroup 线程数,不要盲目调大,线程过多会导致上下文切换增加。第三,考虑使用对象池和堆外内存减少 GC 压力,Netty 的 PooledByteBufAllocator 已经提供较好默认值,需要时可以显式配置。第四,序列化格式对性能影响很大,JSON 适合调试,Protobuf 或 Kryo 更适合生产环境,但更换序列化时要保证客户端和服务端版本一致。
常见问题包括:粘包半包导致解析异常、忘记释放 ByteBuf 导致内存泄漏、回调长时间不返回导致 Future 堆积、channel 空闲没有心跳被网关断开。解决这些问题需要在 pipeline 中加入心跳 handler、设置合理的超时时间,并为 pending 请求设置清理机制。只有把这些边界情况处理好,自定义 RPC 框架才能稳定运行。

