我开源了一套RPC框架,学爆它!

很多同学听到“开发框架”可能会有点胆怯,但其实开发RPC框架并不难,只要几个小时就能学会核心流程!能够快速给简历增加一个区别于增删改查的项目。而且,开发RPC框架涉及很多常用的技术知识点、还能学习到很多架构设计方面的思路和技巧。因此,强烈建议所有后端方向的同学,动手做个自己的RPC框架。

学习能力强的同学,不需要购买教程,也可以按照我划分的目录模块自学。

如图,整整12节详细的保姆级教程:

全文长达近7000字,建议大家收藏起来,慢慢学习。

专业定义:RPC(RemoteProcedureCall)即远程过程调用,是一种计算机通信协议,它允许程序在不同的计算机之间进行通信和交互,就像本地调用一样。

回到RPC的概念,RPC允许一个程序(称为服务消费者)像调用自己程序的方法一样,调用另一个程序(称为服务提供者)的接口,而不需要了解数据的传输处理过程、底层网络通信的细节等。这些都会由RPC框架帮你完成,使得开发者可以轻松调用远程服务,快速开发分布式系统。

举个例子,现在有个项目A提供了点餐服务,项目B需要调用点餐服务完成下单。

点餐服务和接口的示例伪代码如下:

如果没有RPC框架,项目B怎么调用项目A的服务呢?

示例伪代码如下:

看起来就跟调用自己项目的方法没有任何区别!是不是很丝滑?

RPC框架为什么能帮我们简化调用?如何实现一个RPC框架呢?

其实很简单,开局一张图,有服务消费者和服务提供者两个角色:

消费者想要调用提供者,就需要提供者启动一个web服务,然后通过请求客户端发送HTTP或者其他协议的请求来调用。

比如请求/order地址后,提供者会调用orderService的order方法:

但如果提供者提供了多个服务和方法,每个接口和方法都要单独写一个接口?消费者要针对每个接口写一段HTTP调用的逻辑么?

其实可以提供一个统一的服务调用接口,通过请求处理器根据客户端的请求参数来进行不同的处理、调用不同的服务和方法。

可以在服务提供者程序维护一个本地服务注册器,记录服务和对应实现类的映射。

举个例子,消费者要调用orderService服务的order方法,可以发送请求,参数为service=orderService,method=order,然后请求处理器会根据service从服务注册器中找到对应的服务实现类,并且通过Java的反射机制调用method指定的方法。

需要注意的是,由于Java对象无法直接在网络中传输,所以要对传输的参数进行序列化和反序列化。

为了简化消费者发请求的代码,实现类似本地调用的体验。可以基于代理模式,为消费者要调用的接口生成一个代理对象,由代理对象完成请求和响应的过程。

所谓代理,就是有人帮你做一些事情,不用自己操心。

至此,一个最简易的RPC框架架构图诞生了:

上图中的虚线框部分,就是RPC框架需要提供的模块和能力。

虽然上述设计已经跑通了基本调用流程,但离一个完备的RPC框架还有很大的差距,让我们带着问题来进一步完善下架构设计。

问题1:消费者如何知道提供者的调用地址呢?

类比生活场景,我们点外卖时,外卖小哥如何知道我们的地址和店铺的地址?肯定是买家和卖家分别填写地址,由平台来保存的。因此,我们需要一个注册中心,来保存服务提供者的地址。消费者要调用服务时,只需从注册中心获取对应服务的提供者地址即可。

架构图如下:

一般用现成的第三方注册中心,比如Redis、Zookeeper即可。

问题2:如果有多个服务提供者,消费者应该调用哪个服务提供者呢?

我们可以给服务调用方增加负载均衡能力,通过指定不同的算法来决定调用哪一个服务提供者,比如轮询、随机、根据性能动态调用等。

问题3:如果服务调用失败,应该如何处理呢?

为了保证分布式系统的高可用,我们通常会给服务的调用增加一定的容错机制,比如失败重试、降级调用其他接口等等。

