栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

RPC深入解析(上)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

RPC深入解析(上)

1.1 序列化技术

  • 序列化的作用
    在网络传输中,数据必须采用二进制形式, 所以在 RPC 调用过程中, 需要采用序列化技术,对入参对象和返回值对象进行序列化与反序列化。
  • 如何进行序列化
    自定义的二进制协议来实现序列化:


一个对象是如何进行序列化? 下面以 User 对象例举讲解:
User 对象:

package com.itcast;
public class User {
​
 
 private String userNo = "0001";
​
 
 private String name = "zhangsan";
​
}
​

包体的数据组成:
业务指令为 0x00000001 占 1 个字节,类的包名 com.itcast 占 10 个字节, 类名 User 占 4 个字节;
属性 UserNo 名称占 6 个字节,属性类型 string 占 2 个字节表示,属性值为 0001 占 4 个字节;
属性 name 名称占 4 个字节,属性类型 string 占 2 个字节表示,属性值为 zhangsan 占 8 个字节;
包体共计占有 1+10+4+6+2+4+4+2+8 = 41 字节。
包头的数据组成:
版本号 v1.0 占 4 个字节,消息包体实际长度为 41 占 4 个字节表示,序列号 0001 占 4 个字节,校验码 32 位表示占 4 个字节。
包头共计占有 4+4+4+4 = 16 字节。
包尾的数据组成:
通过回车符标记结束 rn,占用 1 个字节。
整个包的序列化二进制字节流共 41+16+1 = 58 字节。这里讲解的是整个序列化的处理思路, 在实际的序列化处理中还要考虑更多细节,比如说方法和属性的区分,方法权限的标记,嵌套类型的处理等等。

  • 序列化的处理要素
  1. 解析效率:序列化协议应该首要考虑的因素,像 xml/json 解析起来比较耗时,需要解析 doom 树,二进制自定义协议解析起来效率要快很多。
  2. 压缩率:同样一个对象,xml/json 传输起来有大量的标签冗余信息,信息有效性低,二进制自定义协议占用的空间相对来说会小很多。
  3. 扩展性与兼容性:是否能够利于信息的扩展,并且增加字段后旧版客户端是否需要强制升级,这都是需要考虑的问题,在自定义二进制协议时候,要做好充分考虑设计。
  4. 可读性与可调试性:xml/json 的可读性会比二进制协议好很多,并且通过网络抓包可以直接读取,二进制则需要反序列化才能查看其内容。
  5. 跨语言:有些序列化协议是与开发语言紧密相关的,例如 dubbo 的 Hessian 序列化协议就只能支持 Java 的 RPC 调用。
  6. 通用性:xml/json 非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,二进制数据的处理方面也有 Protobuf 和 Hessian 等插件,在做设计的时候尽量做到较好的通用性。
  • 常用的序列化技术

    1. JDK 原生序列化

    代码:
...
 public static void main(String[] args) throws IOException, ClassNotFoundException {
 String basePath = "D:/TestCode";
 FileOutputStream fos = new FileOutputStream(basePath + "tradeUser.clazz");
 TradeUser tradeUser = new TradeUser();
 tradeUser.setName("Mirson");
 ObjectOutputStream oos = new ObjectOutputStream(fos);
 oos.writeObject(tradeUser);
 oos.flush();
 oos.close();
 FileInputStream fis = new FileInputStream(basePath + "tradeUser.clazz");
 ObjectInputStream ois = new ObjectInputStream(fis);
 TradeUser deStudent = (TradeUser) ois.readObject();
 ois.close();
 System.out.println(deStudent);
    }
...

(1) 在 Java 中,序列化必须要实现 java.io.Serializable 接口。

(2) 通过 ObjectOutputStream 和 ObjectInputStream 对象进行序列化及反序列化操作。

(3) 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(也就是在代码中定义的序列 ID private static final long serialVersionUID)。

(4) 序列化并不会保存静态变量。

(5) 要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。

(6) Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如基本类型 int 为 0,封装对象型 Integer 则为 null。

(7) 服务器端给客户端发送序列化对象数据并非加密的,如果对象中有一些敏感数据比如密码等,那么在对密码字段序列化之前,最好做加密处理, 这样可以一定程度保证序列化对象的数据安全。

2. JSON 序列化
一般在 HTTP 协议的 RPC 框架通信中,会选择 JSON 方式。
优势:JSON 具有较好的扩展性、可读性和通用性。
缺陷:JSON 序列化占用空间开销较大,没有JAVA的强类型区分,需要通过反射解决,解析效率和压缩率都较差。
如果对并发和性能要求较高,或者是传输数据量较大的场景,不建议采用 JSON 序列化方式。

3. Hessian2 序列化
Hessian 是一个动态类型,二进制序列化,并且支持跨语言特性的序列化框架。
Hessian 性能上要比 JDK、JSON 序列化高效很多,并且生成的字节数也更小。有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。
代码示例:

 ...
 TradeUser tradeUser = new TradeUser();
 tradeUser.setName("Mirson");
 
 //tradeUser对象序列化处理
 ByteArrayOutputStream bos = new ByteArrayOutputStream();
 Hessian2Output output = new Hessian2Output(bos);
 output.writeObject(tradeUser);
 output.flushBuffer();
 byte[] data = bos.toByteArray();
 bos.close();
 
 //tradeUser对象反序列化处理
 ByteArrayInputStream bis = new ByteArrayInputStream(data);
 Hessian2Input input = new Hessian2Input(bis);
 TradeUser deTradeUser = (TradeUser) input.readObject();
 input.close();
 
 System.out.println(deTradeUser);
  ...

Dubbo Hessian Lite 序列化流程:

Dubbo Hessian Lite 反序列化流程:

