手动实现简易版RPC(下)

手动实现简易版RPC(下)

前言

什么是RPC?它的原理是什么?它有什么特点?如果让你实现一个RPC框架,你会如何是实现?带着这些问题,开始今天的学习。

接上一篇博客 手动实现简易版RPC(上) 我们得到了最简易RPC框架的架构图与运行图,本文主要介绍简易版RPC 的简易实现。


架构

最简易的 RPC 框架架构图:

RPC框架

整个简易RPC的调用过程图

RPC的调用过程图

下面我们白手起家,从0实现简易版的RPC框架

项目准备

1、创建项目

优先创建一个名为ape-rpc的空项目,然后使用idea 依次创建几个maven模块

在这里插入图片描述

整个项目包结构也如上图所示,分别介绍一下上述的包结构

  • example-common 整个项目中所用到的公共类,例如一些实体类,公共接口等
  • example-producer 示例服务生产者代码包
  • example-consumer 示例服务消费者代码包
  • ape-rpc-easy 简易RPC框架实现代码包

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

2、common模块代码实现

公共模块需要同时被服务消费者服务提供者引入,主要是编写和服务相关的接口和数据实体

common模块结构如下图

在这里插入图片描述

2.1 引入lombok 简化开发
  	<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.24</version></dependency>
2.2 编写🐱实体
package com.jerry.common.model;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 15:33* @注释 实体 🐱*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Cat implements Serializable {private int id;/**** 名字*/private String name;/**** 颜色*/private String color;
}

注意,对象需要实现序列化接口,为后续网络传输序列化提供支持

2.3编写猫咪服务接口

编写猫咪服务接口CatService,提供两个获取猫咪的方法

package com.jerry.common.service;import com.jerry.common.model.Cat;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 15:38* @注释 🐱服务接口*/public interface CatService {/**** 获取猫咪信息* @param cat* @return*/Cat getCat(Cat cat);/**** 按照id获取猫咪信息* @param id* @return*/Cat getCatById(int id);//....other
}

3、服务生产者(producer)

作为服务生产者,是真正实现服务接口的模块,需要调用简单RPC模块

3.1添加pom依赖
 <dependencies><!--common--><dependency><groupId>com.jerry</groupId><artifactId>example-common</artifactId><version>1.0-SNAPSHOT</version></dependency><!--rpc-easy--><dependency><groupId>com.jerry</groupId><artifactId>ape-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.24</version></dependency></dependencies>
3.2 编写服务实现类

编写代码实现类,实现公共模块中定义的🐱服务接口

功能是打印🐱的信息

package com.jerry.producer.serviceImpl;import com.jerry.common.model.Cat;
import com.jerry.common.service.CatService;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 15:45* @注释 数据提供者实现*/
public class CatServiceImpl implements CatService {@Overridepublic Cat getCat(Cat cat) {System.out.println("调用到了服务提供者:" + cat.toString());return cat;}@Overridepublic Cat getCatById(int id) {Cat tom = new Cat(id, "TOM", "#FFFFFF");System.out.println("调用到了服务提供者:" + tom.toString());return tom;}
}
3.3实现生产者主类

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

