4. RPC框架原理:RPC核心概念(Stub、Proxy、Registry),动态代理实现RPC,手写简易RPC框架
聊到分布式系统,RPC 是绕不开的核心话题。很多同学觉得 RPC 很神秘,其实说白了,它就是「让调用远程服务像调用本地方法一样简单」。今天我就带大家把 RPC 的底裤扒干净,从核心概念到手写一个简易框架,一步到位。
4.1 RPC 的核心三剑客:Stub、Proxy、Registry
先问大家一个问题:你在代码里调一个远程方法,底层到底发生了什么?
我刚开始接触分布式时,也困惑了很久。后来发现,RPC 的核心就三个角色:Stub(存根)、Proxy(代理)、Registry(注册中心)。
4.1.1 Stub(存根)—— 远程调用的「翻译官」
Stub 是什么?它负责把本地调用转换成网络请求。客户端 Stub 把方法名、参数序列化成字节流,服务端 Stub 再把字节流反序列化,找到对应的方法执行。
我在项目中遇到过一个问题:某个服务突然超时严重,排查了半天,发现是 Stub 端的序列化框架版本不一致,导致解析出错。嗯,这里要注意,Stub 两端的序列化协议必须完全一致,否则就是灾难。
- 客户端 Stub:封装调用参数 → 序列化 → 发送网络请求
- 服务端 Stub:接收请求 → 反序列化 → 调用本地服务 → 返回结果
4.1.2 Proxy(代理)—— 让调用「透明化」
你想想看,如果每次远程调用都要手动写序列化、网络通信的代码,那得多痛苦?Proxy 就是来解决这个问题的。它通过动态代理技术,让你感觉不到远程调用的存在。
说白了,Proxy 就是一层「障眼法」。你在代码里写 userService.getUser(1),实际上 Proxy 在背后帮你完成了网络调用。我个人习惯用 JDK 动态代理来实现,因为它不需要额外引入第三方库。
4.1.3 Registry(注册中心)—— 服务的「通讯录」
服务提供者启动后,把自己的地址(IP + 端口)注册到 Registry。服务消费者要调用时,先去 Registry 拉取地址列表。这样服务提供者扩容、缩容,消费者完全无感知。
我曾经踩过一个坑:注册中心挂了,所有服务调用都失败了。后来我加了本地缓存 + 心跳检测,即使 Registry 短暂不可用,也能基于缓存继续工作。避坑指南:永远不要完全依赖注册中心的可用性。
| 角色 | 类比 | 核心功能 |
|---|---|---|
| Stub | 翻译官 | 序列化/反序列化、网络通信 |
| Proxy | 中介 | 透明化远程调用、负载均衡 |
| Registry | 通讯录 | 服务注册、发现、健康检查 |
4.2 动态代理实现 RPC —— 手写一个简易框架
理论说完了,咱们直接上代码。我会手写一个最简 RPC 框架,让你看到核心原理。
4.2.1 定义接口和实现
先定义一个用户服务接口:
public interface UserService {
String getUserName(int userId);
}
服务端实现:
public class UserServiceImpl implements UserService {
@Override
public String getUserName(int userId) {
return "用户_" + userId;
}
}
4.2.2 服务端:注册服务 + 监听请求
服务端启动时,把服务注册到 Registry,然后开启 Socket 监听。这里我用一个简单的 Map 模拟注册中心:
public class RpcServer {
// 模拟注册中心:服务名 → 服务实例
private static Map<String, Object> serviceRegistry = new HashMap<>();
public void register(Class<?> serviceInterface, Object serviceImpl) {
serviceRegistry.put(serviceInterface.getName(), serviceImpl);
}
public void start(int port) throws Exception {
ServerSocket server = new ServerSocket(port);
System.out.println("服务端启动,端口:" + port);
while (true) {
Socket socket = server.accept();
// 每个请求开一个线程处理(生产环境用线程池)
new Thread(() -> handleRequest(socket)).start();
}
}
private void handleRequest(Socket socket) {
try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
// 读取请求:接口名、方法名、参数类型、参数值
String interfaceName = in.readUTF();
String methodName = in.readUTF();
Class<?>[] paramTypes = (Class<?>[]) in.readObject();
Object[] args = (Object[]) in.readObject();
// 找到服务实现
Object service = serviceRegistry.get(interfaceName);
Method method = service.getClass().getMethod(methodName, paramTypes);
Object result = method.invoke(service, args);
// 返回结果
out.writeObject(result);
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.2.3 客户端:动态代理生成 Stub
客户端通过 JDK 动态代理,生成一个代理对象。你调用代理对象的方法时,它会自动发起网络请求:
public class RpcClient {
@SuppressWarnings("unchecked")
public static <T> T getProxy(Class<T> interfaceClass, String host, int port) {
return (T) Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class<?>[]{interfaceClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 建立 Socket 连接
try (Socket socket = new Socket(host, port);
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
// 发送请求数据
out.writeUTF(interfaceClass.getName());
out.writeUTF(method.getName());
out.writeObject(method.getParameterTypes());
out.writeObject(args);
out.flush();
// 读取返回结果
return in.readObject();
}
}
}
);
}
}
4.2.4 测试运行
启动服务端:
public class ServerMain {
public static void main(String[] args) throws Exception {
RpcServer server = new RpcServer();
server.register(UserService.class, new UserServiceImpl());
server.start(8888);
}
}
客户端调用:
public class ClientMain {
public static void main(String[] args) {
UserService userService = RpcClient.getProxy(UserService.class, "127.0.0.1", 8888);
String name = userService.getUserName(1001);
System.out.println("调用结果:" + name); // 输出:用户_1001
}
}
4.3 从简易到生产:还需要做什么?
手写框架能帮你理解原理,但真要上生产,还差得远。我个人建议你关注这几个方向:
- 连接复用:用连接池管理 Socket,避免频繁创建销毁
- 负载均衡:随机、轮询、一致性哈希,根据场景选择
- 超时与重试:设置合理的超时时间,幂等接口可以自动重试
- 服务治理:熔断、限流、降级,防止雪崩
你想想看,如果每次调用都新建一个 Socket,高并发下系统很快就撑不住了。我记得有一次压测,就是因为没做连接复用,QPS 刚到 500 就报错,优化后直接飙到 5000。
好了,RPC 的核心原理就讲到这里。下一章我们会深入 Dubbo 的源码,看看工业级框架是怎么实现这些机制的。如果你在写 RPC 框架时遇到问题,欢迎随时交流。