RPC框架 (Simple)
-
定义:
远程过程调用,是一种计算机通信协议,允许程序在不同的计算机之间像本地调用一样进行通信和交互(允许一个程序像调用自己的方法一样调用另外一个程序,不需要了解数据的传输过程,网络通信细节等,能够快速开发分布式系统)
-
实现思路:
-
基本设计
-
假设需要调用的一方是消费者,被调用的一方(提供服务的一方)是分享者
-
消费者想调用分享者就需要分享者创建一个web服务,然后消费者通过请求客户端,发送HTTP或者其他协议这样去调用分享者提供的接口服务
-
问题:如果分享者提供了多个接口方法怎么办? 【是不是需要分享者每个接口和方法都单独写一个接口?消费者每个接口都写一段HTTP调用的逻辑?】
-
解决:当然不是,如果这样后端接口维护就太麻烦了,并且前端调用也非常的繁琐,接口多参数多,调用繁琐。【其实可以提供一个统一的服务调用接口,通过请求处理器根据客户端的请求参数进行不同的处理,调用不同的接口和方法(注册器模式)】
-
比如:消费者需要调用分享者的message服务的connect方法,那么就可以发送请求,参数为server=message, method=connect,然后请求处理器就会根据service的值从服务注册器里面找到对应的服务实现类,并通过java反射机制调用method的指定方法
如图所示
-
上面基本上的实现逻辑已经完成,但是对于消费者来说还是稍微有一点繁琐,还需要自己完成请求和响应,这里可以使用一个代理对象来帮助完成这些事情
如图所示
-
-
持续优化v1.0:
-
问题1:消费者如何知道分享者的调用地址?
-
解决:需要一个注册中心来存储分享者提供的地址,消费者要调用服务的时候,只需要从注册中心获取对应的地址即可完成调用操作
如图所示
-
问题2:多个分享者提供接口进行调用,消费者应该如何选择?
-
解决:通过给消费者增加负载均衡能力,指定不同算法来决定调用哪一个服务提供者,方法可以有:轮询、随机、根据性能动态调用等
如图所示
-
问题3:如果消费者在调用分享者的接口时调用出现了问题,比如网络拥堵,服务器宕机等问题,如何处理?
-
解决:给消费者增加一些容错机制,比如失败重试、降级调用其他接口等
如图所示
-
-
总结
想要开发一个完整的RPC框架还需要考虑很多的问题,比如消费者每次从注册中心拿取调用地址性能会不会受到影响?如何让整个框架更加的易于扩展等等
框架图
-
-
RPC(简易版)实践
实践项目中,以一个最简单的用户服务为例子,演示整个服务的调用过程
-
创建四个meaven项目
- example-common 存放公共依赖,接口,Model等
- example-consumer 消费者代码
- example-provider 分享者代码
- way-rpc-easy 简易rpc框架
-
各个项目模块的具体实现
-
公共模块 example-common
需要被消费者和分享者同时引入,负责编写服务相关的接口和数据模型
-
文件结构如下
-
User实体类
public class User implements Serializable { private static final long serialVersionUID = -936712004335434748L; // 01. 创建一个用户名 private String username; // 02. 创建一个setName方法 public void setName(String username) { this.username = username; } // 03. 创建一个获取name 的方法 直接返回当前类的属性 public String getUsername() { return username; } }
-
接口(提供一个获取用户的接口)
User getUser(User user);
-
-
服务提供者 example-provider (真正的实现了接口)
-
依赖文件引入
<dependency> <groupId>com.way</groupId> <artifactId>example-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.way</groupId> <artifactId>way-rpc-easy</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency>
-
实现公共模块中定义的获取用户的接口 (输出用户名称,返回参数中的用户对象)
public User getUser(User user) { System.out.println("用户名为:" + user.getUsername()); return user; }
-
初步编写服务提供者的类方法
public class EasyProviderExample { // main方法 后面提供供服务的代码在这里面实现 public static void main(String[] args) { } }
文件结构
-
-
消费者 (example-consummer) 需要调用服务提供者的方法
-
pom依赖文件的引入
<dependency> <groupId>com.way</groupId> <artifactId>way-rpc-easy</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.way</groupId> <artifactId>example-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency>
-
创建消费者启动类,编写调用接口的代码
public static void main(String[] args) { // 这里暂时置为空,后面使用rpc框架实现动态代理获取对象 UserService userService = null; User user = new User(); user.setName("way"); User result = userService.getUser(user); if(result != null) { // 输出用户名 sout(result.getName()); }else { sout("user == null"); } }
文件目录
-
-
-
web服务器
需要实现提供者提供的服务是可以远程访问的,那么就需要一个web服务器,能够接受处理请求、并返回响应,这里选择使用的是Vert.x来实现rpc框架的web服务器
-
引入依赖
<!--https://vertx.io/docs/vertx-core/java/--> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-core</artifactId> <version>4.5.1</version> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency>
-
编写一个接口,定义统一启动服务器的方法
void doStart(int port);
-
编写VertxHttpServer,能够监听指定端口并处理客户端的请求
public class VertxHttpServer implements HttpServer{ public void doStart(int port) { // 01. 创建vertx的实例 Vertx vertx = Vertx.vertx(); // 02. 创建http服务器 io.vertx.core.http.HttpServer server = vertx.createHttpServer(); // 03. 处理请求 server.requestHandler(request -> { // 处理请求 sout("收到请求" + request.method() + " " + request.uri()); // 发送HTTP响应 request.response() .putHeader("content-type", "text/plain") .end("I'm from Vertx http server!"); }); // 04. 启动http服务器并监听指定端口 server.listen(port, result -> { if(result.succeeded()) { sout("server is running on port " + port); }else { sout("failed to running server" + result.cause()); } }); } }
rpc文件目录
-
验证web服务器能否启动成功并接收客户端的请求 (修改提供者的EasyProviderExample类)
... main() { // 启动web服务 HttpServer httpServer = new VertxHttpServer(); httpServer.doStart(8080); }
小结:
到目前为止我们实现了公共模块,服务分享者,消费者,web服务器 还有以下模块没有实现:服务注册器、序列化与反序列化、客户端请求代理(动态代理)提供者的请求处理器
如图所示
-
-
本地服务注册器 (只需要直接把服务注册到本地即可,不需要第三方的注册中心)
-
创建注册器模块 (在rpc模块中新建一个文件registry/LocalRegistry编写注册器模块)
// 本地注册服务是根据服务名获取到对应的实现类 public class LocalRegistry { /** * 注册信息存储 使用线程安全的ConcurrentHashMap村粗,key为服务名称,value为服务实现类 */ private static final Map<String, Class<?>> map = new ConcurrentHashMap<>(); /** * 注册服务 */ public static void register(String serviceName, Class<?> impClass) { map.put(serviceName, impClass); } /** * 获取服务 */ public static Class<?> get(String serviceName) { return map.get(serviceName); } /** * 删除服务 */ public static void remove(String serviceName) { map.remove(serviceName); } }
-
在分享者启动的文件类里面取注册服务到注册器中,修改源代码EasyProviderExample
... // 注册服务 LocalRegistry.register(UserService.class.getName(), UserServiceImpl.class); // 启动web服务 HttpServer httpServer = new VertxHttpServer(); httpServer.doStart(8080);
-
-
序列化器
-
服务注册到本地之后,在实现分享者的请求处理逻辑之前,我们还需要先完成序列化模块;参数传递的过程中需要进行序列化和反序列化。
-
序列化:将java对象转为字节数组 反序列化: 将字节数组转为java对象
-
这里我们使用java原生的序列化器,不需要记住代码,直接copy就是
-
在rpc模块中创建一个序列化接口,然后提供两个方法即可
# 接口代码 public interface Serializer { /** * 序列化 * @param object * @return * @param <T> * @throws IOException */ <T> byte[] serializer(T object) throws IOException; /** * 反序列化 * @param bytes * @param type * @return * @param <T> * @throws IOException */ <T> T unserializer(byte[] bytes, Class<T> type) throws IOException; } # 方法 public class SerializerImpl implements Serializer { /** * 序列化 输入 * @param object * @return * @param <T> * @throws IOException */ @Override public <T> byte[] serializer(T object) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(object); objectOutputStream.close(); return outputStream.toByteArray(); } @Override public <T> T unserializer(byte[] bytes, Class<T> type) throws IOException { ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); try { return (T) objectInputStream.readObject(); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } finally { objectInputStream.close(); } } }
-
-
分享者的请求处理器
请求处理器是rpc框架的关键,处理接收到的请求,根据参数寻找对应的服务和方法,通过反射实现调用,最后封装返回结果并响应请求
-
编写请求和响应的封装类(rpc模块中创建文件夹model/RpcRequest + RpcResponse)
# RpcRequest /** * 封装调用所需要的信息: 服务名称、方法名称、调用参数的类型列表、参数列表 * java反射机制所需要的参数 */ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class RpcRequest implements Serializable { private static final long serialVersionUID = 2919771999544492378L; private String serviceName; private String methodName; /** * 参数类型列表 */ private Class<?>[] parameterTypes; /** * 参数列表 */ private Object[] args; } # RpcResponse /** * 封装调用方法得到的返回值、调用信息等 */ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class RpcResponse { /** * 响应数据 */ private Object data; /** * 响应数据类型 */ private Class<?> dataType; /** * 响应信息 */ private String message; /** * 响应的异常信息 */ private Exception exception; }
-
编写请求处理器HttpServerHandler
/** * 业务流程: * 1. 反序列化请求对象(字节转为java对象),并获取请求参数 * 2. 根据服务名称供本地注册器中获取到对应的服务实现类 * 3. 通过反射机制调用方法,得到返回结果 * 4. 对返回结果进行封装和序列化,写入到响应中 (java对象转字节数组) * */ public class HttpServerHandler implements Handler<HttpServerRequest> { @Override public void handle(HttpServerRequest request) { // 指定序列化器 final Serializer serializer = new SerializerImpl(); // 记录日志 System.out.println("Received request:" + request.method() + " " + request.uri()); // 异步处理HTTP请求 request.bodyHandler(body -> { byte[] bytes = body.getBytes(); RpcRequest rpcRequest = null; try { rpcRequest = serializer.unserializer(bytes, RpcRequest.class); } catch (IOException e) { e.printStackTrace(); } // 构造响应结果对象 RpcResponse rpcResponse = new RpcResponse(); // 如果请求为null 直接返回 if(rpcRequest == null) { rpcResponse.setMessage("rpcRequest is null"); // 调用下面的响应方法 doResponse(request, rpcResponse, serializer); return; } try { // 通过反射调用服务实现类 Class<?> clazz = LocalRegistry.get(rpcRequest.getServiceName()); Method method = clazz.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes()); Object result = method.invoke(clazz.newInstance(), rpcRequest.getArgs()); // 封装返回结果 rpcResponse.setData(result); rpcResponse.setDataType(method.getReturnType()); rpcResponse.setMessage("ok"); } catch (Exception e) { e.printStackTrace(); rpcResponse.setMessage(e.getMessage()); rpcResponse.setException(e); } // 响应 doResponse(request, rpcResponse, serializer); }); } /** * 响应的方法 */ void doResponse(HttpServerRequest request, RpcResponse rpcResponse, Serializer serializer) { HttpServerResponse httpServerResponse = request.response().putHeader("content-type", "application/json"); try { // 序列化 byte[] serializer1 = serializer.serializer(rpcResponse); httpServerResponse.end(Buffer.buffer(serializer1)); } catch (IOException e) { // 在命令行打印异常信息在程序中出错的位置及原因 e.printStackTrace(); httpServerResponse.end(Buffer.buffer()); } } }
-
给HttpServer绑定请求处理器
# 修改rpc框架中的vertxHttpserverImpl的代码,通过server.requestHandler绑定请求处理器 public class VertxHttpServerImpl implements HttpServer { public void doStart(int port) { // 01. 创建vertx实例 Vertx vertx = Vertx.vertx(); // 02. 创建http服务器 io.vertx.core.http.HttpServer httpServer = vertx.createHttpServer(); // 更改处理请求的代码位置 // 03. 监听端口并处理请求 引入了rpc框架服务提供者的模块能够接收请求并且完成服务调用 httpServer.requestHandler(new HttpServerHandler()); // 04. 启动http服务器并监听指定端口 httpServer.listen(port, result -> { if(result.succeeded()) { System.out.println("server is now listening on port" + port); }else { System.out.println("Failed to start server" + result.cause()); } }); } }
小节:
支持,引入了rpc框架的服务分享者已经能够接收请求并完成服务的调用,剩下需要做的工作就是完成消费者这边的请求代理即可
-
-
消费者(请求代理)
我们在前面已经预留了一段调用服务的代码,只要能够获取到Userservice对象即可实现整个流程,下面就通过生成动态代理对象来简化消费方的调用
消费方之前的代码
-
动态代理实现
作用:根据要生成的对象的类型,自动胜场一个代理对象
方式:JDK动态代理(简单易用,只能对接口进行代理) CGLIB动态代理(能对任何类进行代理)
# 在rpc模块中编写动态代理类ServiceProxy实现invoke方法 public class ServiceProxy implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 指定序列化器 SerializerImpl serializer = new SerializerImpl(); // 构造请求 RpcRequest rpcRequest = RpcRequest.builder() .serviceName(method.getDeclaringClass() .getName()).methodName(method.getName()) .parameterTypes(method.getParameterTypes()) .args(args).build(); try { // 序列化 byte[] bodyBytes = serializer.serializer(rpcRequest); // 发送请求 // TODO: 硬编码需要注册中心和服务中心解决 try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080").body(bodyBytes).execute()) { byte[] result = httpResponse.bodyBytes(); // 反序列化 RpcResponse unserializer = serializer.unserializer(result, RpcResponse.class); return unserializer.getData(); } }catch (IOException e) { e.printStackTrace(); } return null; } }
-
创建动态代理工厂ServiceProxyFactory,作用是根据指定类创建动态代理对象
使用了工厂设计模式,简化对象的创建过程
public class ServiceProxyFactory { /** * 根据服务类创建代理对象 * @param serviceClass * @return * @param <T> */ public static <T> T getProxy(Class<T> serviceClass) { return (T) Proxy.newProxyInstance( serviceClass.getClassLoader(), new Class[]{serviceClass}, new ServiceProxy()); } }
-
最后在消费者启动类里面将获取UserService实例的位置使用工厂模式.动态代理方法获取到userService实例
UserService userService = ServiceProxyFactory.getProxy(UserService.class);
-
-
总结:
调试:分别启动分享者和消费者的main方法,如果能够在控制台看到分别输出了user的值就说明整个调用过程成功
感谢鱼皮大佬的教程,大家有需要可以上b站搜索程序员鱼皮进行学习…
-