package com.jerry.producer;import com.jerry.aperpc.localregcenter.LocalRegCenter;
import com.jerry.aperpc.server.VertxHttpServer;
import com.jerry.common.service.CatService;
import com.jerry.producer.serviceImpl.CatServiceImpl;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 15:47* @注释 简单的生产者*/
public class EasyProducer {public static void main(String[] args) {//提供服务....}
}

完成后代码生产者的结构大概如下图所示

在这里插入图片描述

4、服务消费者(Consumer)

服务消费者需要调用简单rpc实现模块

4.1pom依赖配置
 <dependencies><dependency><groupId>com.jerry</groupId><artifactId>example-common</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.jerry</groupId><artifactId>ape-rpc-easy</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.24</version></dependency></dependencies>
4.2创建服务消费者启动类

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

package com.jerry.consumer;import com.jerry.aperpc.proxy.ServiceProxyFactory;
import com.jerry.common.model.Cat;
import com.jerry.common.service.CatService;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 17:14* @注释*/
public class EasyConsumer {public static void main(String[] args) {//todo: 需要获取 CatService 的实现类对象CatService catService = null;//调用Cat newCat = catService.getCatById(1);if (newCat != null) {System.out.println("消费者获取到的 cat :" + newCat.toString());} else {System.out.println("no cat");}}}

值得注意的是:现在是无法获取到 CatService实例的,所以预留为 null。我们之后的目标是,能够通过 RPC 框架快速得到一个支持远程调用服务提供者的代理对象,像调用本地方法一样调用 CatService的方法。

5、简单rpc业务实现

5.1 web服务器

我们要先让服务提供者提供可远程访问的服务。那么,就需要一个 web 服务器,能够接受处理请求、并返回响应。
web 服务器的选择有很多,比如 Spring Boot 内嵌的 Tomcat、NIO 框架 Netty 和 Vert.x等等

此处我们使用高性能的 NIO 框架 Vert.x 来作为 RPC 框架的 web 服务器。

想了解更多,请参考 Vert.x官方文档🌐 || Vert.x官方文档中文版🌐

5.1.1 rpc-easy引入pom依赖
<dependencies><!--        高性能的 NIO 框架 Vert.x 来作为 RPC 框架的 web 服务器。--><dependency><groupId>io.vertx</groupId><artifactId>vertx-core</artifactId><version>4.5.1</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.24</version></dependency></dependencies>
5.1.2编写httpServer做web服务器接口

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

package com.jerry.aperpc.server;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 16:03* @注释*/
public interface HttpServer {/**** 启动器* @param port*/void exec(int port);
}
5.1.3基于vert.x实现请求处理

编写基于 Vert.x 实现的 web 服务器 VertxHttpServer,能够监听指定端口并处理请求。

package com.jerry.aperpc.server;import io.vertx.core.Vertx;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 16:06* @注释*/
public class VertxHttpServer implements HttpServer {@Overridepublic void exec(int port) {//1-创建Vertx 实例Vertx vertx = Vertx.vertx();//2-创建http服务器io.vertx.core.http.HttpServer httpServer = vertx.createHttpServer();//3-监听端口并处理请求httpServer.requestHandler(httpServerRequest -> {//处理里HTTP 请求System.out.println("Received request :"+ httpServerRequest.method() + " " +httpServerRequest.uri());//发送http响应httpServerRequest.response().putHeader("content-type", "text/plain").end("Hello from Vert.x HTTP server!");});//4-启动 HTTP 服务器并监听指定端口httpServer.listen(port, result -> {if (result.succeeded()) {System.out.println("Server is now listening on port:" + port);} elseSystem.err.println("Failed to start server:" + result.cause());});}
}
5.1.4验证web服务器

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

修改服务提供者(example-producer)模块的 EasyProducer类,编写启动 web 服务的代码,如下:

public class EasyProducer {public static void main(String[] args) {//提供服务VertxHttpServer vertxHttpServer = new VertxHttpServer();vertxHttpServer.exec(8080);}
}

浏览器访问localhost:8080,查看能否正常访问并看到输出的文字。

如果能够正常访问,浏览器窗口以及控制台输出如下图所示

在这里插入图片描述

在这里插入图片描述

5.2 本地服务注册器

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

在 rpc-easy 模块中新建文件夹 localregcenter ,创建本地服务注册器 LocalRegCenter,使用线程安全的 ConcurrentHashMap 存储服务注册信息,key 为服务名称、value 为服务的实现类。之后就可以根据要调用的服务名称获取到对应的实现类,然后通过反射进行方法调用了。

具体代码如下:

package com.jerry.aperpc.localregcenter;import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 16:23* @注释 本地服务注册器*/
public class LocalRegCenter {/**** 本地注册中心存储列表*/private static Map<String, Class<?>> map = new ConcurrentHashMap<String, Class<?>>();/**** 注册* @param serviceName* @param impl*/public static void add(String serviceName, Class<?> impl) {map.put(serviceName, impl);}/**** 获取* @param serviceName* @return*/public static Class<?> get(String serviceName) {return map.get(serviceName);}/**** 移除* @param serviceName*/public static void remove(String serviceName) {map.remove(serviceName);}}

注意:本地服务注册器和注册中心的作用是有区别的。

注册中心的作用侧重于管理注册的服务、提供服务信息给消费者;

而本地服务注册器的作用是根据服务名获取到对应的实现类,是完成调用必不可少的模块。

当服务提供者(example-producer)启动时,需要将自己注册服务到注册器中,修改 EasyProducer代码如下

package com.jerry.producer;import com.jerry.aperpc.localregcenter.LocalRegCenter;
import com.jerry.aperpc.server.VertxHttpServer;
import com.jerry.common.service.CatService;
import com.jerry.producer.serviceImpl.CatServiceImpl;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 15:47* @注释 简单的生产者*/
public class EasyProducer {public static void main(String[] args) {//注册服务LocalRegCenter.add(CatService.class.getName(), CatServiceImpl.class);//提供服务VertxHttpServer vertxHttpServer = new VertxHttpServer();vertxHttpServer.exec(8080);}
}
5.3 序列化器

服务在本地注册后,我们就可以根据请求信息取出实现类并通过反射技术调用实现类的方法了。

在编写处理请求的逻辑前,我们要先实现序列化器模块。

5.3.1 什么是序列化

无论是请求或响应,都会涉及参数的传输。而 Java对象是存活在JVM 虚拟机中的,如果想在其他位置存储并访问、或者在网络中进行传输,就需要进行序列化和反席列化。

简单理解:

**序列化:**将数据结构或对象转换成二进制字节流的过程

**反序列化:**将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

5.3.2 序列化处理要素

序列化的处理要素

