从零实现一个轻量级 RPC 框架:通信协议与动态代理的核心原理

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!
技术热点 · 源码实战 · 万字长文拆解 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,核心要解决三个问题:

  1. 协议设计:服务端怎么知道一个请求从哪里开始、到哪里结束

  2. 代理与调用:如何让调用远程方法像调用本地方法一样自然

  3. 服务发现(简化版):服务端如何根据请求找到对应的实现类

我们逐一来实现。


二、自定义通信协议

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 的主要差距在于:

  1. 没有注册中心:目前硬编码 IP:Port,可引入 ZooKeeper / Nacos。

  2. 没有负载均衡:可增加随机、轮询、一致性 Hash 策略。

  3. 没有连接池:每次新建 Socket(性能差),应复用长连接。

  4. 没有超时控制:需要异步 Future + 超时机制。

  5. 没有过滤器链:日志、监控、限流等功能无法插拔。

如果把这几个点都补上,基本上就是一个可用的轻量级 RPC 了。


八、总结

本文从一个非常低的起点(裸 Socket + 反射)开始,逐步构建了一个完整的 RPC 核心流程。你看到的不只是"如何使用 RPC",而是:

  • 协议中的魔数与长度字段如何解决粘包

  • 动态代理如何把方法调用转成网络请求

  • 服务端如何通过注册表 + 反射完成方法执行

  • 序列化框架在其中的角色

下次你在项目里引入 OpenFeign 或 Dubbo 时,应该能更清晰地理解其底层在做什么。

全文代码已整理到 GitHub(示例中未给出真实链接,实际可补充)。欢迎在评论区讨论你眼中的 RPC 核心难点。

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发。下期预告:《从 Socket 到 Netty:重写这个 RPC 框架,性能提升 10 倍》。

相关推荐
用户805533698033 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner3 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz8 天前
QML Hello World 入门示例
qt
xcyxiner11 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner11 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner12 天前
DicomViewer (添加模型类)3
qt
xcyxiner12 天前
DicomViewer (目录调整) 2
qt
xcyxiner12 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00614 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术14 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript