从零实现一个轻量级 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 倍》。

相关推荐
jiushiapwojdap8 小时前
Matlab GUI 界面设计:从入门到实战
开发语言·其他·matlab
灰子学技术8 小时前
Envoy HTTP 协议实现技术文档
网络·网络协议·http
lsx2024068 小时前
Go 语言范围(Range)
开发语言
初心未改HD8 小时前
Go语言同步原语Mutex、WaitGroup、Once深度解析
开发语言·golang
牛大兵8 小时前
IP扫描,局域网内扫描IP地址,找出有用,未使用的。正在使用的信息
服务器·网络·tcp/ip
lynnlovemin8 小时前
C++高精度加减乘除算法详解
开发语言·c++·算法·高精度
梅孔立8 小时前
Aspose.Words Java 表格动态删列、合并列、表头重建、全局字体统一解决方案
java·开发语言·word·aspose·在线编辑
Dxy12393102168 小时前
js如何根据开始位置结束位置在类表中取对应范围的数据
开发语言·javascript·ecmascript
minji...8 小时前
Linux 网络套接字编程(七)TCP服务端和客户端的实现——网络版本计算器
linux·运维·服务器·网络·c++·tcp/ip·udp