  • **解析效率:**序列化协议应该首要考虑的因素,像xml/json解析起来比较耗时,需要解析doom树,二进制自定义协议解析起来效率要快很多。
  • **压缩率:**同样一个对象,xml/json传输起来有大量的标签冗余信息,信息有效性低,二进制自定义协议占用的空间相对来说会小很多。
  • **扩展性与兼容性:**是否能够利于信息的扩展,并且增加字段后旧版客户端是否需要强制升级,这都是需要考虑的问题,在自定义二进制协议时候,要做好充分考虑设计。
  • **可读性与可调试性:**xml/json的可读性会比二进制协议好很多,并且通过网络抓包是可以直接读取,二进制则需要反序列化才能查看其内容
  • **跨语言:**有些序列化协议是与开发语言紧密相关的,例如dubbo的Hessian序列化协议就只能支持Java的RPC调用。
  • **通用性:**xml/json非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,二进制数据的处理方面也有Protobu和和Hessian等插件,在做设计的时候尽量做到较好的通用性。
5.3.3 序列化器
  1. JDK原生序列化,通过实现Serializable接口。通过ObjectOutPutSream和ObjectlnputStream对象进行序列化及反序列化.
  2. JSON序列化。一般在HTTP协议的RPC框架通信中,会选择JSON方式。JSON具有较好的扩展性、可读性和通用性。但JSON序列化占用空间开销较大,没有JAVA的强类型区分,需要通过反射解决,解析效率和压缩率都较差。如果对并发和性能要求较高,或者是传输数据量较大的场景,不建议采用JSON序列化方式。
  3. Hessian2序列化。Hessian 是一个动态类型,二进制序列化,并且支持跨语言特性的序列化框架。Hessian 性能上要比JDK、JSON 序列化高效很多,并且生成的字节数也更小。有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。
  4. kryo序列化。高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积
  5. Protobuf序列化。Protobuf 出自于 Google,性能还比较优秀,也支持多种语言,同时还是跨平台的。就是在使用中过于繁琐,因为你需要自己定义 IDL 文件和生成对应的序列化代码。这样虽然不灵活,但是,另一方面导致 protobuf 没有序列化漏洞的风险。
  6. 其他序列化方式

在此我们实现简单的kryo序列化

  • 首先在rpc模块pom中引入kryo系列化依赖
 <dependency><groupId>com.esotericsoftware</groupId><artifactId>kryo</artifactId><version>4.0.1</version></dependency>

由于其底层依赖于ASM技术,与Spring等框架可能会发生ASM依赖的版本冲突(文档中表示这个冲突还挺容易出现)所以提供了另外一个依赖以供解决此问题

<dependency><groupId>com.esotericsoftware</groupId><artifactId>kryo-shaded</artifactId><version>4.0.1</version></dependency>

同时引入异常管理依赖

