

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
技术热点 · 源码实战 · 万字长文拆解 RPC 调用细节
摘要:
本文手把手教你实现一个极简RPC框架,深入剖析RPC底层原理。从自定义TCP协议解决粘包问题开始,到动态代理屏蔽网络调用细节,再到反射与服务注册机制,完整演示了RPC调用的核心流程。通过Java原生序列化示例,展示了客户端代理如何将方法调用转为网络请求,服务端如何通过反射执行并返回结果。
文章还探讨了序列化选型(JSON/Hessian/Protobuf)的优劣比较,并指出与生产级RPC框架(如Dubbo/gRPC)在注册中心、负载均衡等方面的差距。通过这个实践项目,开发者能透彻理解RPC框架的关键技术点:协议设计、代理机制和服务发现。
前言
微服务架构下,RPC(远程过程调用)几乎成了开发者的"日常工具"。但很多同学用得很熟练,问到"Feign 和 Dubbo 的本质区别""为什么需要序列化协议""动态代理到底做了什么"时,却往往答不上来。
这篇文章不是教你如何使用现成的 RPC 框架,而是手把手带你实现一个极简版 RPC 框架,涉及:
自定义 TCP 通信协议(解决粘包问题)
Java 动态代理(屏蔽网络调用细节)
反射与服务注册(服务端分发)
序列化选型(JSON vs Hessian vs Protobuf)
全程代码可运行,看完你将对 RPC 的底层原理有一个非常具体的认知。
一、RPC 调用流程回顾
一个完整的 RPC 调用包含 5 个关键角色:
text
[Client]
↓ 调用代理对象
[动态代理] → 将方法+参数转成请求对象 → 序列化 →
↓
[网络传输] (Socket)
↓
[Server 接收] → 反序列化 → 根据接口名+方法名找到实现类 →
↓
[反射执行] → 返回结果 → 再序列化 → 写回 Client
要自己实现一个 RPC,核心要解决三个问题:
-
协议设计:服务端怎么知道一个请求从哪里开始、到哪里结束
-
代理与调用:如何让调用远程方法像调用本地方法一样自然
-
服务发现(简化版):服务端如何根据请求找到对应的实现类
我们逐一来实现。
二、自定义通信协议
TCP 是流式协议,没有天然的"消息边界"。如果连续发送两个 RPC 请求,服务端收到的可能是半包或粘包的数据。解决方案:在每个消息前面加上长度字段。
协议格式(简单版)
text
+------------+-------------+----------------+
| 魔数(2B) | 长度(4B) | body(bytes) |
+------------+-------------+----------------+
-
魔数:用来快速判断是否是合法协议包(比如 0xCAFE)
-
长度:body 的字节长度
-
body:实际请求或响应数据(序列化后的字节)
编码器(客户端发送)
java
java
public class RpcEncoder {
public static byte[] encode(byte[] body) {
ByteBuffer buffer = ByteBuffer.allocate(2 + 4 + body.length);
buffer.putShort((short) 0xCAFE); // 魔数
buffer.putInt(body.length); // 长度
buffer.put(body); // 数据
return buffer.array();
}
}
解码器(服务端接收)
服务端需要循环读 Socket 输入流,每次先读 6 个字节(2+4),解析出长度,再读取对应长度的 body 数据。
java
java
public static byte[] decode(InputStream in) throws IOException {
DataInputStream dis = new DataInputStream(in);
short magic = dis.readShort();
if (magic != 0xCAFE) {
throw new RuntimeException("非法协议包");
}
int len = dis.readInt();
byte[] body = new byte[len];
dis.readFully(body); // 确保读满 len 字节
return body;
}
这是 Netty 中 LengthFieldBasedFrameDecoder 的极简原型。
三、动态代理:让远程调用像本地一样
客户端不应该了解 Socket 细节。我们通过 java.lang.reflect.Proxy 生成一个代理对象:
java
java
public class RpcClientProxy implements InvocationHandler {
private String host;
private int port;
public RpcClientProxy(String host, int port) {
this.host = host;
this.port = port;
}
public <T> T getProxy(Class<T> clazz) {
return (T) Proxy.newProxyInstance(
clazz.getClassLoader(),
new Class[]{clazz},
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 构建请求对象
RpcRequest request = new RpcRequest();
request.setInterfaceName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameters(args);
// 2. 通过 Socket 发送请求,并等待响应
try (Socket socket = new Socket(host, port);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream())) {
oos.writeObject(request);
RpcResponse response = (RpcResponse) ois.readObject();
if (response.getError() != null) {
throw response.getError();
}
return response.getResult();
}
}
}
这里为了方便演示直接用了 Java 原生序列化,生产环境会换成更高效的序列化协议。
四、服务端:注册表 + 反射执行
服务端需要维护一个 Map:接口名 → 实现类对象。
java
java
public class RpcServer {
private Map<String, Object> serviceMap = new ConcurrentHashMap<>();
private ExecutorService threadPool = Executors.newCachedThreadPool();
public void registerService(Object service) {
Class<?>[] interfaces = service.getClass().getInterfaces();
for (Class<?> anInterface : interfaces) {
serviceMap.put(anInterface.getName(), service);
System.out.println("注册服务: " + anInterface.getName());
}
}
public void start(int port) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(port)) {
while (true) {
Socket socket = serverSocket.accept();
threadPool.submit(() -> handleRequest(socket));
}
}
}
private void handleRequest(Socket socket) {
try (ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream())) {
RpcRequest request = (RpcRequest) ois.readObject();
// 找到服务实现
Object service = serviceMap.get(request.getInterfaceName());
if (service == null) {
throw new RuntimeException("未找到服务: " + request.getInterfaceName());
}
// 反射调用
Method method = service.getClass().getMethod(
request.getMethodName(),
request.getParameterTypes()
);
Object result = method.invoke(service, request.getParameters());
// 返回响应
RpcResponse response = RpcResponse.success(result);
oos.writeObject(response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
五、Demo 演示:调用远程计算服务
定义接口
java
public interface Calculator {
int add(int a, int b);
}
服务端实现
java
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
}
// 启动服务端
RpcServer server = new RpcServer();
server.registerService(new CalculatorImpl());
server.start(8888);
客户端调用
java
RpcClientProxy proxy = new RpcClientProxy("127.0.0.1", 8888);
Calculator calculator = proxy.getProxy(Calculator.class);
int result = calculator.add(10, 20);
System.out.println(result); // 输出 30,完全像本地方法
看到没有?客户端没有任何网络代码,也没有序列化/反序列化逻辑 ------ 全部被动态代理屏蔽了。
六、进阶话题:序列化选型
上面用了 Java 原生 ObjectOutputStream,存在几个问题:
-
跨语言不友好(Python/Go 无法调用)
-
性能较差,序列化体积大
-
安全性问题
常见替代方案:
| 序列化方案 | 体积 | 速度 | 跨语言 | 可读性 |
|---|---|---|---|---|
| Java 原生 | 大 | 慢 | ❌ | 二进制 |
| JSON | 中 | 中 | ✅ | 高 |
| Hessian2 | 小 | 快 | ✅ | 二进制 |
| Protobuf | 很小 | 很快 | ✅ | 二进制 |
在我们的自定义协议中,只需要替换编码/解码时使用的序列化工具即可,与协议长度字段完全解耦。
示例用 Protobuf 改造:
java
java
// 编码
byte[] body = calcRequest.toByteArray(); // protobuf 生成的对象
ByteBuffer buffer = ByteBuffer.allocate(2 + 4 + body.length);
// 解码
CalcRequest req = CalcRequest.parseFrom(body);
七、还可以优化的点
目前的实现是一个"玩具版" RPC,与 Dubbo / gRPC 的主要差距在于:
-
没有注册中心:目前硬编码 IP:Port,可引入 ZooKeeper / Nacos。
-
没有负载均衡:可增加随机、轮询、一致性 Hash 策略。
-
没有连接池:每次新建 Socket(性能差),应复用长连接。
-
没有超时控制:需要异步 Future + 超时机制。
-
没有过滤器链:日志、监控、限流等功能无法插拔。
如果把这几个点都补上,基本上就是一个可用的轻量级 RPC 了。
八、总结
本文从一个非常低的起点(裸 Socket + 反射)开始,逐步构建了一个完整的 RPC 核心流程。你看到的不只是"如何使用 RPC",而是:
-
协议中的魔数与长度字段如何解决粘包
-
动态代理如何把方法调用转成网络请求
-
服务端如何通过注册表 + 反射完成方法执行
-
序列化框架在其中的角色
下次你在项目里引入 OpenFeign 或 Dubbo 时,应该能更清晰地理解其底层在做什么。
全文代码已整理到 GitHub(示例中未给出真实链接,实际可补充)。欢迎在评论区讨论你眼中的 RPC 核心难点。
如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发。下期预告:《从 Socket 到 Netty:重写这个 RPC 框架,性能提升 10 倍》。