如何手搓一个自定义的RPC(远程过程调用框架)
1、RPC(远程过程调用概述)
远程过程调用(RPC, Remote Procedure Call)是一种通过网络从远程计算机程序上请求服务,而无需了解网络细节的通信技术。在分布式系统中,RPC是一种常用的技术,能够简化客户端与服务器之间的交互。本文将介绍如何基于Netty(网络编程框架)实现一个自定义的简单的RPC框架。
首先简单介绍一下RPC 主要特点:
1.1、RPC远程过程调用的主要特点
1.2、RPC的应用场景
1.3、常见的RPC框架
2、实现自定义的RPC
要实现一个自定义的RPC框架需解决以下几个主要问题:
1.客户端调用:客户端调用本地的代理函数(stub代码,这个函数负责将调用转换为RPC请求)。这其实就是一个接口描述文件,它可以有多种形式如JSON、XML、甚至是一份word文档或是口头约定均可,只要客户端及服务端都是遵守这份接口描述文件契约即可。在我们的实际开发中一种常见的方式是服务提供者发布一个包含服务接口类的jar包到maven 中央仓库,调用方通过pom文件将之依赖到本地。
2.参数序列化:代理函数将调用参数进行序列化,并将请求发送到服务器。
3.服务端数据接收:服务器端接收到请求,并将其反序列化,恢复成原始参数。
4.执行远程过程:服务端调用实际的服务过程(函数)并获取结果。
5.返回结果:服务端将调用结果进行序列化,并通过网络传给客户端。
6.客户端接收调用结果:客户到接收到服务端传输的字节流,进行反序列化,转换为实际的结果数据格式,并返回到原始调用方。
下面需我们通过代码一一展示上述各功能是如何实现的。
2.1、自定义通信协议
本文的目的是要实现一个自定义通信协议的远程调用框架,所以首先要定义一个通信协议数据格式。
整个自定义协议总体上分为Header 及 Body Content两部分;Header 占16个字节,又分为4个部分。
前2位为魔法值用于Netty编解码组件,解决网络通信中的粘包、半包等问题,此处不展开细讲。
msgtype用于表示消息的类型,如request(请求)、respone(响应)、heartbeat(心跳)等。
code 占1位,表示请求的响应状态,成功还是失败。
request id占8位,表示请求的序列号,用于后续调用结果的匹配,保证线程内唯一。
body size 占4位,表示实现请求内容的长度,在反序化时读取此长度的内容字节,解析出正确的数据。
客户端、服务端在通信过程中都要按照上述约定的通信协议进行数据的编、解码工作。
2.2、客户端调用
2.2.1 客户端的使用
客户端一般通过接口代理工厂通过动态代理技术来生成一个代理实例,所有的远程调用中的细节,如参数序列化,网络传输,异常处理等都隐藏在代理实例中实现,对调用方来说调用过程是透明的,就像调用本地方法一样。
首先看一下客户端的使用方式,本文假设一个IShoppingCartService (购物车)的接口类,基中有一个方法根据传入的用户pin,返回购物车详情。
//接口方法
ShoppingCart shopping(String pin);
//客户端通过代理工厂实现接口的一个代理实例
IShoppingCartService serviceProxy = ProxyFactory.factory(IShoppingCartService.class)
.setSerializerType(SerializerType.JDK) //客户端设置所使用的序列化工具,此处为JDK原生
.newProxyInstance(); //返回代理 实现
//像调用本地方法一样,调用此代理实例的shopping 方法
ShoppingCart result = serviceProxy.shopping("userPin");
log.info("result={}", JSONObject.toJSONString(result));
2.2.2、客户端代理工厂的核心功能
public class ProxyFactory<I> {
//……省略
/**
* 代理对象
*
* @return
*/
public I newProxyInstance() {
//服务的元数据信息
ServiceData serviceData = new ServiceData(
group, //分组
providerName, //服务名称,一般为接口的class的全限定名称
StringUtils.isNotBlank(version) ? version : "1.0.0" //版本号
);
//调用器
Calller caller = newCaller().timeoutMillis(timeoutMillis);
//集群策略,用于实现快速失败或失败转等功能
Strategy strategy = StrategyConfigContext.of(strategy, retries);
Object handler = null;
switch (invokeType) {
case "syncCall":
//同步调用handler
handler = new SyncCaller(