如何在没有接口的情况下进行 RPC 调用?

简介: 本文介绍了RPC框架中“泛化调用”的实现原理与应用场景。针对测试平台、服务网关等无法依赖接口API的场景,通过统一的GenericService接口和动态代理机制,将接口名、方法名、参数等封装为请求消息,实现无需API的RPC调用。同时,结合专属序列化插件解决参数序列化问题,并支持异步调用,提升灵活性与性能。

在 RPC 运营的过程中,让调用端在没有接口 API 的情况下发起 RPC 调用的需求,不只是一个业务方和我提过,这里我列举两个非常典型的场景例子。
场景一:我们要搭建一个统一的测试平台,可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值,在线测试自己发布的 RPC 服务。这时我们就有一个问题要解决,我们搭建统一的测试平台实际上是作为各个 RPC 服务的调用端,而在 RPC 框架的使用中,调用端是需要依赖服务提供方提供的接口 API 的,而统一测试平台不可能依赖所有服务提供方的接口 API。我们不能因为每有一个新的服务发布,就去修改平台的代码以及重新上线。这时我们就需要让调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起 RPC 调用。

场景二:我们要搭建一个轻量级的服务网关,可以让各个业务方用 HTTP 的方式,通过服务网关调用其它服务。这时就有与场景一相同的问题,服务网关要作为所有 RPC 服务的调用端,是不能依赖所有服务提供方的接口 API 的,也需要调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起 RPC 调用。

这两个场景都是我们经常会碰到的,而让调用端在没有服务提供方提供接口 API 的情况下仍然可以发起 RPC 调用的功能,在 RPC 框架中也是非常有价值的。
怎么做?
RPC 框架要实现这个功能,我们可以使用泛化调用。那什么是泛化调用呢?我们带着这个问题,先学习下如何在没有接口的情况下进行 RPC 调用。
我们先回想下我在基础篇讲过的内容,通过前面的学习我们了解到,在 RPC 调用的过程中,调用端向服务端发起请求,首先要通过动态代理,正如 第 05 讲 中我说过的,动态代理可以帮助我们屏蔽 RPC 处理流程,真正地让我们发起远程调用就像调用本地一样。
那么在 RPC 调用的过程中,既然调用端是通过动态代理向服务端发起远程调用的,那么在调用端的程序中就一定要依赖服务提供方提供的接口 API,因为调用端是通过这个接口 API 自动生成动态代理的。那如果没有接口 API 呢?我们该如何让调用端仍然能够发起 RPC 调用呢?
所谓的 RPC 调用,本质上就是调用端向服务端发送一条请求消息,服务端接收并处理,之后向调用端发送一条响应消息,调用端处理完响应消息之后,一次 RPC 调用就完成了。那是不是说我们只要能够让调用端在没有服务提供方提供接口的情况下,仍然能够向服务端发送正确的请求消息,就能够解决这个问题了呢?
没错,只要调用端将服务端需要知道的信息,如接口名、业务分组名、方法名以及参数信息等封装成请求消息发送给服务端,服务端就能够解析并处理这条请求消息,这样问题就解决了。过程如下图所示:

现在我们已经清楚了解决问题的关键,但 RPC 的调用端向服务端发送消息是需要以动态代理作为入口的,我们现在得继续想办法让调用端发送我刚才讲过的那条请求消息。
我们可以定义一个统一的接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,而 GenericService 接口的 $invoke 方法的入参就是方法名以及参数信息。
这样我们传递给服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等都可以通过调用 GenericService 代理的 $invoke 方法来传递。具体的接口定义如下:
class GenericService {
Object $invoke(String methodName, String[] paramTypes, Object[] params);

}
这个通过统一的 GenericService 接口类生成的动态代理,来实现在没有接口的情况下进行 RPC 调用的功能,我们就称之为泛化调用。
通过泛化调用功能,我们可以解决在没有服务提供方提供接口 API 的情况下进行 RPC 调用,那么这个功能是否就完美了呢?
回顾下 第 17 讲 我过的内容,RPC 框架可以通过异步的方式提升吞吐量,还有如何实现全异步的 RPC 框架,其关键点就是 RPC 框架对 CompletableFuture 的支持,那么我们的泛化调用是否也可以支持异步呢?
当然可以。我们可以给 GenericService 接口再添加一个异步方法 $asyncInvoke,方法的返回值就是 CompletableFuture,GenericService 接口的具体定义如下:

class GenericService {
Object $invoke(String methodName, String[] paramTypes, Object[] params);
CompletableFuture $asyncInvoke(String methodName, String[] paramTypes, Object[] params);
}
学到这里相信你已经对泛化调用的功能有一定的了解了,那你有没有想过这样一个问题?在没有服务提供方提供接口 API 的情况下,我们可以用泛化调用的方式实现 RPC 调用,但是如果没有服务提供方提供接口 API,我们就没法得到入参以及返回值的 Class 类,也就不能对入参对象进行正常的序列化。这时我们会面临两个问题:
问题 1:调用端不能对入参对象进行正常的序列化,那调用端、服务端在接收到请求消息后,入参对象又该如何序列化与反序列化呢?
回想下 第 07 讲,在这一讲中我讲解了如何设计可扩展的 RPC 框架,我们通过插件体系来提高 RPC 框架的可扩展性,在 RPC 框架的整体架构中就包括了序列化插件,我们可以为泛化调用提供专属的序列化插件,通过这个插件,解决泛化调用中的序列化与反序列化问题。
问题 2:调用端的入参对象(params)与返回值应该是什么类型呢?
在服务提供方提供的接口 API 中,被调用的方法的入参类型是一个对象,那么使用泛化调用功能的调用端,可以使用 Map 类型的对象,之后通过泛化调用专属的序列化方式对这个 Map 对象进行序列化,服务端收到消息后,再通过泛化调用专属的序列化方式将其反序列成对象。
总结
今天我们主要讲解了如何在没有接口的情况下进行 RPC 调用,泛化调用的功能可以实现这一目的。
这个功能的实现原理,就是 RPC 框架提供统一的泛化调用接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,通过调用 GenericService 代理的 $invoke 方法将服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等封装成请求消息,发送给服务端,实现在没有接口的情况下进行 RPC 调用的功能。
而通过泛化调用的方式发起调用,由于调用端没有服务端提供方提供的接口 API,不能正常地进行序列化与反序列化,我们可以为泛化调用提供专属的序列化插件,来解决实际问题。
课后思考
在讲解泛化调用时,我讲到服务端在收到调用端通过泛化调用的方式发送过来的请求时,会使用泛化调用专属的序列化插件实现对其进行反序列化,那么服务端是如何判定这个请求消息是通过泛化调用的方式发送过来的消息呢?
笔者认为:可以在协议中设置序列化类型,在服务端接受到后,就知道用什么插件反序列化了

相关文章
|
Kubernetes 负载均衡 网络协议
Kubernetes 跨集群流量调度实战
Kubernetes 问世于 2015 年,从一开始秉持着松耦合和可扩展的设计理念,也因此带来了 Kubernetes 生态的蓬勃发展。但这些大部分先限制在单一集群内,然后由于种种原因和目的企业内部创建的集群越来越多,比如单集群故障、监管要求、异地多机房可用区容灾、出于敏捷、降本考虑的混合云、多云部署、单一集群的承载能力受限、多版本 Kubernetes 集群共存等。多集群之后除了提升管理的难度外,首当其冲的就是多集群间的流量调度,这也是多集群部署的基础。没有跨集群的通信,多集群的收益也会大打折扣。
1036 1
Kubernetes 跨集群流量调度实战
|
7月前
|
监控 Java Spring
AOP切面编程快速入门
AOP(面向切面编程)通过分离共性逻辑,简化代码、减少冗余。它通过切点匹配目标方法,在不修改原方法的前提下实现功能增强,如日志记录、性能监控等。核心概念包括:连接点、通知、切入点、切面和目标对象。Spring AOP支持多种通知类型,如前置、后置、环绕、返回后、异常通知,灵活控制方法执行流程。通过@Pointcut可复用切点表达式,提升维护性。此外,结合自定义注解,可实现更清晰的切面控制。
621 5
|
3月前
|
jenkins Java 持续交付
Jenkins前置配置
本文介绍Jenkins与GitLab集成的完整配置流程:包括GitLab账号创建、SSH密钥配置、API Token生成,Jenkins中GitLab连接、凭据管理、全局Git信息设置,以及节点服务器环境搭建(JDK、Maven、Node、Docker等),并详细说明Jenkins节点通过SSH方式接入的步骤,实现自动化拉取代码、构建打包与持续集成。
|
3月前
|
缓存 前端开发 JavaScript
头条面经
涵盖前端、网络、JS核心、框架及算法等多方面知识,包括深拷贝、双向绑定、HTTP缓存、跨域、Vue原理、TCP/UDP、设计模式、事件循环、类型判断、闭包、原型链、性能优化等高频面试题,全面考察技术深度与综合能力。
|
存储 人工智能
|
12月前
|
人工智能 编解码 自动驾驶
RF-DETR:YOLO霸主地位不保?开源 SOTA 实时目标检测模型,比眨眼还快3倍!
RF-DETR是首个在COCO数据集上突破60 mAP的实时检测模型,结合Transformer架构与DINOv2主干网络,支持多分辨率灵活切换,为安防、自动驾驶等场景提供高精度实时检测方案。
2767 6
RF-DETR:YOLO霸主地位不保?开源 SOTA 实时目标检测模型,比眨眼还快3倍!
|
存储 缓存 网络协议
第五问:一个程序从点击到启动发生了什么?
一个可执行程序从用户点击启动到运行,经历了8个主要阶段:用户触发启动、操作系统查找文件、进程创建、可执行文件加载到内存、初始化程序上下文、执行程序入口点、程序运行和程序退出。涉及硬盘、内存、缓存等硬件交互。
|
设计模式 消息中间件 安全
面试官:说说读写锁的实现原理?
面试官:说说读写锁的实现原理?
437 1
|
算法 Java 测试技术
【软件设计师备考 专题 】软件开发方法:生命周期法、原型法、面向对象法、CASE
【软件设计师备考 专题 】软件开发方法:生命周期法、原型法、面向对象法、CASE
686 0
|
机器学习/深度学习 传感器 算法
如何优化物联网设备的能源消耗?
【7月更文挑战第29天】如何优化物联网设备的能源消耗?
631 8