很多同学听到“开发框架”可能会有点胆怯,但其实开发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
当前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
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
3)最后,在EasyConsumerExample中,就可以通过调用工厂来为UserService获取动态代理对象了。
至此,简易版的RPC框架已经开发完成,下面我们进行测试。
1)以debug模式启动服务提供者,执行main方法:
2)以debug模式启动服务消费者,执行main方法。
在ServiceProxy代理类中添加断点,可以看到调用userService时,实际是调用了代理对象的invoke方法,并且获取到了serviceName、methodName、参数类型和列表等信息。
如下图:
3)继续debug,可以看到序列化后的请求对象,结构是字节数组:
4)在服务提供者模块的请求处理器中打断点,可以看到接受并反序列化后的请求,跟发送时的内容一致:
5)继续debug,可以看到在请求处理器中,通过反射成功调用了方法,并得到了返回的User对象。
6)最后,在服务提供者和消费者模块中都输出了用户名称,说明整个调用过程成功。
以上,就是本期教程。麻雀虽小,五脏俱全。大家一定要自己动手实现,印象才会更深刻。