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

【Java从0到架构师】分布式框架通信核心基础 - 序列化(JDK、Protobuf)、远程过程调用 RMI

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

【Java从0到架构师】分布式框架通信核心基础 - 序列化(JDK、Protobuf)、远程过程调用 RMI

分布式框架通信核心基础
  • 序列化
    • JDK 的序列化
      • JDK 序列化的一些细节
    • Protobuf 序列化
      • Protobuf 环境搭建与操作
      • Protobuf 原理分析
      • 实际数据传输
    • 序列化技术选型
  • 远程过程调用 RMI
    • RPC
    • Java RMI 应用实战 rmi-api、rmi-server、rmi-client
    • Java RMI 流程分析
    • Java RMI 源码分析

Java 从 0 到架构师目录:【Java从0到架构师】学习记录

序列化

在 JVM 创建的对象是在内存当中的,当 JVM 停止运行,释放内存以后,JVM 内存中的对象也会被销毁。但是在有些场景下,我们需要把对象的数据持久化保存起来,就需要使用对应的序列化和反序列化技术。

  • 序列化:把内存中的对象信息转化为字节数组的过程
    序列化的目的:数据持久化,数据的网络传输
  • 反序列化:序列化的逆向操作,把字节数组转换为对象的过程

Java 语言本身提供了对象序列化机制,也是 Java 语言本身最重要的底层机制之一。Java 本身提供的序列化机制存在两个问题:

  1. 序列化的数据比较大,传输效率低
  2. 其他语言无法识别和对接

序列化框架选型的常用指标:

  • 序列化的字节数据大小
  • 序列化的速度和系统资源开销
JDK 的序列化

参考:【Java I/O流】File、字符集、字节流、字符流、缓冲流、数据流、对象流、序列化

对于 JDK 的序列化对象一定要实现 Serializable 接口

实际操作:

  • 定义序列化对象
@Data
public class User implements Serializable {
    private static final long serialVersionUID = -9212613021140645522L;
    private String name;
    private Integer age;
}
  • 定义接口
// 定义一个序列化和反序列化的操作
public interface ISerializer {
    //序列化操作, 把一个对象转换为字节数组(二进制数组)
     byte[] serialize(T obj);
    //反序列化操作, 把一个字节数组转换为对象
     T deSerialize(byte[] data,Class clazz);
}
  • 基于 JDK 的方式实现序列化
public class JavaSerialize implements ISerializer {
    @Override
    public  byte[] serialize(T obj) {
        //字节输出流
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("序列化错误", e);
        }
        return bos.toByteArray();
    }

    @Override
    public  T deSerialize(byte[] data, Class clazz) {
        ByteArrayInputStream bis = new ByteArrayInputStream(data);
        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(bis);
            return (T)objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("反序列化错误",e);
        }
    }
}
  • 测试
public class JavaSerializeTest {

    ISerializer serializer = new JavaSerialize();
    
    @Test
    public void testSerialize() throws Exception {
        User user = new User();
        user.setAge(18);
        user.setName("wolf");
        byte[] datas = serializer.serialize(user);
        System.out.println("序列化大小:" + datas.length);
		// 保存到文件
        try (FileOutputStream fos = new FileOutputStream("user.dat")){
            fos.write(datas);
            fos.flush();
        }
    }

    @Test
    public void testDeserialize() throws Exception {
        try (FileInputStream fis = new FileInputStream("user.dat")){
            byte[] buffer = new byte[1024];
            int len = -1;
            if((len = fis.read(buffer)) != -1) {
                buffer = Arrays.copyOf(buffer, len);
                User u = serializer.deSerialize(buffer, User.class);
                System.out.println("u = " + u);
            }
        }
    }
}
JDK 序列化的一些细节

serialVersionUID:Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。如果不指定 serialVersionUID,序列化某个类到本地以后,修改了类的一些属性,则反序列化就会失败,因为由于数据变更,自动计算出来的 serialVersionUID 也不一样了。

静态变量不会序列化:序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。

父类的序列化:

  • 一个子类实现了 Serializable 接口,若它的父类没有实现 Serializable,那么对于父类中的变量不会实现序列化操作。
  • 如果一个父类实现了 Serializabla 接口,子类可以不用实现 Serializable,也会对对象的属性进行序列化操作

transient:声明为 transient 的字段不会进行序列化,对于敏感信息可以声明为 transient

对象的克隆:在 Java 中存在一个 Cloneable 接口,实现这个接口的类都会具备 clone 的能力(clone 是在内存中进行),在性能方面会比直接通过 new 生成对象更好。在 Java 中,克隆分为深度克隆和浅克隆。

  • 浅克隆:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。
// 1 实现 Cloneable 接口
// 2 实现 clone 方法, 修改方法权限为 public
@Override
public Boy clone() throws CloneNotSupportedException {
    return (Boy) super.clone();
}
  • 深度克隆:克隆出来的对象都是不一样的,引用的对象类型也是新的引用