除了上面几个经典设计外,如果想要做一个优秀的RPC框架,还要考虑很多问题。

比如:

所以,完成RPC项目并不难,但做一个完美的RPC项目却是难于上青天啊!

总结一下,我们可以通过做一个RPC项目学习到网络、序列化、代理、服务注册发现、负载均衡、容错、可扩展设计等知识,相信完成项目后会收获满满。

下面我们就从0开始,先完成一个简易版的RPC框架,后面再持续扩展优化。

架构设计图如下:

首先创建一个项目根目录yu-rpc,然后使用IDEA开发工具依次创建几个Maven模块。

整个基础RPC框架的项目目录如图:

分别介绍几个模块:

在示例项目中,我们将以一个最简单的用户服务为例,演示整个服务调用过程。下面我们依次实现上述的几个模块。

整个模块的结构如下:

1)编写用户实体类User:

packagecom.yupi.example.common.model;importjava.io.Serializable;/***用户*/publicclassUserimplementsSerializable{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}}注意,对象需要实现序列化接口,为后续网络传输序列化提供支持。

2)编写用户服务接口UserService,提供一个获取用户的方法:

服务提供者是真正实现了接口的模块。

1)在pom.xml文件中引入依赖:

功能是打印用户的名称,并且返回参数中的用户对象。

代码如下:

3)编写服务提供者启动类EasyProviderExample,之后会在该类的main方法中编写提供服务的代码。

packagecom.yupi.example.provider;importcom.yupi.example.common.service.UserService;importcom.yupi.yurpc.registry.LocalRegistry;importcom.yupi.yurpc.server.HttpServer;importcom.yupi.yurpc.server.VertxHttpServer;/***简易服务提供者示例*/publicclassEasyProviderExample{publicstaticvoidmain(String[]args){//提供服务}}最终得到的该模块目录如下:

服务消费者是需要调用服务的模块。

1)在pom.xml文件中引入依赖,和提供者模块的依赖一致:

2)创建服务消费者启动类EasyConsumerExample,编写调用接口的代码。

packagecom.yupi.example.consumer;importcom.yupi.example.common.model.User;importcom.yupi.example.common.service.UserService;importcom.yupi.yurpc.proxy.ServiceProxyFactory;/***简易服务消费者示例*/publicclassEasyConsumerExample{publicstaticvoidmain(String[]args){//todo需要获取UserService的实现类对象UserServiceuserService=null;Useruser=newUser();user.setName('yupi');//调用UsernewUser=userService.getUser(user);if(newUser!=null){System.out.println(newUser.getName());}else{System.out.println('user==null');}}}需要注意的是,现在是无法获取到userService实例的,所以预留为null。我们之后的目标是,能够通过RPC框架,快速得到一个支持远程调用服务提供者的代理对象,像调用本地方法一样调用UserService的方法。

最终得到的该模块目录如下:

接下来,我们要先让服务提供者提供可远程访问的服务。那么,就需要一个web服务器,能够接受处理请求、并返回响应。

web服务器的选择有很多,比如SpringBoot内嵌的Tomcat、NIO框架Netty和Vert.x等等。

此处鱼皮带大家使用高性能的NIO框架Vert.x来作为RPC框架的web服务器。

1)打开yu-rpc-easy项目,引入Vert.x和工具类的依赖:

2)编写一个web服务器的接口HttpServer,定义统一的启动服务器方法,便于后续的扩展,比如实现多种不同的web服务器。

packagecom.yupi.yurpc.server;/***HTTP服务器接口*/publicinterfaceHttpServer{/***启动服务器**@paramport*/voiddoStart(intport);}3)编写基于Vert.x实现的web服务器VertxHttpServer,能够监听指定端口并处理请求。

4)验证web服务器能否启动成功并接受请求。

修改示例服务提供者模块的EasyProviderExample类,编写启动web服务的代码,如下:

此时的RPC模块目录结构如下:

我们现在做的简易RPC框架主要是跑通流程,所以暂时先不用第三方注册中心,直接把服务注册到服务提供者本地即可。

在RPC模块中创建本地服务注册器LocalRegistry,当前目录结构如下:

使用线程安全的ConcurrentHashMap存储服务注册信息,key为服务名称、value为服务的实现类。之后就可以根据要调用的服务名称获取到对应的实现类,然后通过反射进行方法调用了。

注意,本地服务注册器和注册中心的作用是有区别的。注册中心的作用侧重于管理注册的服务、提供服务信息给消费者;而本地服务注册器的作用是根据服务名获取到对应的实现类,是完成调用必不可少的模块。

服务提供者启动时,需要注册服务到注册器中,修改EasyProviderExample代码如下:

但是在编写处理请求的逻辑前,我们要先实现序列化器模块。因为无论是请求或响应,都会涉及参数的传输。而Java对象是存活在JVM虚拟机中的,如果想在其他位置存储并访问、或者在网络中进行传输,就需要进行序列化和反序列化。

什么是序列化和反序列化呢?

有很多种不同的序列化方式,比如Java原生序列化、JSON、Hessian、Kryo、protobuf等。

为了实现方便,此处选择Java原生的序列化器。

1)在RPC模块中编写序列化接口Serializer,提供序列化和反序列化两个方法,便于后续扩展更多的序列化器。

2)基于Java自带的序列化器实现JdkSerializer,代码如下:

packagecom.yupi.yurpc.serializer;importjava.io.*;/***JDK序列化器*/publicclassJdkSerializerimplementsSerializer{/***序列化**@paramobject*@param*@return*@throwsIOException*/@Overridepublicbyte[]serialize(Tobject)throwsIOException{ByteArrayOutputStreamoutputStream=newByteArrayOutputStream();ObjectOutputStreamobjectOutputStream=newObjectOutputStream(outputStream);objectOutputStream.writeObject(object);objectOutputStream.close();returnoutputStream.toByteArray();}/***反序列化**@parambytes*@paramtype*@param*@return*@throwsIOException*/@OverridepublicTdeserialize(byte[]bytes,Classtype)throwsIOException{ByteArrayInputStreaminputStream=newByteArrayInputStream(bytes);ObjectInputStreamobjectInputStream=newObjectInputStream(inputStream);try{return(T)objectInputStream.readObject();}catch(ClassNotFoundExceptione){thrownewRuntimeException(e);}finally{objectInputStream.close();}}}上面这段代码无需记忆,需要用到的时候照抄即可,关键是要理解序列化和反序列化的区别。

当前RPC模块的目录结构如下:

请求处理器是RPC框架的实现关键,它的作用是:处理接收到的请求,并根据请求参数找到对应的服务和方法,通过反射实现调用,最后封装返回结果并响应请求。

1)在RPC模块中编写请求和响应封装类。

目录结构如下:

请求类RpcRequest的作用是封装调用所需的信息,比如服务名称、方法名称、调用参数的类型列表、参数列表。这些都是Java反射机制所需的参数。

响应类RpcResponse的作用是封装调用方法得到的返回值、以及调用的信息(比如异常情况)等。

packagecom.yupi.yurpc.model;importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;importjava.io.Serializable;/***RPC响应*/@Data@Builder@AllArgsConstructor@NoArgsConstructorpublicclassRpcResponseimplementsSerializable{/***响应数据*/privateObjectdata;/***响应数据类型(预留)*/privateClass<>dataType;/***响应信息*/privateStringmessage;/***异常信息*/privateExceptionexception;}2)编写请求处理器HttpServerHandler。

业务流程如下:

完整代码如下,配合上述流程和注释应该不难理解:

需要注意,不同的web服务器对应的请求处理器实现方式也不同,比如Vert.x中是通过实现Handler接口来自定义请求处理器的。并且可以通过request.bodyHandler异步处理请求。

3)给HttpServer绑定请求处理器。

修改VertxHttpServer的代码,通过server.requestHandler绑定请求处理器。

