由浅入深让你搞透RPC,不要让框架遮住你的眼
ccwgpt 2024-09-17 12:49 51 浏览 0 评论
HTTP经常接触,大家也不陌生,这是一个超文本传输协议,能够在网络直接传输数据。目前微服务项目很火,微服务之间基本都是使用HTTP传输,例如Feign,OkHttp,RestTemlpate等等。但是分布式这个话题目前还不过时,今天在这里说下分布式的基石:RPC(Remote Procedure Call:远程过程调用)技术,在Java中称为RMI( Remote Method Invocation ,远程方法调用)。白话解释下:该技术就是让使用者在调用一个服务时的时候无感知地调用一个远程服务。这种无感知让大部分开发者不需要了解技术本身就可以很容易的使用 ,但是框架封装了底层(各种抽象、分层),使得无法知道内部是如何运行, 下面来介绍下什么是RPC
写文章不能千篇一律,专业名字一大堆,各种技术天上飞。我们一步步来,由浅入深。
存在这么一个方法
public class UserService {
public String findNameById(Long id) {
return "my name " + id;
}
}
正常调用
public class Test {
public static void main(String[] args) {
UserService userService = new UserService();
String name = userService.findNameById(1L);
System.out.println(name);
}
}
控制台
my name 1
Process finished with exit code 0
这里不涉及框架,只是基本功能。到这里方法也调用了,功能也实现了。
方法执行核心
但是,某一天领导告诉你,你写的UserService没有用,具体的处理流程是另一个人管的,而且另一个人不和你一个项目,他(提供者)写好了一个实现
public class OtherUserService {
public String findNameById(Long id) {
return "other name " + id ;
}
}
此时你一想,他在大西洋,我怎么去调用他写的方法?于是RPC便可以出手了,RPC能干嘛?RPC能顺着网线去找到他。且看RPC如何做
先说方法调用: 正常情况下是通过实例对象+点操作符+方法名称调用
userService.findNameById(1L);
但是一个远程服务,是拿不到实例对象的,也就不能去写一个点操作符去调用,这时候方法的调用还可以通过反射
public Object invoke(Object obj, Object... args){...}
// 第一个参数为 实例对象,第二个对象为 参数列表
但是Method这个也只能通过反射来获取,
public Method getMethod(String name, Class<?>... parameterTypes)
// 第一个参数为 方法名称,第二个参数为参数类型列表
这样以来,方法的调用便可以分解为
- 获取类的Method实例(需要类标识、方法名称(标识)、方法参数类型列表)
- 通过Method实例调用方法(需要类实例对象、参数列表)
其中通过实例是可以获取其类型的,instance->class,这里的参数列表便可以获取对应参数类型列表,类实例对象可以获取类Class,但是这是在同一个JVM中才能完成的,一旦分离开来,这些都不能获取,而且分离时类实例对象只有提供者具有,使用者是不知道的(如果知道就没必要去远程调用了)。
所以:远程执行一个方法最少需要4个条件:类标识、方法标识、方法参数类型列表、方法参数列表。拿到这些条件,提供方就可以找到实例对象、调用对应的方法了,而且为了不随意构造实例对象,提供者会主动控制实例对象的构建,之后需要放到一个地方以方便交给Method的来调用
所以基本功能示意图如下,描述为:顺着网线,带着四大金刚,直接找到提供方,围殴一顿后,把结果带回来给使用者
RPC基本示意图
RPC就是对以上功能的完善与封装,好吧,上面描述太粗鲁了,代码写的要优雅!!!,要符合设计原则
接口分离原则
使用方和提供方是两个不同的模块,模块与模块之间应该是通过接口解耦的。所以一般在使用RPC服务时都会把接口层给解耦出来,作为第三方jar给使用方和提供方使用,也便于锁定实例对象。
单一职责原则
一个功能就应该职责单一,不仅容易看懂,也便于后续重构。RPC其实是多个功能模块组合起来的。例如:使用方参数封装模块、数据传输模块职责、数据序列化模块、提供方管理类实例模块等。一个完善的RPC功能组件,其职责也会分的越细,每个模块功能越单一。
开放封闭原则
作为一个RPC,其设计应该是满足开放封闭原则的,每个模块都应该是可扩展的,但是RPC流程应该是对内封闭的。
里氏替换原则
构建实例对象的时候,其类型定义应该是接口类型,而不是具体的实现类型。
依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象对象-->抽象不应该依赖细节,细节应该依赖抽象-->应该针对接口编程,不应该针对实现编程。
RPC简单设计
先定义接口
接口应该单独一个包,作为jar对外提供。使用方和提供方都依赖该jar
public interface IUserService {
/**
* 通过ID获取用户名
*
* @param id 用户ID
* @return java.lang.String
* @author Tinyice
*/
String findNameById(Long id);
}
提供方实现
简单实现
public class OtherUserService implements IUserService {
@Override
public String findNameById(Long id) {
return "other name " + id ;
}
}
使用方请求参数封装
主要封装四大调用条件对象
@Getter
@Setter
public class RpcRequest implements java.io.Serializable {
private static final long serialVersionUID = -7223166969378743326L;
/**
* 类名称
*/
private String className;
/**
* 方法名称
*/
private String methodName;
/**
* 参数类型列表
*/
private Class<?>[] parameterTypes;
/**
* 参数列表
*/
private Object[] parameters;
}
网络传输工具封装
网络传输方式有很多种,这里使用socket
@Slf4j
public class TcpTransport {
private String host;
private int port;
public TcpTransport(String host, int port) {
this.host = host;
this.port = port;
}
public Socket newSocketInstance() {
Socket socket;
try {
socket = new Socket(host, port);
log.info("[{}] 客户端新建连接:服务端地址=【{}】",LocalDateTime.now(), socket.getRemoteSocketAddress());
return socket;
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("客户端连接失败");
}
}
public Object send(RpcRequest rpcRequest) {
Socket socket = null;
try {
socket = newSocketInstance();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(rpcRequest);
objectOutputStream.flush();
log.info("[{}] 请求参数序列化完毕",LocalDateTime.now());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object obj = objectInputStream.readObject();
objectOutputStream.close();
objectInputStream.close();
log.info("[{}] 请求结果接收完毕",LocalDateTime.now());
return obj;
} catch (Exception e) {
throw new RuntimeException("RPC 调用异常");
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
其中send方法就是数据传输方法,包含请求参数的传输和请求结果的接收。
网络请求发起
为了让使用方感知不到具体实现的位置(本地还是远程),一般都会使用代理技术给接口构建一个本地代理对象,通过代理对象来进行远程调用,使得使用者产生使用的是本地方法的错觉。。
动态代理,主要分为JDK动态代理和CGLIB动态代理,不懂原理的可以去看我写的文章。为了简单,这里使用JDK动态代理
public class RpcClientProxy {
public <T> T clientProxy(final Class<T> interfaces, final String host, final int port) {
return (T) Proxy.newProxyInstance(interfaces.getClassLoader(), new Class[]{interfaces}, new RpcInvocationHandler(host, port));
}
}
RpcInvocationHandler为处理代理流程的类,也是网络请求发起类,发起位置在invoke方法,这是JDK动态代理的核心
@Slf4j
public class RpcInvocationHandler implements InvocationHandler {
private String host;
private int port;
public RpcInvocationHandler(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
log.info("客户端开始封装参数【RpcRequest】");
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setClassName(method.getDeclaringClass().getName());
rpcRequest.setMethodName(method.getName());
rpcRequest.setParameterTypes(method.getParameterTypes());
rpcRequest.setParameters(args);
log.info("客户端开始获取传输工具【tcpTransport】");
TcpTransport tcpTransport = new TcpTransport(host, port);
log.info("客户端开始发送请求参数【RpcRequest】");
return tcpTransport.send(rpcRequest);
}
}
知道JDK动态代理,也就指定invoke对java方法调用的意义:方法的真正执行流程。这里封装了请求参数、然后发起网络请求,获取到网络请求结果。这个过程替代了方法的执行过程。
服务端接口监听
客户端和服务端是需要网络通信的,这边也要建立网络监听,这里为了简单,将类实例对象存储功能放在了一起
@Slf4j
public class RpcServer {
private ExecutorService executorService=Executors.newCachedThreadPool();
public void publishServer(final Map<String,Object> serviceRegistCenter, int port){
ServerSocket serverSocket;
try {
serverSocket=new ServerSocket(port);
log.info("服务端启动监听,端口=[{}]",port);
while (true) {
Socket socket=serverSocket.accept();
log.info("服务端监听到新链接,客户端地址=【{}】",socket.getRemoteSocketAddress());
executorService.execute(new RpcServerProcessor(socket,serviceRegistCenter));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端方法调用
上面的RpcServerProcessor就是服务端对客户端请求的处理方法
@Slf4j
public class RpcServerProcessor implements Runnable {
private Socket socket;
private Map<String, Object> serviceRegistCenter;
public RpcServerProcessor(Socket socket, Map<String, Object> serviceRegistCenter) {
this.socket = socket;
this.serviceRegistCenter = serviceRegistCenter;
}
@Override
public void run() {
// 获取客户端传输对象,并反序列化
try (ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
RpcRequest rpcRequest = (RpcRequest) inputStream.readObject();
log.info("[{}] 服务端反序列化完毕", LocalDateTime.now());
Object obj = invoke(rpcRequest);
log.info("[{}] 服务端调用完毕", LocalDateTime.now());
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
log.info("[{}] 服务端序列化完毕——将结果发送给客户端", LocalDateTime.now());
} catch (Exception e) {
e.printStackTrace();
}
}
private Object loadBalance(String className) {
return serviceRegistCenter.get(className);
}
private Object invoke(RpcRequest rpcRequest) throws Exception {
Object[] args = rpcRequest.getParameters();
Class<?>[] types = rpcRequest.getParameterTypes();
String className = rpcRequest.getClassName();
// 服务发现,负载均衡
Object service = loadBalance(className);
// 服务调用
Method method = service.getClass().getMethod(rpcRequest.getMethodName(), types);
return method.invoke(service, args);
}
}
这里也有个invoke方法,就是刚开始分析的反射方法调用,了解这块核心也就指定了RPC如何执行远程方法的。这里也涉及到了服务发现与负载均衡。
功能测试
先启动服务端,服务端发布服务
@Slf4j
public class ServerBootStrap {
public static void main(String[] args) {
// 注册中心
Map<String, Object> registCenter = new HashMap<>(16, 1);
// 实例构建
IUserService userService = new OtherUserService();
// 实例注册
registCenter.put(IUserService.class.getName(), userService);
RpcServer rpcServer = new RpcServer();
log.info("[{}] 服务端发布对外服务【IUserService】", LocalDateTime.now());
rpcServer.publishServer(registCenter, 8888);
}
}
控制台
[2020-09-25T20:29:40.699] 服务端发布对外服务【IUserService】
服务端启动监听,端口=[8888]
客户端调用
@Slf4j
public class ClientBootStrap {
public static void main(String[] args) {
RpcClientProxy proxy = new RpcClientProxy();
IUserService userService = proxy.clientProxy(IUserService.class, "localhost", 8888);
log.info("[{}] 客户端开始发起RPC调用", LocalDateTime.now());
String name = userService.findNameById(101L);
System.out.println(name);
}
}
客户端控制台
[2020-09-25T20:29:55.737] 客户端开始发起RPC调用
客户端开始封装参数【RpcRequest】
客户端开始获取传输工具【tcpTransport】
客户端开始发送请求参数【RpcRequest】
[2020-09-25T20:29:55.748] 客户端新建连接:服务端地址=【localhost/127.0.0.1:8888】
[2020-09-25T20:29:55.763] 请求参数序列化完毕
[2020-09-25T20:29:55.769] 请求结果接收完毕
other name 101
Process finished with exit code 0
服务端控制台
[2020-09-25T20:29:40.699] 服务端发布对外服务【IUserService】
服务端启动监听,端口=[8888]
服务端监听到新链接,客户端地址=【/127.0.0.1:29456】
[2020-09-25T20:29:55.766] 服务端反序列化完毕
[2020-09-25T20:29:55.767] 服务端调用完毕
[2020-09-25T20:29:55.767] 服务端序列化完毕——将结果发送给客户端
可以通过时间顺序观察下调用流程。
总结
以上示例说明了RPC调用流程:包括服务注册、服务发现、负载均衡、请求参数封装、数据传输、序列化与反序列化等等,这里为了简单示例,只是做了演示。一个完善的RPC也是对上面功能的完善。例如:
服务注册可以选择JVM、Redis、Zookeeper、Nacos等
负载均衡:随机负载、轮询负载、权重负载等
数据传输:可以定制传输协议,例如dubbo、http、tcp/ip等
序列化:开源的序列化工具也很多,选择适宜的序列化能提高RPC能力,常见的有:JDK序列化、Hessian、Hessian2、Kryo、protostuff等
其他优化:服务配置、重试策略、失败拒绝策略、版本控制、权限控制等等。
在与Spring Boot集成中,服务客户端调用对象的代理可以和Sping的代理配合,实现无缝连接,直接注入使用。
原文链接:https://my.oschina.net/u/4090547/blog/4650965?_from=gitee_search
如果觉得本文对你有帮助,可以转发关注支持一下
相关推荐
- 一个基于.Net Core遵循Clean Architecture原则开源架构
-
今天给大家推荐一个遵循CleanArchitecture原则开源架构。项目简介这是基于Asp.netCore6开发的,遵循CleanArchitecture原则,可以高效、快速地构建基于Ra...
- AI写代码翻车无数次,我发现只要提前做好这3步,bug立减80%
-
写十万行全是bug之后终于找到方法了开发"提示词管理助手"新版本那会儿,我差点被bug整崩溃。刚开始两周,全靠AI改代码架构,结果十万行程序漏洞百出。本来以为AI说没问题就稳了,结果...
- OneCode低代码平台的事件驱动设计:架构解析与实践
-
引言:低代码平台的事件驱动范式在现代软件开发中,事件驱动架构(EDA)已成为构建灵活、松耦合系统的核心范式。OneCode低代码平台通过创新性的注解驱动设计,将事件驱动理念深度融入平台架构,实现了业务...
- 国内大厂AI插件评测:根据UI图生成Vue前端代码
-
在IDEA中安装大厂的AI插件,打开ruoyi增强项目:yudao-ui-admin-vue31.CodeBuddy插件登录腾讯的CodeBuddy后,大模型选择deepseek-v3,输入提示语:...
- AI+低代码技术揭秘(二):核心架构
-
本文档介绍了为VTJ低代码平台提供支持的基本架构组件,包括Engine编排层、Provider服务系统、数据模型和代码生成管道。有关UI组件库和widget系统的信息,请参阅UI...
- GitDiagram用AI把代码库变成可视化架构图
-
这是一个名为gitdiagram的开源工具,可将GitHub仓库实时转换为交互式架构图,帮助开发者快速理解代码结构。核心功能一键可视化:替换GitHubURL中的"hub...
- 30天自制操作系统:第六天:代码架构整理与中断处理
-
1.拆开bootpack.c文件。根据设计模式将对应的功能封装成独立的文件。2.初始化pic:pic(可编程中断控制器):在设计上,cpu单独只能处理一个中断。而pic是将8个中断信号集合成一个中断...
- AI写代码越帮越忙?2025年研究揭露惊人真相
-
近年来,AI工具如雨后春笋般涌现,许多人开始幻想程序员的未来就是“对着AI说几句话”,就能轻松写出完美的代码。然而,2025年的一项最新研究却颠覆了这一期待,揭示了一个令人意外的结果。研究邀请了16位...
- 一键理解开源项目:两个自动生成GitHub代码架构图与说明书工具
-
一、GitDiagram可以一键生成github代码仓库的架构图如果想要可视化github开源项目:https://github.com/luler/reflex_ai_fast,也可以直接把域名替换...
- 5分钟掌握 c# 网络通讯架构及代码示例
-
以下是C#网络通讯架构的核心要点及代码示例,按协议类型分类整理:一、TCP协议(可靠连接)1.同步通信//服务器端usingSystem.Net.Sockets;usingTcpListene...
- 从复杂到优雅:用建造者和责任链重塑代码架构
-
引用设计模式是软件开发中的重要工具,它为解决常见问题提供了标准化的解决方案,提高了代码的可维护性和可扩展性,提升了开发效率,促进了团队协作,提高了软件质量,并帮助开发者更好地适应需求变化。通过学习和应...
- 低代码开发当道,我还需要学习LangChain这些框架吗?| IT杂谈
-
专注LLM深度应用,关注我不迷路前两天有位兄弟问了个问题:当然我很能理解这位朋友的担忧:期望效率最大化,时间用在刀刃上,“不要重新发明轮子”嘛。铺天盖地的AI信息轰炸与概念炒作,很容易让人浮躁与迷茫。...
- 框架设计并不是简单粗暴地写代码,而是要先弄清逻辑
-
3.框架设计3.框架设计本节我们要开发一个UI框架,底层以白鹭引擎为例。框架设计的第一步并不是直接撸代码,而是先想清楚设计思想,抽象。一个一个的UI窗口是独立的吗?不是的,...
- 大佬用 Avalonia 框架开发的 C# 代码 IDE
-
AvalonStudioAvalonStudio是一个开源的跨平台的开发编辑器(IDE),AvalonStudio的目标是成为一个功能齐全,并且可以让开发者快速使用的IDE,提高开发的生产力。A...
- 轻量级框架Lagent 仅需20行代码即可构建自己的智能代理
-
站长之家(ChinaZ.com)8月30日消息:Lagent是一个专注于基于LLM模型的代理开发的轻量级框架。它的设计旨在简化和提高这种模型下代理的开发效率。LLM模型是一种强大的工具,可以...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- mfc框架 (52)
- abb框架断路器 (48)
- beego框架 (52)
- java框架spring (58)
- grpc框架 (65)
- tornado框架 (48)
- 前端框架bootstrap (54)
- orm框架有哪些 (51)
- 知识框架图 (52)
- ppt框架 (55)
- 框架图模板 (59)
- 内联框架 (52)
- cad怎么画框架 (58)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)