// 1 所有对象都实现序列化的接口
// 2 自定义一个深度克隆方法deepClone, 通过字节数组流和对象流的方式实现对象的深度拷贝
public Boy deepClone() throws Exception {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
        oos.writeObject(this);
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (Boy) ois.readObject();
    }
}
Protobuf 序列化

Protobuf 是 Google 的一种数据交换格式,它独立于语言、独立于平台。Google 提供了多种语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件。

  • Protobuf 空间开销小、性能好,非常适合用于对性能要求高的 RPC 调用
  • 由于解析性能好,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中

但是使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器。

Protobuf 的特点:

  • 处理速度快:编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
  • 数据体积小:采用了独特的编码方式,如 Varint、Zigzag 编码方式等等
  • 兼容性高:采用 T - L - V 的数据存储方式
Protobuf 环境搭建与操作
  1. 安装 IDEA 的插件 Protobuf Support
  2. 导入 Maven 插件

windows-x86_64






    com.google.protobuf
    protobuf-java
    3.12.0




	org.xolstice.maven.plugins
	protobuf-maven-plugin
	0.6.1
	
		
		com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}
		
		grpc-java
	

  1. 编写 proto 文件:在 main/proto 目录下创建对应的资源
// 使用的协议版本 proto3 proto2
syntax = "proto3";
// 生成的Java类的包
option java_package = "com.hesj.demo.serdemo.domain";
// 创建一个文件名 PersonModel
option java_outer_classname = "PersonModel";

// 具体需要序列化的类
message Person {
    // 对于后面的数字是唯一的一个编号
    string name = 1;
    int32 age= 2;
}
  1. 使用 Maven 插件生成 Java 模型对象
Protobuf 原理分析

数据传输方式:在 prot o中,数据的存储形式是 T - L - V 的数据存储方式:

  • T:Tag 数据描述的标签头、数据类型、数据编号
  • L:数据长度,可选值,对于变长的数据需要指定长度,比如字符串
  • V:具体的数据值,有不同的编码方式

数据存储格式

数据存储编码

Varint 编码:对于整数 int 类型的数据采用 vint 进行编码,该编码的特点是:

  1. 对于一个 int 类型的数据,只保留有效的二进制位的数据
    比如:100 --> 00000000 00000000 00000010 00001000 --> 00000010 00001000

  2. 对数据进行序列化:从低位到高位每次取 7 位,放入一个字节中,其中新的字节的最高位 1 表示还有数据,0 表示后面没有数据
    00000010 00001000 --> 10001000 00000100

    二进制数据的高低位参考:二进制什么叫低位高位?

  3. 反序列化:10001000 00000100 --> 00000010 00001000

Tag 标签:Tag = (field_number << 3) | wire_type

Tag = (field_number << 3) | wire_type
	field_number: 字段的编号(唯一的)
	wire_type: proto内置定义好的, 对于不同的数据类型, 有不同的wire_type
message Person {
    string name = 1;
    int32 age = 2;
}

对于name来说, field_number = 1, wire_type = 2
则 tag_name = (0000 0001 << 3) | 0000 0010 
= 0000 1000 | 0000 0010
= 0000 1010 = 9

对于age来说, field_number = 2, wire_type = 0
则 tag_age = (0000 0010 << 3) | 0000 0000 
= 0001 0000 | 0000 0000
= 0001 0000 = 16

Zigzag 编码(拓展):在对负数进行编码的时候,如果是使用 vint 编码的话,对于一个负数编码,可能会占用更多的空间,比如 -2 对应的二进制 1111 1111 1111 1111 1111 1111 1111 1110

  • proto 中的解决方案是:通过 Zigzag 的编码方式将 -2 转换为对应的正整数存储,再去进行 vint 编码
  • 转换规则:略…
实际数据传输
数据格式:
message Person {
    string name = 1;
    int32 age = 2;
}
# 对数据进行编码存储
name字段是字符串, 数据格式是 Tag + Length + Value
- Tag: (1 << 3) | 2 ==> 10 ==> 0000 1010, 1个字节
- Length: utf编码, 4个字节 ==> 0000 0100, 1个字节
- Value: hesj ==> 104 101 115 106 ==> 01101000 01100101 01110011 01101010, 4个字节
Tag + Length + Value 共6字节

age字段是 int32, 数据格式是 Tag + Value
- Tag: (2<< 3) | 0 ==> 16 ==> 00010000, 1个字节
- Value: 520, 采用vint编码, 10001000 00000100, 2个字节
Tag + Value 共3字节

# 因此传输Person序列化后所占的字节是9字节

序列化技术选型

常见的序列化方式:https://github.com/eishay/jvm-serializers/wiki