修改后的代码如下:

在项目准备阶段,我们已经预留了一段调用服务的代码,只要能够获取到UserService对象(实现类),就能跑通整个流程。

但UserService的实现类从哪来呢?

在之前的架构中讲过,我们可以通过生成代理对象来简化消费方的调用。

代理的实现方式大致分为2类:静态代理和动态代理,下面依次实现。

静态代理是指为每一个特定类型的接口或对象,编写一个代理类。

比如在example-consumer模块中,创建一个静态代理UserServiceProxy,实现UserService接口和getUser方法。

只不过实现getUser方法时,不是复制粘贴服务提供者UserServiceImpl中的代码,而是要构造HTTP请求去调用服务提供者。

需要注意发送请求前要将参数序列化,代码如下:

然后修改EasyConsumerExample,new一个代理对象并赋值给userService,就能完成调用:

/***简易服务消费者示例*/publicclassEasyConsumerExample{publicstaticvoidmain(String[]args){//静态代理UserServiceuserService=newUserServiceProxy();...}}静态代理虽然很好理解(就是写个实现类嘛),但缺点也很明显,我们如果要给每个服务接口都写一个实现类,是非常麻烦的,这种代理方式的灵活性很差!

所以RPC框架中,我们会使用动态代理。

动态代理的作用是,根据要生成的对象的类型,自动生成一个代理对象。

常用的动态代理实现方式有JDK动态代理和基于字节码生成的动态代理(比如CGLIB)。前者简单易用、无需引入额外的库,但缺点是只能对接口进行代理;后者更灵活、可以对任何类进行代理,但性能略低于JDK动态代理。

此处我们使用JDK动态代理。

1)在RPC模块中编写动态代理类ServiceProxy,需要实现InvocationHandler接口的invoke方法。

代码如下(几乎就是把静态代理的代码搬运过来):

解释下上述代码,当用户调用某个接口的方法时,会改为调用invoke方法。在invoke方法中,我们可以获取到要调用的方法信息、传入的参数列表等,这不就是我们服务提供者需要的参数么?用这些参数来构造请求对象就可以完成调用了。

需要注意的是,上述代码中,请求的服务提供者地址被硬编码了,需要使用注册中心和服务发现机制来解决。

没办法直接看懂上述代码也没关系,先跟着敲完,之后可以通过debug来帮助理解。

2)创建动态代理工厂ServiceProxyFactory,作用是根据指定类创建动态代理对象。

目录结构如图:

这里是使用了工厂设计模式,来简化对象的创建过程,代码如下:

packagecom.yupi.yurpc.proxy;importjava.lang.reflect.Proxy;/***服务代理工厂(用于创建代理对象)*/publicclassServiceProxyFactory{/***根据服务类获取代理对象**@paramserviceClass*@param*@return*/publicstaticTgetProxy(ClassserviceClass){return(T)Proxy.newProxyInstance(serviceClass.getClassLoader(),newClass[]{serviceClass},newServiceProxy());}}上述代码中,主要是通过Proxy.newProxyInstance方法为指定类型创建代理对象。

3)最后,在EasyConsumerExample中,就可以通过调用工厂来为UserService获取动态代理对象了。

至此,简易版的RPC框架已经开发完成,下面我们进行测试。

1)以debug模式启动服务提供者,执行main方法:

2)以debug模式启动服务消费者,执行main方法。

在ServiceProxy代理类中添加断点,可以看到调用userService时,实际是调用了代理对象的invoke方法,并且获取到了serviceName、methodName、参数类型和列表等信息。

如下图:

3)继续debug,可以看到序列化后的请求对象,结构是字节数组:

4)在服务提供者模块的请求处理器中打断点,可以看到接受并反序列化后的请求,跟发送时的内容一致:

5)继续debug,可以看到在请求处理器中,通过反射成功调用了方法,并得到了返回的User对象。

6)最后,在服务提供者和消费者模块中都输出了用户名称,说明整个调用过程成功。

