由浅入深让你搞透RPC,不要让框架遮住你的眼
ccwgpt 2024-09-17 12:49 44 浏览 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
如果觉得本文对你有帮助,可以转发关注支持一下
相关推荐
- 2025南通中考作文解读之四:结构框架
-
文题《继续走,迈向远方》结构框架:清晰叙事,层层递进示例结构:1.开头(点题):用环境描写或比喻引出“走”与“远方”,如“人生如一条长路,每一次驻足后,都需要继续走,才能看见更美的风景”。2.中间...
- 高中数学的知识框架(高中数学知识框架图第三章)
-
高中数学的知识框架可以划分为多个核心板块,每个板块包含具体的知识点与内容,以下为详细的知识框架结构:基础知识1.集合与逻辑用语:涵盖集合的概念、表示方式、性质、运算,以及命题、四种命题关系、充分条件...
- 决定人生的六大框架(决定人生的要素)
-
45岁的自己混到今天,其实是失败的,要是早点意识到影响人生的六大框架,也不至于今天的模样啊!排第一的是环境,不是有句话叫人是环境的产物,身边的环境包括身边的人和事,这些都会对一个人产生深远的影响。其次...
- 2023年想考过一级造价师土建计量,看这30个知识点(三)
-
第二章工程构造考点一:工业建筑分类[考频分析]★★★1.按厂房层数分:(1)单层厂房;(2)多层厂房;(3)混合层数厂房。2.按工业建筑用途分:(1)生产厂房;(2)生产辅助厂房;(3)动力用厂房;(...
- 一级建造师习题集-建筑工程实务(第一章-第二节-2)
-
建筑工程管理与实务题库(章节练习)第一章建筑工程技术第二节结构设计与构造二、结构设计1.常见建筑结构体系中,适用建筑高度最小的是()。A.框架结构体系B.剪力墙结构体系C.框架-剪力墙结构体系D...
- 冷眼读书丨多塔斜拉桥,这么美又这么牛
-
”重大交通基础设施的建设是国民经济和社会发展的先导,是交通运输行业新技术集中应用与创新的综合体现。多塔斜拉桥因跨越能力强、地形适应性强、造型优美等特点,备受桥梁设计者的青睐,在未来跨越海峡工程中将得...
- 2021一级造价师土建计量知识点:民用建筑分类
-
2021造价考试备考开始了,学霸君为大家整理了一级造价师备考所用的知识点,希望对大家的备考道路上有所帮助。 民用建筑分类 一、按层数和高度分 1.住宅建筑按层数分类:1~3层为低层住宅,4~6层...
- 6个建筑结构常见类型,你都知道吗?
-
建筑结构是建筑物中支承荷载(作用)起骨架作用的体系。结构是由构件组成的。构件有拉(压)杆、梁、板、柱、拱、壳、薄膜、索、基础等。常见的建筑结构类型有6种:砖混结构、砖木结构、框架结构、钢筋混凝土结构、...
- 框架结构设计经验总结(框架结构设计应注意哪些问题)
-
1.结构设计说明主要是设计依据,抗震等级,人防等级,地基情况及承载力,防潮抗渗做法,活荷载值,材料等级,施工中的注意事项,选用详图,通用详图或节点,以及在施工图中未画出而通过说明来表达的信息。2.各...
- 浅谈混凝土框架结构设计(混凝土框架结构设计主要内容)
-
浅谈混凝土框架结构设计 摘要:结构设计是个系统的全面的工作,需要扎实的理论知识功底,灵活创新的思维和严肃认真负责的工作态度。钢筋混凝土框架结构虽然相对简单,但设计中仍有很多需要注意的问题。本文针...
- 2022一级建造师《建筑实务》1A412020 结构设计 精细考点整理
-
历年真题分布统计1A412021常用建筑结构体系和应用一、混合结构体系【2012-3】指楼盖和屋盖采用钢筋混凝土或钢木结构,而墙和柱采用砌体结构建造的房屋,大多用在住宅、办公楼、教学楼建筑中。优点:...
- 破土动工!这个故宫“分院”科技含量有点儿高
-
故宫“分院”设计图。受访者供图近日,位于北京海淀区西北旺镇的故宫北院区项目已开始破土动工,该项目也被称作故宫“分院”,筹备近十年之久。据悉,故宫本院每年展览文物的数量不到1万件,但是“分院”建成后,预...
- 装配式结构体系介绍(上)(装配式结构如何设计)
-
PC构件深化、构件之间连接节点做法等与相应装配式结构体系密切相关。本节列举目前常见的几种装配式结构体系:装配整体式混凝土剪力墙结构体系、装配整体式混凝土框架结构体系、装配整体式混凝土空腔结构体系(S...
- 这些不是双向抗侧结构体系(这些不是双向抗侧结构体系的特点)
-
双向抗侧土木吧规范对双向抗恻力结构有何规定?为何不应采用单向有墙的结构?双向抗侧土木吧1.规范对双向抗侧力结构体系的要求抗侧力体系是指抵抗水平地震作用及风荷载的结构体系。对于结构体系的布置,规范针对...
- 2022一级建造师《建筑实务》1A412020 结构设计 精细化考点整理
-
1A412021常用建筑结构体系和应用一、混合结构体系【2012-3】指楼盖和屋盖采用钢筋混凝土或钢木结构,而墙和柱采用砌体结构建造的房屋,大多用在住宅、办公楼、教学楼建筑中。优点:抗压强度高,造价...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- JAVA集合框架 (47)
- grpc框架 (55)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)