选取原则:

  • 序列化的数据大小
  • 序列化的处理速度
  • 是否支持跨平台,跨语言
  • 可扩展性和兼容性
  • 技术的流行度
  • 学习的难度
                                   create     ser   deser   total   size  dfl

colfer                                 49     248     396     643    238   148

protostuff                             68     433     634    1067    239   150

minified-json/dsl-platform             46     432     684    1116    353   197

fst-flat-pre                           53     501     675    1175    251   165

json/dsl-platform                      44     526     806    1332    485   261

json-array/fastjson/databind           53     650     696    1345    281   163

kryo-flat-pre                          54     597     758    1355    212   132

smile-col/jackson/databind             54     723    1082    1805    252   165

msgpack/databind                       54     796    1052    1848    233   146

cbor-col/jackson/databind              54     732    1147    1879    251   165

protobuf                              121    1173     719    1891    239   149

smile/jacksonafterburner/databind     54     913    1175    2088    352   252

thrift-compact                         97    1280     808    2088    240   148

cbor/jackson+afterburner/databind      53     888    1239    2126    397   246

flatbuffers                            56    1417     758    2175    432   226

thrift                                 95    1455     731    2186    349   197

json-col/jackson/databind              53     887    1329    2216    293   178

json/fastjson/databind                 53    1058    1241    2299    486   262

smile/jackson/databind                 53    1011    1300    2311    338   241

scala/sbinary                         442    1311    1069    2381    255   147

capnproto                              55    1574     979    2553    400   204

json/jackson+afterburner/databind      52    1094    1489    2584    485   261

cbor/jackson/databind                  53    1023    1561    2585    397   246

json/protostuff-runtime                54    1353    1632    2986    469   243

json/jackson/databind                  54    1164    1866    3030    485   261

json/jackson-jr/databind               53    1426    1962    3389    468   255

xml/jackson/databind                   54    2639    4720    7359    683   286

json/gson/databind                     56    4667    4403    9070    486   259

bson/jackson/databind                  54    4105    5449    9554    506   286

xml/xstream+c                          52    4383    9434   13817    487   244

json/javax-tree/glassfish            1249    6818   10284   17102    485   263

xml/exi-manual                         54   11375    9891   21266    337   327

java-built-in                          53    5046   23279   28325    889   514

scala/java-built-in                   514    8280   36105   44385   1293   698

json/protobuf                         123    6630   56787   63417    488   253

json/json-lib/databind                 61   19853   71969   91822    485   263
远程过程调用 RMI RPC

RPC (Remote Procedure Call) 远程过程调用,简单的理解是一个服务请求另一个服务提供的服务

本地过程调用:就是在同一个 JVM 中,直接调用本地的方法

常见的 RPC 框架:

  • RMI (JRMP):纯 Java 的 RPC 框架
  • SOAP (webservice)
  • gRPC
  • Dubbo
  • SpringCloud

为什么需要 RPC 远程调用:

Java RMI 应用实战 rmi-api、rmi-server、rmi-client

业务流程:

rmi-api

  • 代码:
public interface IHello extends Remote {
    
    public String hello(String name) throws RemoteException;
}

rmi-server

  • pom.xml:

    
        com.hesj.demo
        rmi-api
        1.0-SNAPSHOT
    

  • 代码:
public class HelloImpl extends UnicastRemoteObject implements IHello {

    public HelloImpl() throws RemoteException {
        super();
    }

    public String hello(String name) throws RemoteException   {
        System.out.println("name = " + name);
        return "你好:" + name;
    }
}


public class AppServer {
    public static void main(String[] args) throws Exception {
        // 发布Hello的远程调用
        IHello hello = new HelloImpl(); 
        // 创建一个注册监听, 使用远程调用方法
        LocateRegistry.createRegistry(8888);
        // 注册方法, 类似注册中心
        Naming.bind("rmi://127.0.0.1:8888/hello", hello);
        System.out.println("服务AppServer启动成功");
    }
}

rmi-client

  • pom.xml

    
        com.hesj.demo
        rmi-api
        1.0-SNAPSHOT
    

  • 代码:
public class AppClient {
    public static void main(String[] args) throws Exception {
        //获取远程调用的代理对象
        IHello hello = (IHello) Naming.lookup("rmi://192.168.48.1:8888/hello");
        // 调用远程方法
        String result = hello.hello("hesj");
        System.out.println("result = " + result);
    }
}
Java RMI 流程分析
  1. 启动服务端程序,暴露对应的端口和名称服务
  2. 启动客户端程序,通过对应的名称服务找到对应的代理对象
  3. 通过代理对象调用远程方法
  4. 对于调用参数和返回结果需要进行序列化操作

RMI 调用时序图:

RMI 类图 — 对象发布:

RMI 类图 — 远程调用:

Java RMI 源码分析

暂略…

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

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

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