尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
如何实现一个简单的RPC
在如何给老婆解释什么是RPC中我们讨论了RPC的实现思路。那么这一次就让我们通过代码来实现一个简单的RPC吧一、RPC的实现原理正如上一讲所说RPC主要是为了解决的两个问题解决分布式系统中服务之间的调用问题。远程调用时要能够像本地调用一样方便让调用者感知不到远程调用的逻辑。1.1 本地调用 vs 远程调用还是以计算器Calculator为例如果实现类CalculatorImpl是放在本地的那么直接调用即可现在系统变成分布式了CalculatorImpl和调用方不在同一个地址空间那么就必须要进行远程过程调用1.2 RPC完整流程那么如何实现远程过程调用也就是RPC呢一个完整的RPC流程可以用下面这张图来描述其中左边的Client对应的就是前面的Service A而右边的Server对应的则是Service B。下面一步一步详细解释一下。客户端调用Service A的应用层代码中调用了Calculator的一个实现类的add方法希望执行一个加法运算Stub代理这个Calculator实现类内部并不是直接实现计算器的加减乘除逻辑而是通过远程调用Service B的RPC接口来获取运算结果因此称之为Stub远程通讯Stub怎么和Service B建立远程通讯呢这时候就要用到远程通讯工具了也就是图中的Run-time Library这个工具将帮你实现远程通讯的功能比如Java的Socket就是这样一个库当然你也可以用基于Http协议的HttpClient或者其他通讯工具类都可以RPC并没有规定说你要用何种协议进行通讯序列化与传输Stub通过调用通讯工具提供的方法和Service B建立起了通讯然后将请求数据发给Service B。需要注意的是由于底层的网络通讯是基于二进制格式的因此这里Stub传给通讯工具类的数据也必须是二进制比如calculator.add(1,2)你必须把参数值1和2放到一个Request对象里头这个Request对象当然不只这些信息还包括要调用哪个服务的哪个RPC接口等其他信息然后序列化为二进制再传给通讯工具类这一点也将在下面的代码实现中体现服务端接收二进制的数据传到Service B这一边了Service B当然也有自己的通讯工具通过这个通讯工具接收二进制的请求反序列化既然数据是二进制的那么自然要进行反序列化了将二进制的数据反序列化为请求对象然后将这个请求对象交给Service B的Stub处理服务端执行和之前的Service A的Stub一样这里的Stub也同样是个假玩意它所负责的只是去解析请求对象知道调用方要调的是哪个RPC接口传进来的参数又是什么然后再把这些参数传给对应的RPC接口也就是Calculator的实际实现类去执行。很明显如果是Java那这里肯定用到了反射。结果返回RPC接口执行完毕返回执行结果现在轮到Service B要把数据发给Service A了怎么发一样的道理一样的流程只是现在Service B变成了ClientService A变成了Server而已Service B反序列化执行结果-传输给Service A-Service A反序列化执行结果 - 将结果返回给Application完毕。理论的讲完了是时候把理论变成实践了。二、代码实现一个简单的RPC示例本文的示例代码可到Github下载。2.1 客户端实现首先是Client端的应用层怎么发起RPCComsumerApppublic class ComsumerApp { public static void main(String[] args) { Calculator calculator new CalculatorRemoteImpl(); int result calculator.add(1, 2); } }通过一个CalculatorRemoteImpl我们把RPC的逻辑封装进去了客户端调用时感知不到远程调用的麻烦。下面再来看看CalculatorRemoteImpl代码有些多但是其实就是把上面的2、3、4几个步骤用代码实现了而已CalculatorRemoteImplpublic class CalculatorRemoteImpl implements Calculator { public int add(int a, int b) { ListString addressList lookupProviders(Calculator.add); String address chooseTarget(addressList); try { Socket socket new Socket(address, PORT); // 将请求序列化 CalculateRpcRequest calculateRpcRequest generateRequest(a, b); ObjectOutputStream objectOutputStream new ObjectOutputStream(socket.getOutputStream()); // 将请求发给服务提供方 objectOutputStream.writeObject(calculateRpcRequest); // 将响应体反序列化 ObjectInputStream objectInputStream new ObjectInputStream(socket.getInputStream()); Object response objectInputStream.readObject(); if (response instanceof Integer) { return (Integer) response; } else { throw new InternalError(); } } catch (Exception e) { log.error(fail, e); throw new InternalError(); } } }代码解析add方法的前面两行lookupProviders和chooseTarget可能大家会觉得不明觉厉。服务发现分布式应用下一个服务可能有多个实例比如Service B可能有ip地址为198.168.1.11和198.168.1.13两个实例lookupProviders其实就是在寻找要调用的服务的实例列表。在分布式应用下通常会有一个服务注册中心来提供查询实例列表的功能。负载均衡查到实例列表之后要调用哪一个实例呢只时候就需要chooseTarget了其实内部就是一个负载均衡策略。由于我们这里只是想实现一个简单的RPC所以暂时不考虑服务注册中心和负载均衡因此代码里写死了返回ip地址为127.0.0.1。代码继续往下走我们这里用到了Socket来进行远程通讯同时利用ObjectOutputStream的writeObject和ObjectInputStream的readObject来实现序列化和反序列化。2.2 服务端实现最后再来看看Server端的实现和Client端非常类似ProviderApppublic class ProviderApp { private Calculator calculator new CalculatorImpl(); public static void main(String[] args) throws IOException { new ProviderApp().run(); } private void run() throws IOException { ServerSocket listener new ServerSocket(9090); try { while (true) { Socket socket listener.accept(); try { // 将请求反序列化 ObjectInputStream objectInputStream new ObjectInputStream(socket.getInputStream()); Object object objectInputStream.readObject(); log.info(request is {}, object); // 调用服务 int result 0; if (object instanceof CalculateRpcRequest) { CalculateRpcRequest calculateRpcRequest (CalculateRpcRequest) object; if (add.equals(calculateRpcRequest.getMethod())) { result calculator.add(calculateRpcRequest.getA(), calculateRpcRequest.getB()); } else { throw new UnsupportedOperationException(); } } // 返回结果 ObjectOutputStream objectOutputStream new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(new Integer(result)); } catch (Exception e) { log.error(fail, e); } finally { socket.close(); } } } finally { listener.close(); } } }Server端主要是通过ServerSocket的accept方法来接收Client端的请求接着就是反序列化请求-执行-序列化执行结果最后将二进制格式的执行结果返回给Client。2.3 实现总结就这样我们实现了一个简陋而又详细的RPC。说它简陋是因为这个实现确实比较挫在下一小节会说它为什么挫。说它详细是因为它一步一步的演示了一个RPC的执行流程方便大家了解RPC的内部机制。三、当前实现的局限性这个RPC实现只是为了给大家演示一下RPC的原理要是想放到生产环境去用那是绝对不行的。3.1 缺乏通用性我通过给Calculator接口写了一个CalculatorRemoteImpl来实现计算器的远程调用下一次要是有别的接口需要远程调用是不是又得再写对应的远程调用实现类这肯定是很不方便的。那该如何解决呢先来看看使用Dubbo时是如何实现RPC调用的Reference private Calculator calculator; ... calculator.add(1,2); ...Dubbo通过和Spring的集成在Spring容器初始化的时候如果扫描到对象加了Reference注解那么就给这个对象生成一个代理对象这个代理对象会负责远程通讯然后将代理对象放进容器中。所以代码运行期用到的calculator就是那个代理对象了。我们可以先不和Spring集成也就是先不采用依赖注入但是我们要做到像Dubbo一样无需自己手动写代理对象怎么做呢那自然是要求所有的远程调用都遵循一套模板把远程调用的信息放到一个RpcRequest对象里面发给Server端Server端解析之后就知道你要调用的是哪个RPC接口、以及入参是什么类型、入参的值又是什么就像Dubbo的RpcInvocationpublic class RpcInvocation implements Invocation, Serializable { private static final long serialVersionUID -4355285085441097045L; private String methodName; private Class?[] parameterTypes; private Object[] arguments; private MapString, String attachments; private transient Invoker? invoker;3.2 其他待优化点2、集成Spring在实现了代理对象通用化之后下一步就可以考虑集成Spring的IOC功能了通过Spring来创建代理对象这一点就需要对Spring的bean初始化有一定掌握了。3、长连接or短连接总不能每次要调用RPC接口时都去开启一个Socket建立连接吧是不是可以保持若干个长连接然后每次有rpc请求时把请求放到任务队列中然后由线程池去消费执行只是一个思路后续可以参考一下Dubbo是如何实现的。4、服务端线程池我们现在的Server端是单线程的每次都要等一个请求处理完才能去accept另一个socket的连接这样性能肯定很差是不是可以通过一个线程池来实现同时处理多个RPC请求同样只是一个思路。5、服务注册中心正如之前提到的要调用服务首先你需要一个服务注册中心告诉你对方服务都有哪些实例。Dubbo的服务注册中心是可以配置的官方推荐使用Zookeeper。如果使用Zookeeper的话要怎样往上面注册实例又要怎样获取实例这些都是要实现的。6、负载均衡如何从多个实例里挑选一个出来进行调用这就要用到负载均衡了。负载均衡的策略肯定不只一种要怎样把策略做成可配置的又要如何实现这些策略同样可以参考DubboDubbo - 负载均衡7、结果缓存每次调用查询接口时都要真的去Server端查询吗是不是要考虑一下支持缓存8、多版本控制服务端接口修改了旧的接口怎么办9、异步调用客户端调用完接口之后不想等待服务端返回想去干点别的事可以支持不10、优雅停机服务端要停机了还没处理完的请求怎么办......诸如此类的优化点还有很多这也是为什么实现一个高性能高可用的RPC框架那么难的原因。四、总结与展望当然我们现在已经有很多很不错的RPC框架可以参考了我们完全可以借鉴一下前人的智慧。后面如果有dian机zan会duo的话也将和大家分享一下如何一步一步优化现有的这块RPC代码把它做成一个小型RPC框架五、参考资料一本很棒的分布式书籍《大型网站系统与Java中间件实践》Dubbo 使用文档Dubbo 源码开发手册
RELATED

相关推荐

通俗易懂的 RPC 原理与实践:从生活场景到技术实现

通俗易懂的 RPC 原理与实践:从生活场景到技术实现

一个阳光明媚的早晨,老婆又在翻看我订阅的技术杂志。 “老公,什么是RPC呀,为什么你们程序员那么多黑话!”,老婆还是一如既往的好奇。 “RPC,就是Remote Procedure Call的简称呀,翻译成中文就是…

📅 2026/10/28 7:50:34
基于BOLT IoT的智能车库门监控系统:从硬件连接到云端自动化

基于BOLT IoT的智能车库门监控系统:从硬件连接到云端自动化

1. 项目概述:从“智能车库”到“智慧空间”的实践最近在折腾一个挺有意思的项目,用BOLT IoT模块给自己的车库做了个智能化改造。听起来可能有点“老生常谈”,毕竟智能家居概念火了这么多年。但说实话,很多市面上的成品要么功能大而…

📅 2026/9/15 13:46:42
2018年新车市场回顾:平台换代、技术博弈与二手车价值分析

2018年新车市场回顾:平台换代、技术博弈与二手车价值分析

1. 引言:为什么我们还在回顾2018年的新车?如果你是一个汽车爱好者,或者从事汽车媒体、市场分析、二手车评估等相关工作,你可能会觉得,在2024年这个时间点,去回顾2018年第24周(大约在6月中旬&…

📅 2026/10/8 4:33:28
MORE NEWS

更多资讯

📰

多模态智能技术解析:从原理到医疗与工业应用

1. 多模态智能的本质与突破2010年我在处理医疗影像数据时首次意识到:单一模态的数据就像盲人摸象。当时需要将CT扫描结果与病理报告关联分析,传统方法需要人工建立映射规则,这个过程耗时且容易出错。正是这种痛点催生了多模态智能的早期实践—…

📰

五大图像生成模型横评:FLUX.2、Seedream、Z-image、Qwen-Image、GLM-Image

2025年的图像生成圈,热闹得有些离谱。前脚 FLUX.2 刚放出消息,后脚 Qwen-Image、GLM-Image 就扎堆开源,再加上一直活跃在社区里的 Z-image,以及普通用户更熟悉的 Seedream(即梦),光是把这些名字…

📰

PE内存加载EXE:手写加载器的关键表与边界条件

简介:围绕进程镂空(RunPE)的代码示例,面向安全研究、逆向工程与系统编程学习者,演示如何借助已有进程在内存中加载并执行exe,避免可执行文件落盘,内容涉及Windows API、PE文件格式与进程管理等底…

📰

Cocos安卓ANT打包全链路:版本对齐、签名与排错

简介:这份 Cocos 安卓打包 ANT 全套配置资料面向在 Mac 环境下使用 Cocos 开发、需要将游戏工程编译并签名成 Android APK 的开发者,重点解决 ANT 工具安装、环境变量设置、构建脚本调整、签名配置及 release 打包中的常见问题,也可作为熟悉跨…

📰

Agent技能库越用越笨?SkillBrew多目标精炼实现技能库“做减法”

“技能库只增不减”,你发现没有,这不只是存储浪费的问题,它会让整个Agent系统越用越笨。我在做长尾任务Agent的时候,技能库从一百个涨到上千个,检索时间翻了几倍,Top-1命中率反而往下掉,有些老技…

📰

雷达信号处理链路拆解:DBF、相干积累与目标角度测量

简介:面向雷达信号处理与目标检测学习者的DBF数字波束形成仿真包,完整复现数字波束形成中的相干积累与目标角度测量流程,用以解决阵列信号处理中信噪比不足、角度分辨率受限的问题。压缩包为rar格式,共6个文件,含4个MA…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