由浅入深让你搞透RPC,不要让框架遮住你的眼
ccwgpt 2024-09-17 12:49 32 浏览 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年的今天,盲盒小程序作为一种新兴的购物方式,正以其独特的魅力和个性化体验吸引着越来越多的消费者。这种将线上购物与盲盒概念相结合的应用,不仅为消费者带来了未知的惊喜,还通过一系列技术手段实现了...
- 小程序·云开发已支持单日亿级调用量,接口可用率高达99.99%
-
2019-10-1914:1210月19日,由腾讯云与微信小程序团队联合举办的“小程序·云开发”技术峰会在北京召开。会上,微信小程序团队相关负责人表示“小程序·云开发”系统架构已经支持每天亿级别的...
- 程序员副业开启模式:8个GitHub上可以赚钱的小程序
-
前言开源项目作者:JackonYang今天推荐的这个项目是「list-of-wechat-mini-program-list」,开源微信小程序列表的列表、有赚钱能力的小程序开源代码。这个项目分为两部分...
- 深度科普:盲盒小程序开发的底层逻辑
-
在当下的数字化浪潮中,盲盒小程序以其独特的趣味性和互动性,吸引着众多消费者的目光。无论是热衷于收集玩偶的年轻人,还是享受拆盒惊喜的上班族,都对盲盒小程序情有独钟。那么,这种备受欢迎的盲盒小程序,其开发...
- 微信小程序的制作步骤
-
SaaS小程序制作平台,作为数字化转型时代下的创新产物,不仅将易用性置于设计的核心位置,让非技术背景的用户也能轻松上手,快速制作出功能丰富、界面精美的小程序,更在性能和稳定性方面投入了大量精力,以确保...
- 携程开源--小程序构建工具,三分钟搞定
-
前言今天推荐的这个项目是「wean」,一个小程序构建打包工具。在wean之前,大量小程序工具使用webpack进行打包,各种loader、plugin导致整个开发链路变长。wean旨在解...
- 校园小程序的搭建以及营收模式校园外卖程序校园跑腿校园圈子系统
-
校园小程序的架构设计主要包括云端架构和本地架构两部分。云端架构方面,采用Serverless架构可以降低技术门槛,通过阿里云、腾讯云等平台提供的云服务,可以实现弹性扩容和快速部署。例如,使用云数据库、...
- 盲盒小程序开发揭秘:技术架构与实现原理全解析
-
在2025年的今天,盲盒小程序作为一种结合了线上购物与趣味性的创新应用,正受到越来越多用户的喜爱。其背后的技术架构与实现原理,对于想要了解或涉足这一领域的人来说,无疑充满了神秘与吸引力。本文将为大家科...
- 月活百万的小程序架构设计:流量暴增秘籍
-
从小程序到"大"程序的蜕变之路当你的小程序用户量从几千跃升至百万级别时,原有的架构就像一件不合身的衣服,处处紧绷。这个阶段最常遇到的噩梦就是服务器崩溃、接口超时、数据丢失。想象一下,在...
- 认知智能如何与产业结合?专家学者共探理论框架与落地实践
-
当前,以大模型为代表的生成式人工智能等前沿技术加速迭代,如何将认知智能与产业结合,成为摆在各行各业面前的一个问题。论坛现场。主办方供图7月4日,2024世界人工智能大会暨人工智能全球治理高级别会议在...
- 现代中医理论框架
-
...
- 认知行为(CBT)中的ABC情绪理论
-
情绪ABC理论是由美国心理学家阿尔伯特·艾利斯(AlbertEllis1913-2007)创建的理论,A表示诱发性事件(Activatingevent),B表示个体针对此诱发性事件产生的一些信...
- 说说卡伦霍妮的理论框架,对你调整性格和人际关系,价值很大
-
01自在今天我主要想说下霍妮的理论框架。主要说三本书,第一本是《我们时代的神经症人格》,第二本是《我们内心的冲突》,第三本是《神经症与人的成长》。根据我的经验,三本书价值巨大,但并不是每个人都能读进去...
- 供应链管理-理论框架
-
一个最佳价值的供应链,应该是一个具有敏捷性、适应性和联盟功能(3A)的供应链,其基本要素包括战略资源、物流管理、关系管理以及信息系统,目标是实现速度、质量、成本、柔性的竞争优势。篇幅有...
- 微信WeUI设计规范文件下载及使用方法
-
来人人都是产品经理【起点学院】,BAT实战派产品总监手把手系统带你学产品、学运营。WeUI是一套同微信原生视觉体验一致的基础样式库,由微信官方设计团队为微信Web开发量身设计,可以令用户的使用感知...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- express框架 (43)
- scrapy框架 (52)
- beego框架 (42)
- java框架spring (43)
- grpc框架 (55)
- 前端框架bootstrap (42)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)