当前位置: 首页 > news >正文

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

http://www.cnnetsun.cn/news/2167854.html

相关文章:

  • 别再只用PPT画图了!试试这款39元的国产科研绘图神器AXglyph,附数学建模实战案例
  • Unity Mod Manager:轻松管理Unity游戏模组的终极解决方案
  • FITC标记的Siglec-2/CD22 Fc嵌合蛋白在B细胞免疫治疗研究中的应用
  • R 4.5正式版TS处理模块源码级拆解(src/main/timeseries.c新增fast_gregorian_parser,提速41倍)
  • AI GEO值得做吗
  • 五一劳动节|局放监测不“打烊”,致敬坚守在电网一线的每一个你
  • 你的BLDC仿真电流波形为啥是锯齿?手把手调Simscape双闭环PI参数(附调试记录)
  • IT内幕11:海思工程师薪资揭秘:芯片岗真的年包 50W+?
  • 【云藏山鹰代数信息系统】浅析气质砥砺学研究范式
  • 零售行业合同管理数智化转型解决方案
  • 第十四节:数据安全与越狱防御——给 Agent 穿上铠甲
  • Python正则表达式
  • 将8088 BootLoader分拆烧写到8086 ROM中
  • SoC FPGA在汽车雷达数字信号处理中的优势与应用
  • 推荐一下都江堰中央空调、地暖
  • 打卡18:有效括号
  • 从一道异步电路面试题出发,聊聊跨时钟域信号采样的那些‘坑’与最佳实践
  • 动手学深度学习(PyTorch版)深度详解(6):现代卷积神经网络-从经典模型到图像分类实战
  • 企业云安全四维防护框架与实践指南
  • 期货量化模拟转实盘检查清单:延迟、成交偏差与异常处理
  • 海棠山铁哥用《第一大道》对决《灵魂摆渡・浮生梦》,不躺平我们还有机会吗
  • 通过环境变量为Hermes Agent配置Taotoken自定义模型提供方的详细方法
  • 华三防火墙NAT Hairpin配置实战:内网用户也能用公网IP访问OA服务器(附完整命令)
  • 2026年阿里云Hermes Agent/OpenClaw搭建攻略+百炼token Plan配置解析攻略教程
  • 抖音直播数据采集终极指南:3个关键技术解决匿名用户识别难题
  • 从静态到动态:AI生成可交互虚拟场景的技术原理与实践
  • Windows下Python连接瀚高数据库(HGDB)踩坑记:SM3认证报错‘authentication method 13 not supported’的三种解法
  • GJB电磁兼容标准对加固SSD有哪些要求?测试项目与合格指标
  • CNV calling精度骤降37%?R 4.5环境变量与GRanges2.0版本冲突深度溯源(附一键修复脚本)
  • 告别后端转发:前端直传S3的权限安全与成本优化全解析