以上,就是本期教程。麻雀虽小,五脏俱全。大家一定要自己动手实现,印象才会更深刻。

THE END
1.拓者设计吧室内设计室内设计师拓者设计吧(TuoZhe8),中国室内设计师互动平台。深耕设计领域十二年,拓者吧聚集了400万设计师、室内设计师、效果图设计师、软装设计师、装修设计人,设计创意群体中具有较高的影响力与号召力!https://www.tuozhe8.com/
2.安装Adobe应用程序的插件扩展和加载项开始前,请确保您的应用程序版本支持该扩展。请在下方选择您的操作系统并按照步骤安装扩展。有关更多信息,请参阅从命令行工作。 选择操作系统 Windows macOS Catalina 10.15 macOS(早期版本) 常见问题解答 我在安装插件或扩展时遇到问题。我该怎么办? 如何卸载插件或扩展? https://helpx.adobe.com/cn/creative-cloud/kb/installingextensionsandaddons.html
3.拓展设计吧素材网站图片免费下载共有1166247个拓展设计吧相关内容为您呈现 设计拓展吧 优之采 拓展坞设计 3 疯风 多合一拓展坞设计 59 DragonTan 生姜切丝 拓展坞×详情页设计全案 2 玉子芋头 亚马逊主图设计a 乐怡君要坚持 跨境电商 亚马逊主图设计案例 拓展坞渲染设计 16 亚马逊拍照设计 https://www.zcool.com.cn/tag/ZNTM3OTM2NA==.html
4.扩初设计包括哪些内容扩初设计包括哪些内容 今天来给大家分享一下关于扩初设计包括哪些内容的问题,以下是对此问题的归纳整理,让我们一起来看看吧。 扩初设计包括哪些内容 扩建设计是建筑设计的重要环节,是建筑设计的第一步,也是最关键的一步。最初的扩展设计包括什么?下面就一个一个介绍吧。https://www.opabc.com/youxi/166946.html
5.Invision官方出品!超全面的「设计系统」构建指南(一)用系统思维扩展设计 你可能已经清楚地意识到,设计系统已经成为当今软件行业的一个热门话题,并且理由也很充分。很多企业投资设计系统,因为他们认识到产品体验能够带来竞争优势,不仅能吸引和留住客户,更降低产品学习成本。 在重视设计系统的公司内部,通常能看见这种情况: https://www.uisdc.com/design-system-1
6.闲来无事,弄个“纯CSS”的伪3D柱状图吧~在这个过程中也算是有一些比较有意思的效果,今天先弄一个 “伪3D” 的柱状图吧。 样式设计 UI 给的稿子上,通常会带有一些“细微”的样式,用来提高观赏性,但是这里为了加快速度,我们就先实现一个比较 “比较纯粹” 的柱状图。 首先上效果图: 网络异常,图片无法展示 https://developer.aliyun.com/article/1117130
7.“吉祥物设计”扩展知识: 北京2022冬奥会吉祥物“冰墩墩”以熊猫为原型进行设计创作,墩墩意喻敦厚、敦实、可爱,契合熊猫的整体形象,象征着冬奥会运动员强壮有力的身体、坚韧不拔的意志和鼓舞人心的奥林匹克精神! 2022年北京冬奥会吉祥物——“冰墩墩” 大熊猫已在地球上生存了至少800万年,被誉为“活化石”和“中国国宝”,近年来https://www.meipian.cn/2u7ynvd2
8.咖啡店策划书(精选7篇)2、要确定咖啡店的经营形式,是纯咖啡馆,还是咖啡+简餐(酒水)的咖啡吧。我感觉最好建成是含简餐和酒水的咖啡吧。 四、给店面起名、设计店面标志、店面形象设计方案(装修方案) 1、吧台设计、厨房设备及机器定位上有考量的基础。 2、座位数、经济规模(坪数)。 https://www.ruiwen.com/cehuashu/7411084.html
9.码分多址(CDMA)的本质正交之美码分多址技术2.长途旅行中,你很困,旁边的两个家伙叽叽喳喳说个不停,如果他们使用你听不懂的语言,你照样能睡着,但是如果他们说的是普通话,那你就完蛋了,那就忍耐吧 三.该我们自己设计了 大自然已经设计了我们的大脑这种高度复杂的“码分多址设备”,我们当然使用这个设备也能造出一个克隆体,如果我们就是上帝,那么我们希望造https://blog.csdn.net/dog250/article/details/6420427
10.尧字歌教案同学们,今天咱们学习了听了古代帝王尧的故事,还学了以尧为声旁的形声字。其实,咱们中国方块汉字的魅力还远不止这些,有机会再带领大家来探索吧。 课堂作业新设计: 1.比一比再组词。 晓( )饶( )挠( )线( ) 烧( )浇( )绕( )绕( ) 附答案: https://www.jy135.com/jiaoan/865705.html
11.新课标版四年级上册数学《沏茶问题》教案(通用13篇)5. 师:现在,请同学们以小组为单位,先交流讨论之后把你的设计方案用你喜欢的`方式写在练习本上,并算算你们的安排需要多长时间? 6. 动手操作,主动探究: (1) 学生动手摆出方案,师巡视 (2) 学生展示,介绍自己的安排,和所用时间 预设: A:洗水壶(1分钟)→接水(1分钟)→烧水(8分钟)→沏茶(1分钟) https://www.fwsir.com/jiaoan/html/jiaoan_20240523160041_3851301.html
12.哪种人是软件设计中的稀缺型人才?有扩展性需求的时候 可扩展设计,主要是利用了面向对象的多态特性,所以这里的接口是一个广义的概念,如果用编程语言的术语来说,它既可以是Interface,也可能是Abstract Class。 这种扩展性的诉求在软件工作中可以说无处不在,小到一个工具类。例如,我现在系统中需要一个开关的功能,开关的配置目前是用数据库做配置的,但https://www.51cto.com/article/605578.html
13.幼儿园教案:《找朋友》(通用15篇)3、 我们要多吃ji蛋,ji蛋营养好。我们到教室里品尝一下ji蛋的美味吧! 设计意图: 1、根据主题《我们爱吃的食物》教师预设了这样一个活动。通过活动扩大幼儿的认知经验和发展幼儿的观察能力。 2、在活动中环节中设计了区分归类这一内容,初步融入粗浅的数学内容,做到学科的整和。 http://mip.yuwenmi.com/jiaoan/youeryuan/3800569.html
14.扩展语句范文8篇(全文)值得注意的是,扩展的要求一定要体现思维和语言的训练,换一句话说学生思维和语言的训练是通过扩展要求的引导来完成的。所以在设计扩展要求时一定要体现层进性,让学生的思维在比较中逐渐走向深入,让学生在反复运用中逐渐熟悉各种表达方式、表现手法的特征与作用。https://www.99xueshu.com/w/ikeych2madyk.html
15.简单的教学设计思路写法扩展资料: 教学设计思路的原则: 1、系统性原则:教学设计是一项系统工程,它是由教学目标和教学对象的分析、教学内容和方法的选择以及教学评估等子系统所组成,各子系统既相对独立,又相互依存、相互制约,组成一个有机的整体。在诸子系统中,各子系统的功能并不等价,其中教学目标起指导其他子系统的作用。 https://www.oh100.com/kaoshi/jiaoxuesheji/396809.html
16.FEMAPv12注册破解版专业有限元分析软件SiemensFE新的解决方案功能包括支持用于NX Nastran多端非线性分析和拓扑优化的端口,具有扩展的设计优化功能。新版本的特色是交互式控制和更清晰的模型查看,改进的工作流程和增强的建模工具以及扩展的仿真应用范围新的解决方案,功能包括支持NX Nastran多步非线性分析以及具有扩展设计优化功能的拓扑优化。Femap版本12针对几何创建和操作http://www.sd173.com/soft/7647.html