  <dependency><groupId>com.nimbusds</groupId><artifactId>oauth2-oidc-sdk</artifactId><version>8.36</version></dependency>
  • 然后在rpc模块新建序列化接口Serializer,提供序列化和反序列化两个接口,方便后续拓展
package com.jerry.aperpc.serializer;import java.io.IOException;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 16:33* @注释*/
public interface Serializer {/**** 序列化* @param object* @return* @param <T>* @throws IOException*/<T> byte[] serialize(T object) throws IOException;/**** 反序列化器* @param bytes* @param type* @return* @param <T>* @throws IOException*/<T> T deserialize(byte[]bytes, Class<T> type) throws IOException;
}
  • 基于kryo实现KryoSerializer
package com.jerry.aperpc.serializer;import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.jerry.aperpc.model.RpcRequest;
import com.jerry.aperpc.model.RpcResponse;
import com.nimbusds.oauth2.sdk.SerializeException;import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;/*** @version 1.0* @Author jerryLau* @Date 2024/4/11 9:08* @注释 基于kryo的序列化器*/
public class KryoSerializer implements Serializer {/*** Because Kryo is not thread safe. So, use ThreadLocal to store Kryo objects*/private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {Kryo kryo = new Kryo();kryo.register(RpcResponse.class);kryo.register(RpcRequest.class);return kryo;});@Overridepublic byte[] serialize(Object obj) {try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();Output output = new Output(byteArrayOutputStream)) {Kryo kryo = kryoThreadLocal.get();// Object->byte:将对象序列化为byte数组kryo.writeObject(output, obj);kryoThreadLocal.remove();return output.toBytes();} catch (Exception e) {e.printStackTrace();throw new SerializeException("Serialization failed");}}@Overridepublic <T> T deserialize(byte[] bytes, Class<T> clazz) {try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);Input input = new Input(byteArrayInputStream)) {Kryo kryo = kryoThreadLocal.get();// byte->Object:从byte数组中反序列化出对象Object o = kryo.readObject(input, clazz);kryoThreadLocal.remove();return clazz.cast(o);} catch (Exception e) {e.printStackTrace();throw new SerializeException("Deserialization failed");}}}

至此序列化器完成

5.4 请求处理器 (生产者处理调用)

请求处理器是RPC框架的核心,其主要功能就是:

根据接收到的请求,并根据请求参数找到对应的服务和方法,通过反射实现调用,最后封装返回结果并响应请求。

5.4.1 rpc模块请求响应实体封装

rpc模块中进行请求以及响应的实体封装

rpc请求体(RpcRequest)

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

package com.jerry.aperpc.model;import lombok.*;import java.io.Serializable;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 16:43* @注释 rpc 请求封装*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class RpcRequest implements Serializable {/***服务名称*/private String serviceName;/****方法名称*/private String methodName;/***参数类型列表*/private Class<?> [] parameterTypes;/****参数列表*/private Object[] args;
}

rpc响应体(RpcResponse)

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

package com.jerry.aperpc.model;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 16:47* @注释 rpc 响应封装*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class RpcResponse implements Serializable {/****响应数据*/private Object data;/****响应数据类型(预留)*/private Class<?> dataType;/****响应信息*/private String message;/**** 异常信息*/private Exception exception;
}
5.4.2 业务请求处理器

作为业务请求处理器,他有如下几个职责:

1.反序列化请求为对象,并从请求对象中获取参数
2.根据服务名称从本地注册器中获取到对应的服务实现类
3.通过反射机制调用服务实现类中的方法,得到返回结果
4.对返回结果进行封装和序列化,并写入到响应中

完整代码如下

package com.jerry.aperpc.server;import com.jerry.aperpc.localregcenter.LocalRegCenter;
import com.jerry.aperpc.model.RpcRequest;
import com.jerry.aperpc.model.RpcResponse;
import com.jerry.aperpc.serializer.JDKSerializer;
import com.jerry.aperpc.serializer.KryoSerializer;
import com.jerry.aperpc.serializer.Serializer;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;import java.io.IOException;
import java.lang.reflect.Method;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 16:49* @注释 请求处理器* 1.反序列化请求为对象,并从请求对象中获取参数。* 2.根据服务名称从本地注册器中获取到对应的服务实现类,* 3.通过反射机制调用方法,得到返回结果。* 4.对返回结果进行封装和序列化,并写入到响应中,*/
public class HttpServerHandler implements Handler<HttpServerRequest> {@Overridepublic void handle(HttpServerRequest request) {//1-指定序列化器final Serializer serializer = new KryoSerializer();//打印请求信息日志System.out.println("Received request :" +"request.uri: ["+ request.uri() + "] request.method : ["+request.method()+"]");//2-异步进行请求request.bodyHandler(body -> {byte[] bytes = body.getBytes();RpcRequest rpcRequest = null;try {rpcRequest = serializer.deserialize(bytes, RpcRequest.class);//反序列化请求为对象} catch (Exception e) {e.printStackTrace();}// 构造响应结果对象RpcResponse rpcResponse = new RpcResponse();//如果请求为 nu11,直接返回if (rpcRequest == null) {rpcResponse.setMessage("rpcRequest is null");doResponse(request, rpcResponse, serializer);return;}try {// 获取要调用的服务实现类,通过反射调用Class<?> implClass = LocalRegCenter.get(rpcRequest.getServiceName());//获取实现类Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());//反射获取实现类的方法Object result = method.invoke(implClass.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);});}/*** 响应** @param request* @param rpcResponse* @param serializer*/void doResponse(HttpServerRequest request, RpcResponse rpcResponse, Serializer serializer) {HttpServerResponse httpServerResponse = request.response().putHeader("content-type", "application/json");try {// 序列化byte[] serialized = serializer.serialize(rpcResponse);httpServerResponse.end(Buffer.buffer(serialized));} catch (IOException e) {e.printStackTrace();httpServerResponse.end(Buffer.buffer());}}
}

简单解释一下上述部分代码:

  1. 获取实现类:
Class<?> implClass = LocalRegCenter.get(rpcRequest.getServiceName());

这行代码从LocalRegCenter(本地服务发现器)中,根据rpcRequest.getServiceName()提供的服务名,获取了对应的实现类的Class对象。这个Class对象代表了实现类的元数据,包括它的方法、字段等信息。

  1. 获取实现类的方法:
Method method = implClass.getMethod(rpcRequest.getMethodName(),rpcRequest.getParameterTypes());

这行代码通过implClass.getMethod(...)方法,根据方法名和参数类型来获取实现类中的某个方法。其中:

  • rpcRequest.getMethodName()提供了方法名。
  • rpcRequest.getParameterTypes()提供了方法的参数类型。

getMethod方法返回的是一个Method对象,代表了这个方法。

  1. 创建实现类的实例:
implClass.newInstance()

通过newInstance方法,创建了实现类的一个新实例。这是Java反射中创建对象的一种方式(注意:从Java 9开始,newInstance方法已被标记为过时,建议使用getDeclaredConstructor().newInstance()来替代)。

