一、前言
本篇文章主要是用Java和Vert.x设计一个简易版本的RPC框架
代码仓库:https://gitee.com/not-speaking-666/hpq_rpc.git
什么是RPC框架
RPC(Remote Procedure Call),即远程过程调用,是一种远程通信协议,允许程序在不同计算机之间进行通讯和交互的机制或方法,可以通过网络从远程计算机程序上请求服务,而客户端不需要了解底层网络技术的细节。RPC框架的主要目的是让远程服务调用看起来像是本地调用一样简单和透明。
RPC的主要特点:
- 透明性:RPC 提供了一种透明的方式来进行远程调用,使客户端能够像调用本地函数一样调用远程服务。
- 客户机/服务器模式:RPC 通常基于客户机/服务器架构,其中客户端发起调用,服务器端执行实际的服务逻辑。
- 请求/响应模式:客户端向服务器发送请求,服务器处理请求后返回响应。
- 序列化:为了在网络上传输数据,RPC 需要将数据结构或对象状态转换为可以传输的形式,这通常涉及序列化和反序列化过程。
- 网络通信:RPC 可以使用多种底层网络协议,如 TCP 或 Http进行数据传输。
为什么要用RPC?
在系统开发过程中随着系统用户量和规模不断扩大,简单的单机系统已经无法处理高并发的服务请求,此时我们采用分布式的架构使用多台服务器负责不同模块,以此来减小每台服务器的压力,但随着业务不断增加,不同功能模块之间也存在着调用关系,此时由于RPC允许在不同计算机之间进行通讯和交互,使开发者像调用本地方法一样调用远程服务,隐藏了网络通信的复杂性,这样就实现了不同模块之间相互通信,大大简化了分布式系统的开发。 同时许多RPC框架支持跨语言调用,这使得不同语言编写的系统组件也可以无缝集成。通过将服务接口和实现分离,RPC框架使得代码更加模块化,便于维护和升级,以及不同模块功能的增加和拓展。
常用的RPC框架
- Thrift:由Facebook开发的跨语言服务开发框架,结合了强大的软件堆栈和代码生成引擎,支持多种编程语言。
- Dubbo:阿里巴巴开源的分布式服务框架,提供了服务动态寻址与路由、软负载均衡与容错、依赖分析与降级等功能。
- Spring Cloud:由众多子项目组成,提供了搭建分布式系统及微服务常用的工具,如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性token、全局锁、选主、分布式会话和集群状态等。
- gRPC:由Google开发的远程过程调用系统,基于HTTP 2.0协议,支持多种编程语言。
RPC框架实现原理
简易的RPC调用流程:
在框架中主要有两个角色:消费者和提供者,消费者想得到相应服务或接口需要向提供者发送请求,提供者接收到请求后为消费者提供给对应的服务或者接口,就像在淘宝上买东西一样,商家为我们提供各种商品,而我们则根据需求向商家指明要买的东西付钱即可。
而消费者想要想要向提供者发起请求调用,就需要提供者启动Web服务,消费者通过请求服务端发送Http或者其他请求给web服务器,web服务器就将请求信息发送给提供者,这是提供者就会提供对应的服务。
然而服务提供者一般会有多个服务或者方法,如果为每一个接口都重新编写一个服务调用的方法就太麻烦了,所以考虑为提供者加一个请求处理器,可以根据客户端请求参数调用不同的服务。
为提供者增加一个本地注册中心,用于存储提供者的服务和方法,当客户端发送请求后,请求处理器收到请求会从注册中心找到对应方法,并通过Java的反射机制调用method指定的方法。
同时由于java对象无法在网络中传输,在发送和接受请求时需要对数据进行序列化和反序列化。
而为了实现类似于本地调用的效果,我们需要使用代理模式增加一个代理对象,它允许客户端通过一个本地代理对象来间接地调用远程服务上的方法,进而简化消费者发送请求的代码。
二、 项目实现
1.项目整体结构
该项目分为以下几个模块:
- demo_common 代码公共依赖,包括model和service
- demo_consumer 服务消费者代码
- demo_provider 服务提供者代码
- hpq_rpc rpc框架代码(简易版)
2.公共模块
1). 公共模块被consumer和provider模块所引用,提供相应的User数据模型和UserService接口
2). User数据模型代码如下:
package com.hpq.model;
import java.io.Serializable;
public class User implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
该代码中需要实现序列化接口,为后续网络传输对参数序列化提供支持
3). UserService接口代码,定义一个获取用户的方法
package com.hpq;
import com.hpq.model.User;
/**
* 用户服务接口
*/
public interface UserService {
/**
* 获取用户
* @param user
* @return
*/
User getUser(User user);
}
3.Provider模块
1). 实现UserService接口代码
package com.hpq;
import com.hpq.model.User;
/**
* 用户服务实现类
*/
public class UserServiceImpl implements UserService{
public User getUser(User user) {
System.out.println("用户名:"+user.getName());
return user;
}
}
2). 服务提供者启动类编写
package com.hpq;
import com.hpq.register.LocalRegister;
import com.hpq.server.HttpServer;
import com.hpq.server.VertxHttpServerImpl;
import java.io.IOException;
public class ProviderApplication {
public static void main(String[] args) throws IOException {
//提供服务
}
}
3).依赖引入
<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.34</version>
</dependency>
<dependency>
<groupId>com.hpq</groupId>
<artifactId>demo_common</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
我们引入了Common模块的依赖以此来使用User模型和接口,另外加入了相关工具类的依赖,便于后续使用。
4.Consumer模块
1).依赖引入
该模块依赖与Provider模块依赖一致,不再重复展示,参考前面代码
2).服务消费者启动类代码
package com.hpq;
import com.hpq.model.User;
import com.hpq.proxy.ServiceProxyFactory;
public class ConsumerApplication {
public static void main(String[] args) {
//动态代理
UserService userService = null;
User user = new User();
user.setName("hpq");
//调用服务
User newUser = userService.getUser(user);
if (newUser != null){
System.out.println("用户名:"+newUser.getName());
}else{
System.out.println("用户不存在");
}
}
}
由于代码尚未完善,此处UserService暂时设置为空,等后续完善代码后即可实现远程调用服务提供者,就像本地调用一样调用UserService的方法
5.web服务器
为了让服务提供者可以提供远程调用的服务,我们使用web服务器实现,常见的web服务器有很多。比如Tomcat,Netty和Vert.x等。这里我们使用Vert.x实现rpc框架的web服务器
Vert.x官方文档:https://vertx.io/
1).在hpq_rpc模块引入Vert.x和相关工具类的依赖
<!-- 引入 vertx web服务器-->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.4.0</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.34</version>
</dependency>
rpc模块的代码框架如下:
2).编写Http服务器启动接口
package com.hpq.server;
/**
* Http服务接口
*/
public interface HttpServer {
/**
* 启动服务器
* @param port
*/
void doStart(int port);
}
3).使用Vert.x实现服务器启动接口
首先将Vertx实例化,然后通过vertx创建http服务器,调用resquestHandler方法用于处理Http请求并监听指定端口
package com.hpq.server;
import io.vertx.core.Vertx;
public class VertxHttpServerImpl implements HttpServer {
@Override
public void doStart(int port) {
//创建vert.x实例
Vertx vertx = Vertx.vertx();
//创建http服务器
io.vertx.core.http.HttpServer httpServer = vertx.createHttpServer();
//监听端口并处理请求
httpServer.requestHandler(req -> {
//处理HTTP请求
System.out.println("Received request: " + req.method() + " " + req.uri());
//响应请求
req.response()
.putHeader("content-type", "text/plain")
.end("Hello HttpServer from Vert.x!");
});
//启动服务器并监听指定端口
httpServer.listen(port, res -> {
if (res.succeeded()) {
System.out.println("HttpServer started on port " + port);
} else {
System.out.println("Failed to start HttpServer: " + res.cause().getMessage());
}
});
}
}
6.本地注册中心
本地注册中心通常指的是在单个应用实例内部或者在开发环境中使用的注册中心。一般不提供高可用性或持久化存储,主要用于开发和测试目的。本地注册中心一般不支持跨网络或跨机器的服务发现。
远程注册中心是一个中心化的服务,通常部署在服务器集群上,可以跨网络访问,例如Redis,Zookeeper。它提供了高可用性、持久化存储和跨机器的服务发现能力。远程注册中心支持大规模分布式系统的服务注册和发现,能够处理大量的服务实例和请求。
本地注册中心可能只支持基本的服务注册和发现功能,而远程注册中心可能提供更高级的功能,如负载均衡、健康检查、服务分组、命名空间等,由于我们在此只是实现的简易版的RPC框架,因此简单实现本地注册中心即可
在RPC模块中创建LocalRegister方法:
package com.hpq.register;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 本地服务注册中心
*/
public class LocalRegister {
//注册信息存储
private static final Map<String, Class<?>> map = new ConcurrentHashMap<>();
//注册服务
public static void register(String interfaceName, Class<?> implClass) {
map.put(interfaceName, implClass);
}
/**
* 获取服务
*/
public static Class<?> get(String interfaceName) {
return map.get(interfaceName);
}
/**
* 删除服务
*/
public static void remove(String interfaceName) {
map.remove(interfaceName);
}
}
该注册中心用于存储服务接口名称到其实现类的映射,并使用ConcurrentHashMap来保证线程安全性和高并发性能,提供注册,获取和删除服务
7.序列化器
在分布式系统中,例如RPC框架,序列化和反序列化是通信过程中不可或缺的部分。客户端发送请求时,需要将请求对象序列化后通过网络发送到服务端;服务端接收到请求后,需要进行反序列化,将字节流转换为服务端可以理解的对象,然后执行相应的操作。操作完成后,服务端将结果序列化后发送回客户端,客户端再进行反序列化以获取结果。
- 序列化:java对象转化为字节数组
- 反序列化:将字节数组转化为java对象
常见的序列化格式包括 JSON、XML、Protocol Buffers、Hessian等
1).在RPC模块编写Serizlizer接口,便于后续拓展
package com.hpq.serializer;
import java.io.IOException;
public interface Serializer {
/**
* 序列化
* @param object
* @return
* @param <T>
*/
<T> byte[] serialize(T object) throws IOException;
/**
* 反序列化
* @param data
* @param clazz
* @return
* @param <T>
*/
<T> T deserialize(byte[] data, Class<T> clazz) throws IOException;
}
2).完成序列化接口的实现类jdkSerializer
package com.hpq.serializer;
import java.io.*;
/**
* jdk自带的序列化器
*/
public class jdkSerializer implements Serializer{
/**
* 序列化
* @param object
* @return
* @param <T>
* @throws IOException
*/
@Override
public <T> byte[] serialize(T object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw e;
}
}
/**
* 反序列化
* @param data
* @param clazz
* @return
* @param <T>
* @throws IOException
*/
@Override
public <T> T deserialize(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();
}
}
}
这里的序列化和反序列化代码为固定格式,无需死记硬背,知道原理即可
8.请求处理器
定义了一个 HTTP 服务器请求处理器 HttpServerHandler,它实现了 Vert.x 的 Handler<HttpServerRequest> 接口。这个处理器主要用于处理来自客户端的 HTTP 请求,并通过序列化和反序列化来实现远程过程调用 (RPC)
1).定义请求调用类RpcRequest
package com.hpq.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* Rpc请求
*/
@Data
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class RpcRequest implements Serializable {
// 服务接口名
private String interfaceName;
// 服务方法名
private String methodName;
// 参数类型列表
private Class<?>[] parameterTypes;
// 参数列表
private Object[] parameters;
}
该类的作用是对请求中的信息进行封装,例如服务接口名、服务方法名、参数类型列表和参数列表
2).定义响应类RpcResponse
package com.hpq.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* Rpc响应
*/
@Data
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class RpcResponse implements Serializable {
//响应数据
private Object data;
//响应信息
private String message;
//响应数据类型
private Class<?> dataType;
//异常信息
private Exception exception;
}
该类的作用是对服务提供者响应的信息进行封装,例如响应数据、响应信息、响应数据类型和异常信息
3).完成了请求和响应数据的封装即可完成HttpServerHandler的实现:
- 将字节数组反序列化为对象,并从请求对象中获取参数
- 根据接口名称从本地注册中心获取对应接口实现类
- 通过反射机制invoke调用方法得到返回结果
- 对返回结果进行处理封装和序列化,写入到响应中
package com.hpq.server;
import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import com.hpq.register.LocalRegister;
import com.hpq.serializer.Serializer;
import com.hpq.serializer.jdkSerializer;
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.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 处理http请求
*/
public class HttpServerHandler implements Handler<HttpServerRequest> {
@Override
public void handle(HttpServerRequest request) {
//指定序列化器
final Serializer serializer = new jdkSerializer();
//记录日志
System.out.println("Receive request:"+ request.method()+ " " + request.uri());
//异步处理请求
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();
//如果请求为null,直接返回
if(rpcRequest == null){
rpcResponse.setMessage("Request is null");
doResponse(request,rpcResponse,serializer);
return;
}
//获取要调用的服务实现类,通过反射调用
try {
Class<?> imClass = LocalRegister.get(rpcRequest.getInterfaceName());
Method method = imClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
Object result = method.invoke(imClass.newInstance(), rpcRequest.getParameters());
//封装返回结果
rpcResponse.setData(result);
rpcResponse.setDataType(result.getClass());
rpcResponse.setMessage("success");
} catch (Exception e) {
e.printStackTrace();
rpcResponse.setMessage(e.getMessage());
rpcResponse.setException(e);
}
//响应
doResponse(request,rpcResponse,serializer);
});
}
/**
* 响应
* @param request
* @param rpcResponse
* @param serializer
*/
private void doResponse(HttpServerRequest request, RpcResponse rpcResponse, Serializer serializer) {
HttpServerResponse response = request.response().putHeader("Content-Type", "application/json");
//序列化
try {
byte[] serialized = serializer.serialize(rpcResponse);
response.end(Buffer.buffer(serialized));
} catch (IOException e) {
e.printStackTrace();
response.end(Buffer.buffer());
}
}
}
9.代理对象
静态代理和动态代理是两种常用的代理模式。静态代理指通过创建一个与目标对象接口相同的接口,然后在代理类中实现该接口,并在内部持有目标对象的引用。但由于其不够灵活,每增加一个目标对象,都需要增加一个代理类,增加了系统的复杂性。
动态代理指在程序运行时,根据目标对象动态创建代理对象,代理对象的接口和目标对象的接口是一致的。并且非常灵活,可以动态地为任何接口生成代理,不需要为每个目标对象编写单独的代理类。
因此在这里重点讲述动态代理的实现
1).通过实现 InvocationHandler 接口并重写 invoke 方法,当通过代理对象调用接口中的方法时,会触发 invoke 方法的执行。
然后根据被调用的方法信息(方法名、参数类型、参数值等)创建 RpcRequest 对象,用于封装远程调用的所有必要信息。
使用 JdkSerializer 对象将 RpcRequest 序列化为字节数组,以便在网络上传输。
在接收到服务器响应后,将字节数组反序列化为 RpcResponse 对象。
package com.hpq.proxy;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import com.hpq.serializer.Serializer;
import com.hpq.serializer.jdkSerializer;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* jdk动态代理
*/
public class ServiceProxy implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 指定序列化器
Serializer serializer = new jdkSerializer();
// 构造请求
RpcRequest rpcRequest = RpcRequest.builder()
.interfaceName(method.getDeclaringClass().getName())
.methodName(method.getName())
.parameterTypes(method.getParameterTypes())
.parameters(args)
.build();
try {
// 序列化
byte[] bodyBytes = serializer.serialize(rpcRequest);
// 发送请求
// ,这里地址被硬编码了(需要使用注册中心和服务发现机制解决)
try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080")
.body(bodyBytes)
.execute()) {
byte[] result = httpResponse.bodyBytes();
// 反序列化
RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);
return rpcResponse.getData();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
由于此处只是简单实现RPC框架,并未实现第三方注册中心和服务发现机制,所以请求地址采用硬编码。如需使用可自行解决。
2).创建代理工厂ServiceProxyFactory,使用 Java 的动态代理机制 (Proxy.newProxyInstance) 来创建代理对象。
package com.hpq.proxy;
import java.lang.reflect.Proxy;
/**
* 服务代理工厂(创建代理对象)
*/
public class ServiceProxyFactory {
/**
* 根据服务类创建代理对象
*/
public static <T> T getProxy(Class<T> interfaceClass) {
return (T) Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class[]{interfaceClass},
new ServiceProxy()
);
}
}
3).完成代理工厂的设计之后即可通过调用工厂动态获取代理对象
最后我们来到服务消费者的启动类ConsumerApplication里使用代理对象即可,代码如下:
//动态代理
UserService userService = ServiceProxyFactory.getProxy(UserService.class);
到此我们的rpc框架就已经构建完成
三、测试
1).首先点击调试按钮调试我们的ProviderApplication(服务提供者启动类)
2)调试完成后即可在控制台看到下面字样,
3).然后找到服务消费者启动类(ConsumerApplication),同样点击调试按钮开始调试
4)在启动了两个启动类之后就可以看到控制台的输出结果,如果两个控制台都显示了名字,则代表调试成功。
以上即时RPC框架的整个开发流程和相关代码,欢迎各位学习,如果文章中有什么错误也欢迎各位评论指正。