Hessian 自身也存在一些缺陷,大家在使用过程中要注意:

  • 对 Linked 系列对象不支持,比如 LinkedHashMap、LinkedHashSet 等,但可以通过CollectionSerializer 类修复。
  • Locale 类不支持,可以通过扩展 ContextSerializerFactory 类修复。
  • Byte/Short 在反序列化的时候会转成 Integer。

4. Protobuf 序列化
Protobuf 是 Google 推出的开源序列库,它是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等多种语言。
Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类,它具备以下优点:

  • 压缩比高,体积小,序列化后体积相比 JSON、Hessian 小很多;
  • 消息格式的扩展、升级和兼容性都不错,可以做到向后兼容。
  • 序列化反序列化速度很快,不需要通过反射获取类型;
  • IDL 能清晰地描述语义,可以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器;

代码示例:
Protobuf 脚本定义:

 // 定义Proto版本
 syntax = "proto3";
 // 是否允许生成多个JAVA文件
 option java_multiple_files = true;
 // 生成的包路径
 option java_package = "com.itcast.bulls.stock.struct.netty.trade";
 // 生成的JAVA类名
 option java_outer_classname = "TradeUserProto";
 
 
 // 预警通知消息体
 message TradeUser {
 
      
 int64 userId = 1 ;
 
      
 string userName = 2 ;
  }
 

代码操作:

 // 创建TradeUser的Protobuf对象
 TradeUserProto.TradeUser.Builder builder = TradeUserProto.TradeUser.newBuilder();
 builder.setUserId(101);
 builder.setUserName("Mirson");
 
 //将TradeUser做序列化处理
 TradeUserProto.TradeUser msg = builder.build();
 byte[] data = msg.toByteArray();
 
 //反序列化处理, 将刚才序列化的byte数组转化为TradeUser对象
 TradeUserProto.TradeUser deTradeUser = TradeUserProto.TradeUser.parseFrom(data);
 System.out.println(deTradeUser);
1.2 动态代理

  • 内部接口如何调用实现?
    RPC 的调用内部核心技术采用的就是动态代理。
  • JDK 动态代理的如何实现?
    实例代码:
public class JdkProxyTest {
​
 
 public interface User {
 String job();
    }
​
​
 
 public static class Teacher {
​
 public String invoke(){
 return "i'm Teacher";
        }
    }
​
​
 
 public static class JDKProxy implements InvocationHandler {
 private Object target;
​
 JDKProxy(Object target) {
 this.target = target;
        }
​
 @Override
 public Object invoke(Object proxy, Method method, Object[] paramValues) {
 return ((Teacher)target).invoke();
        }
    }
​
​
 public static void main(String[] args){
 // 构建代理器
 JDKProxy proxy = new JDKProxy(new Teacher());
 ClassLoader classLoader = ClassLoaderUtils.getClassLoader();
​
 // 生成代理类
 User user = (User) Proxy.newProxyInstance(classLoader, new Class[]{User.class}, proxy);
​
 // 接口调用
 System.out.println(user.job());
        }
}
​

JDK 动态代理的实现原理:

JDK 内部如何处理?
代理类 $Proxy 里面会定义相同签名的接口,然后内部会定义一个变量绑定 JDKProxy 代理对象,当调用User.job 接口方法,实质上调用的是 JDKProxy.invoke() 方法。

  • 为什么要加入动态代理?
    第一、 不便于管理,不利于扩展维护。
    第二、 可以做到拦截,添加其他额外功能。
  • 动态代理开源技术
    (1) Cglib 动态代理
    Cglib 是一个强大的、高性能的代码生成包。


(2) Javassist 动态代理
一个开源的分析、编辑和创建 Java 字节码的类库。
(3) Byte Buddy 字节码增强库
Byte Buddy是致力于解决字节码操作和简化操作复杂性的开源框架。

几种动态代理性能比较:
综合结果:
Byte Buddy > Javassist > CGLIB > JDK。

1.3 服务注册发现
  • 服务注册发现的作用
    感知服务端的变化,获取最新服务节点的连接信息。
  • 服务注册发现的处理流程

服务注册:服务提供方将对外暴露的接口发布到注册中心内,注册中心为了检测服务的有效状态,一般会建立双向心跳机制。
服务订阅:服务调用方去注册中心查找并订阅服务提供方的 IP,并缓存到本地用于后续调用。

  • 如何实现服务的注册发现
    基于 ZooKeeper 的服务发现方式:


A. 在 ZooKeeper 中创建一个服务根路径,可以根据接口名命名(例如:/micro/service/com.itcast.orderService),在这个路径再创建服务提供方与调用方目录(server、client),分别用来存储服务提供方和调用方的节点信息。
B. 服务端发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储注册信息。
C. 客户端发起订阅时,会在服务调用方目录中创建一个临时节点,节点中存储调用方的信息,同时 watch 服务提供方的目录(/micro/service/com.itcast.orderService/server)中所有的服务节点数据。当服务端产生变化时 ZK 就会通知给订阅的客户端。


ZooKeeper 方案的特点:
强一致性,ZooKeeper 集群的每个节点的数据每次发生更新操作,都会通知其它 ZooKeeper 节点同时执行更新。

1.4 健康监测
  • 为什么需要健康监测?
    比如网络中的波动,硬件设施的老化等等。可能造成集群当中的某个节点存在问题,无法正常调用。

  • 健康监测实现分析
    心跳检测的过程总共包含以下状态:
    • 健康状态
    • 波动状态
    • 失败状态

  • 完善的解决方案
    (1)阈值: 健康监测增加失败阈值记录。
    (2)成功率: 可以再追加调用成功率的记录(成功次数/总次数)。
    (3)探针: 对服务节点有一个主动的存活检测机制。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/838106.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号