  1. 调用实现类的方法:
Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs());

使用method.invoke(...)方法,调用上面获取到的Method对象所代表的方法。这里做了两件事:

  • 首先,通过implClass.newInstance()创建了一个实现类的新实例。
  • 然后,使用rpcRequest.getArgs()提供的参数来调用这个方法。

invoke方法返回的是方法的返回值,这里被存储在result变量中。

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

5.4.3 给 HttpServer 绑定请求处理器。

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

修改后的代码如下

package com.jerry.aperpc.server;import io.vertx.core.Vertx;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 16:06* @注释*/
public class VertxHttpServer implements HttpServer {@Overridepublic void exec(int port) {//1-创建Vertx 实例Vertx vertx = Vertx.vertx();//2-创建http服务器io.vertx.core.http.HttpServer httpServer = vertx.createHttpServer();//3-监听端口并处理请求
//        httpServer.requestHandler(httpServerRequest -> {
//            //处理里HTTP 请求
//            System.out.println("Received request :"
//                    + httpServerRequest.method() + " " +
//                    httpServerRequest.uri());
//
//            //发送http响应
//            httpServerRequest.response()
//                    .putHeader("content-type", "text/plain")
//                    .end("Hello from Vert.x HTTP server!");
//        });httpServer.requestHandler(new HttpServerHandler());//4-启动 HTTP 服务器并监听指定端口httpServer.listen(port, result -> {if (result.succeeded()) {System.out.println("Server is now listening on port:" + port);} elseSystem.err.println("Failed to start server:" + result.cause());});}
}

至此,引入了 RPC 框架的服务提供者模块,已经能够接受请求并完成服务调用了

5.5 代理 (消费者发起调用)

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

总不能把服务提供者的CatServicelmpl复制粘贴到消费者模块吧?要能那样做还需要 RPC 框架干什么?分布式系统中,我们调用其他项目或团队提供的接口时,一般只关注请求参数和响应结果,而不关注具体实现。

在之前的架构中讲过,我们可以通过生成代理对象来简化消费方的调用,代理的实现方式大致分为两类类:静态代理和动态代理,下面依次实现。

5.5.1 静态代理

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

🌰:在 example-consumer 模块中,创建一个静态代理 CatServiceProxy,实现 CatService接口和 getCatById 方法。

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

package com.jerry.consumer.proxy;import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.jerry.aperpc.model.RpcRequest;
import com.jerry.aperpc.model.RpcResponse;
import com.jerry.aperpc.serializer.JDKSerializer;
import com.jerry.aperpc.serializer.KryoSerializer;
import com.jerry.aperpc.serializer.Serializer;
import com.jerry.common.model.Cat;
import com.jerry.common.service.CatService;import java.io.IOException;/*** @version 1.0* @Author jerryLau* @Date 2024/4/9 17:14* @注释 🐱服务的静态代理*/public class CatServiceProxy implements CatService {@Overridepublic Cat getCat(Cat cat) {// 指定序列化器final Serializer serializer = new KryoSerializer();// 构造请求RpcRequest rpcRequest = RpcRequest.builder().serviceName(CatService.class.getName()).methodName("getCat").parameterTypes(new Class[]{Cat.class}).args(new Object[]{cat}).build();try {// 序列化(Java 对象 => 字节数组)byte[] bodyBytes = serializer.serialize(rpcRequest);// 发送请求try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080").body(bodyBytes).execute()) {byte[] result = httpResponse.bodyBytes();// 反序列化(字节数组 => Java 对象)RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);return (Cat) rpcResponse.getData();}} catch (IOException e) {e.printStackTrace();}return null;}@Overridepublic Cat getCatById(int id) {// 指定序列化器final Serializer serializer = new KryoSerializer();// 构造请求RpcRequest rpcRequest = RpcRequest.builder().serviceName(CatService.class.getName()).methodName("getCatById").parameterTypes(new Class[]{int.class}).args(new Object[]{id}).build();try {// 序列化(Java 对象 => 字节数组)byte[] bodyBytes = serializer.serialize(rpcRequest);// 发送请求try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080").body(bodyBytes).execute()) {byte[] result = httpResponse.bodyBytes();// 反序列化(字节数组 => Java 对象)RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);return (Cat) rpcResponse.getData();}} catch (IOException e) {e.printStackTrace();}return null;}
}

然后修改example-consumer 模块 EasyConsumer,new 一个代理对象并赋值给 CatService,就能完成调用:

package com.jerry.consumer;import com.jerry.aperpc.proxy.ServiceProxyFactory;
import com.jerry.common.model.Cat;
import com.jerry.common.service.CatService;
import com.jerry.consumer.proxy.CatServiceProxy;/*** @version 1.0* @Author jerryLau* @Date ${DATE} ${TIME}* @注释*/
public class EasyConsumer {public static void main(String[] args) {//todo: 需要获取 CatService 的实现类对象CatService catService = new CatServiceProxy();//调用Cat newCat = catService.getCatById(1);if (newCat != null) {System.out.println("消费者获取到的 cat :" + newCat.toString());} else {System.out.println("no cat");}}}

接下来我们尝试运行一下,优先启动生产者,然后启动消费者,正常的情况下,控制台会出现如下信息

在这里插入图片描述

在这里插入图片描述

静态代理虽然很好理解(就是写个实现类嘛),但缺点也很明显,我们如果要给每个服务接口都写一个实现类,是非常麻烦的,这种代理方式的灵活性也是比较差的。

因此我们尝试使用动态代理

5.5.2 动态代理

动态代理的作用是

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

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

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

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

几乎就是将静态代理的方式搬运过来

package com.jerry.aperpc.proxy;import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.jerry.aperpc.model.RpcRequest;
import com.jerry.aperpc.model.RpcResponse;
import com.jerry.aperpc.serializer.JDKSerializer;
import com.jerry.aperpc.serializer.KryoSerializer;
import com.jerry.aperpc.serializer.Serializer;import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;/*** @version 1.0* @Author jerryLau* @Date 2024/4/10 8:44* @注释 rpc 模块中的动态代理*/
public class ServiceProxy implements InvocationHandler {/**** 调用代理* @param proxy the proxy instance that the method was invoked on** @param method the {@code Method} instance corresponding to* the interface method invoked on the proxy instance.  The declaring* class of the {@code Method} object will be the interface that* the method was declared in, which may be a superinterface of the* proxy interface that the proxy class inherits the method through.** @param args an array of objects containing the values of the* arguments passed in the method invocation on the proxy instance,* or {@code null} if interface method takes no arguments.* Arguments of primitive types are wrapped in instances of the* appropriate primitive wrapper class, such as* {@code java.lang.Integer} or {@code java.lang.Boolean}.** @return* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("invoke current  method :" + method + "args: " + args);System.out.println("invoke current  method :" + method.getName() + "  args: " + args +"  parameterTypes:"+method.getParameterTypes());// 指定序列化器final Serializer serializer = new KryoSerializer();// 构造请求RpcRequest rpcRequest = RpcRequest.builder().serviceName(method.getDeclaringClass().getName()).methodName(method.getName()).parameterTypes(method.getParameterTypes()).args(args).build();try {// 序列化(Java 对象 => 字节数组)byte[] bodyBytes = serializer.serialize(rpcRequest);// 发送请求//todo:暂时设置成硬编码,后面改成服务发现try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080").body(bodyBytes).execute()) {byte[] result = httpResponse.bodyBytes();// 反序列化(字节数组 => Java 对象)RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);return rpcResponse.getData();}} catch (IOException e) {e.printStackTrace();}return null;}
}
  • 创建动态代理工厂 ServiceProxyFactory,作用是根据指定类创建动态代理对象

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

package com.jerry.aperpc.proxy;import java.lang.reflect.Proxy;/*** @version 1.0* @Author jerryLau* @Date 2024/4/10 8:50* @注释 动态代理工厂,通过指定类创建代理对象*/
public class ServiceProxyFactory {public static <T> T getProxy(Class<T> serviceClass) {return (T) Proxy.newProxyInstance(serviceClass.getClassLoader(),new Class[]{serviceClass}, new ServiceProxy());}}

上述代码中,主要是通过 Proxy.newProxyInstance 方法为指定类型创建代理对象

  • 最后将example-consumer模块中获取通过静态代理获取CatService,调整成调用动态代理工厂得到动态代理对象
package com.jerry.consumer;import com.jerry.aperpc.proxy.ServiceProxyFactory;
import com.jerry.common.model.Cat;
import com.jerry.common.service.CatService;
import com.jerry.consumer.proxy.CatServiceProxy;/*** @version 1.0* @Author jerryLau* @Date ${DATE} ${TIME}* @注释*/
public class EasyConsumer {public static void main(String[] args) {//todo: 需要获取 CatService 的实现类对象
//        CatService catService = new CatServiceProxy();CatService catService = ServiceProxyFactory.getProxy(CatService.class);//调用Cat newCat = catService.getCatById(1);if (newCat != null) {System.out.println("消费者获取到的 cat :" + newCat.toString());} else {System.out.println("no cat");}}}

运行结果与静态差不太多

在这里插入图片描述

在这里插入图片描述

简单解释下上述动态代理部分代码执行流程,稍后再测试环节客以debug看到他是如何运行的

当服务消费者中通过调用catService.getCatById时,实际上并没有直接调用 CatService 类的 getCatById 方法,而是调用了 通过动态代理工厂生成的代理对象的 getCatById 方法。这个代理对象内部持有 InvocationHandler 的引用(在这个例子中是 ServiceProxy 的实例),并将方法调用转发给 ServiceProxyinvoke 方法。

invoke 方法是如何拿到对应的方法进行执行的呢?这是通过反射机制实现的。当代理对象的方法被调用时,Proxy 类生成的代理对象会捕获这个调用,并获取被调用的方法(Method 对象)、调用该方法的代理对象本身(proxy 参数)以及传递给该方法的参数(args 参数)。然后,它将这些信息传递给 InvocationHandlerinvoke 方法。

invoke 方法内部,你可以通过 method 参数来获取被调用的方法对象,这个例子中做了系统的打印输出的同时,获取到了要调用的方法信息、传入的参数列表等,这不就是我们服务提供者需要的参数么?用这些参数来构造请求对象就可以完成调用了。

因此,当你通过代理对象调用方法时,实际上执行的是 InvocationHandlerinvoke 方法,而在这个方法中,你可以通过反射机制调用目标对象上的相应方法,并在调用前后添加你想要的额外逻辑。这种方式允许你在不修改目标对象代码的情况下,为目标对象添加一些额外的功能或行为。

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

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

6、简单测试

  • 以debug模式运行EasyProducer

在这里插入图片描述

  • 以debug模式运行EasyConsumer,在动态代理工厂ServiceProxy设置断点,可以看到调用 catService时,实际是调用了代理对象的 invoke 方法,并且获取到了 serviceName、methodName、参数类型和列表等信息。

在这里插入图片描述

在这里插入图片描述

  • 在服务提供者出请求处理出设置断点,可以看到接受并反序列化后的请求,跟发送时的内容一致

在这里插入图片描述

最后运行输出的结果就在不给大家演示了,参考上面5.5.2 动态代理的运行结果截图


至此,我们实现了简易版的PRC框架

码字不易,希望大家能够一键三连🌝⭐🌟


代码仓库 ape-rpc: 轮子项目,手动实现rpc github🌐 || ape-rpc: 轮子项目,手动实现rpc gitee🌐

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/807893.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【git】为什么git要有 暂存区

关于git暂存区的个人理解 暂存区 一般存放在 .git 目录下的 index 文件&#xff08;.git/index&#xff09;中。 git中的修改需要先add到暂存区&#xff0c;再commit到本地库&#xff0c;乍一看好像是多此一举了。 看了些别人的讨论&#xff0c;自己也想了很久&#xff0c;…

二叉树应用——最优二叉树(Huffman树)、贪心算法—— Huffman编码

1、外部带权外部路径长度、Huffman树 从图中可以看出&#xff0c;深度越浅的叶子结点权重越大&#xff0c;深度越深的叶子结点权重越小的话&#xff0c;得出的带权外部路径长度越小。 Huffman树就是使得外部带权路径最小的二叉树 2、如何构造Huffman树 &#xff08;1&#xf…

【每日刷题】Day10

【每日刷题】Day10 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f345; 目录 1. 环形链表的约瑟夫问题_牛客题霸_牛客网 (nowcoder.com) 2. 21. 合并两个有序链表 - 力扣&#xff08;LeetCode&#xff09; 3. 152…

灌醉阿里P8大佬!获取内部二进制网络安全学习路线(建议收藏

0x01 二进制学习路线 1.踏实的基础。 基础是很重要的&#xff0c;可以通过计算机体系结构来学习&#xff0c;当然肯定不只是计算机体系结构&#xff0c;还有很多的知识。计算机科学系统基础知识的积累和沉淀&#xff0c;提升自己的计算机科学素养&#xff0c;理解计算机的工作…

windows中anaconda下创建新的新的jupyter环境

https://blog.csdn.net/weixin_43491496/article/details/130325001?spm1001.2014.3001.5502 这里写目录标题 1.1界面化创建虚拟环境1.2命令行创建虚拟环境2.查看是否创建成功3.激活虚拟环境pylessonppt4.更改工作目录5.删除6.查看是否删除成功 1.1界面化创建虚拟环境 1.2命令…

润乾报表平台 InputServlet 任意文件上传漏洞复现

0x01 产品简介 润乾报表是一个纯JAVA的企业级报表工具,支持对J2EE系统的嵌入式部署,无缝集成。服务器端支持各种常见的操作系统,支持各种常见的关系数据库和各类J2 EE的应用服务器,客户端采用标准纯html方式展现,支持ie和netscape, 润乾报表是领先的企业级报表分析软件。…

CPI高于预期!比特币与美股“脱钩”,下挫后急拉盘!减半“护航”下,加密市场四月剧本如何走?

美国劳工部于昨&#xff08;10&#xff09;晚公布了最新的CPI&#xff08;消费者物价指数&#xff09;数据&#xff0c;显示美国3月CPI年增幅达3.5%&#xff0c;不只高于前月的年增3.2%&#xff0c;也高于市场预估的3.4%&#xff0c;表明通货膨胀依然顽固。 高于预期的CPI数据释…

uniapp小程序给指定的页面新增下拉刷新功能

需求:有些页面需要实时更新数据,但是又不能做实时刷新,所以给用户一个手动下拉刷新指定接口的功能 第一步:在pages.json给页面加"enablePullDownRefresh": true配置 第二步:在指定页面写onPullDownRefresh方法,和methods同级 onPullDownRefresh() {//加个定时器1秒…

辉芒微FMD之FT61EC2x

辉芒微的官网&#xff1a;辉芒微电子 FMD | 官方网站 (fremontmicro.com) 1、安装开发环境 一共有如下三款APP&#xff0c; 第一个是 FMDIDE&#xff1b; FMDIDE软件是支持全系列8位MCU的集成开发环境&#xff0c;集代码编辑、分析、编译、调试等功能于一身。 编译器支持C89…

融中财经专访 | 欧科云链:从跟随行业到引领行业

导读 THECAPITAL 新行业中的经验“老兵”。 本文4089字&#xff0c;约5.8分钟 作者 | 吕敬之 编辑 | 吾人 来源 | 融中财经 &#xff08;ID&#xff1a;thecapital&#xff09; 一个新兴行业从发展到成熟需要几个必要的推手&#xff1a;人才、产品、制度。 Web3.0&…

Github第一Star数的国产免费开源防火墙--雷池社区版初步体验

前言 近期准备搭建一个博客网站&#xff0c;用来存储工作室同学们的学习笔记。服务器准备直接放在公网上&#xff0c;方便大家随时随地的上传和浏览&#xff0c;为了防止网站被人日穿成为肉鸡&#xff0c;一些防御措施还是要部署的。 首先明确自己的需求&#xff1a; 零成本…

头歌-机器学习 第10次实验 逻辑回归

第1关&#xff1a;逻辑回归核心思想 任务描述 本关任务&#xff1a;根据本节课所学知识完成本关所设置的编程题。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 什么是逻辑回归&#xff1b; sigmoid函数。 什么是逻辑回归 当一看到“回归”这两个字&a…

企业出海--跨境时延测试(拉美篇)

随着全球化不断发展&#xff0c;中国企业也不断向海外拓展业务&#xff0c;开拓市场&#xff0c;增加收入来源&#xff0c;扩大自身品牌影响力。然而出海企业面临不同以往的困难和挑战&#xff0c;在其中不可避免面临的跨境网络时延问题&#xff0c;如何选择区域进行部署企业业…

石子合并(区间dp)-java

石子合并问题是经典的区间dp问题&#xff0c;我们需要枚举中间端点k的情况从而来推出dp数组的值。 文章目录 前言 一、石子合并问题 二、算法思路 1.问题思路 2.状态递推公式 二、代码如下 代码如下&#xff08;示例&#xff09;&#xff1a; 2.读入数据 3.代码运行结果如下&am…

yolov9直接调用zed相机实现三维测距(python)

yolov9直接调用zed相机实现三维测距&#xff08;python&#xff09; 1. 相关配置2. 相关代码2.1 相机设置2.2 测距模块2.2 实验结果 相关链接 此项目直接调用zed相机实现三维测距&#xff0c;无需标定&#xff0c;相关内容如下&#xff1a; 1. yolov4直接调用zed相机实现三维测…

LPRNet车牌识别模型训练及CCPD数据集预处理

LPRNet车牌识别模型训练及CCPD数据集预处理 1 LPRNet车牌识别模型训练 1.1 源码:LPRNet_Pytorch-master 源码官网:GitHub - sirius-ai/LPRNet_Pytorch: Pytorch Implementation For LPRNet, A High Performance And Lightweight License Plate Recognition Framework. 链…

Windows搭建Jellyfin影音服务结合内网穿透实现公网访问本地视频文件

文章目录 1. 前言2. Jellyfin服务网站搭建2.1. Jellyfin下载和安装2.2. Jellyfin网页测试 3.本地网页发布3.1 cpolar的安装和注册3.2 Cpolar云端设置3.3 Cpolar本地设置 4.公网访问测试5. 结语 1. 前言 随着移动智能设备的普及&#xff0c;各种各样的使用需求也被开发出来&…

【Linux】vim 编辑器

Linux 系统自带了 gedit 和 vi 编辑器&#xff0c;gedit 是图形化界面的操作&#xff0c;而 vi 由比较难用&#xff0c;所以建议安装 vim 编辑器&#xff0c;vim 是从 vi 发展出来的一个文本编辑器&#xff0c;相当于增强版的 vi &#xff0c;其代码补完、编译及错误跳转等功能…

【Unity】组件组合使用心得(单行可自动拓展Scroll View)

在这之前&#xff0c;一直是在使用Scroll View进行滑动内容设置&#xff0c;但设置的都是不明不白的&#xff0c;而且有的时候设置好了之后也不知道是为什么&#xff0c;总感觉哪里不对劲&#xff0c;而且好也不知道为什么好&#xff0c;可能是长时间在做管理上的内容&#xff…

【LeetCode热题100】189. 轮转数组(数组)

一.题目要求 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 二.题目难度 中等 三.输入样例 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: …