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();
        }
    }
}
个人经验:生产环境中,序列化建议用 Protobuf 或 Kryo,JDK 原生序列化性能差、且存在安全漏洞。这里用 ObjectStream 只是为了演示原理。

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
    }
}
注意:这个简易框架没有处理异常重试、连接池、超时控制。生产级 RPC 框架(如 Dubbo、gRPC)在这些方面做了大量优化。我曾经在生产环境遇到过连接泄漏的问题,就是因为没有复用 Socket 连接。

4.3 从简易到生产:还需要做什么?

手写框架能帮你理解原理,但真要上生产,还差得远。我个人建议你关注这几个方向:

  • 连接复用:用连接池管理 Socket,避免频繁创建销毁
  • 负载均衡:随机、轮询、一致性哈希,根据场景选择
  • 超时与重试:设置合理的超时时间,幂等接口可以自动重试
  • 服务治理:熔断、限流、降级,防止雪崩

你想想看,如果每次调用都新建一个 Socket,高并发下系统很快就撑不住了。我记得有一次压测,就是因为没做连接复用,QPS 刚到 500 就报错,优化后直接飙到 5000。

好了,RPC 的核心原理就讲到这里。下一章我们会深入 Dubbo 的源码,看看工业级框架是怎么实现这些机制的。如果你在写 RPC 框架时遇到问题,欢迎随时交流。