【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 本身提供的序列化机制存在两个问题:
- 序列化的数据比较大,传输效率低
- 其他语言无法识别和对接
序列化框架选型的常用指标:
- 序列化的字节数据大小
- 序列化的速度和系统资源开销
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 {//序列化操作, 把一个对象转换为字节数组(二进制数组)<T> byte[] serialize(T obj);//反序列化操作, 把一个字节数组转换为对象<T> T deSerialize(byte[] data,Class<T> clazz);
}
- 基于 JDK 的方式实现序列化
public class JavaSerialize implements ISerializer {@Overridepublic <T> 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();}@Overridepublic <T> T deSerialize(byte[] data, Class<T> 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();@Testpublic 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();}}@Testpublic 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 环境搭建与操作
- 安装 IDEA 的插件 Protobuf Support
- 导入 Maven 插件
<!-- 1 定义属性 -->
<!-- Windows操作系统 -->
<os.detected.classifier>windows-x86_64</os.detected.classifier>
<!-- Mac操作系统 -->
<!-- <os.detected.classifier> osx-x86_64</os.detected.classifier> --><!-- https://github.com/trustin/os-maven-plugin -->
<!-- 2 导入依赖 -->
<dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>3.12.0</version>
</dependency><!-- 3 导入依赖插件 -->
<plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.6.1</version><configuration><protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact><pluginId>grpc-java</pluginId></configuration>
</plugin>
- 编写 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;
}
- 使用 Maven 插件生成 Java 模型对象
Protobuf 原理分析
数据传输方式:在 proto 中,数据的存储形式是 T - L - V 的数据存储方式:
- T:Tag 数据描述的标签头、数据类型、数据编号
- L:数据长度,可选值,对于变长的数据需要指定长度,比如字符串
- V:具体的数据值,有不同的编码方式
数据存储格式:
数据存储编码:
Varint 编码:对于整数 int 类型的数据采用 vint 进行编码,该编码的特点是:
对于一个 int 类型的数据,只保留有效的二进制位的数据
比如:100
-->00000000 00000000 00000010 00001000
-->00000010 00001000
对数据进行序列化:从低位到高位每次取 7 位,放入一个字节中,其中新的字节的最高位 1 表示还有数据,0 表示后面没有数据
00000010 00001000
-->10001000 00000100
二进制数据的高低位参考:二进制什么叫低位高位?
反序列化:
10001000 00000100
-->00000010 00001000
Tag 标签:Tag = (field_number << 3) | wire_type
Tag = (field_number << 3) | wire_typefield_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 dflcolfer 49 248 396 643 238 148protostuff 68 433 634 1067 239 150minified-json/dsl-platform 46 432 684 1116 353 197fst-flat-pre 53 501 675 1175 251 165json/dsl-platform 44 526 806 1332 485 261json-array/fastjson/databind 53 650 696 1345 281 163kryo-flat-pre 54 597 758 1355 212 132smile-col/jackson/databind 54 723 1082 1805 252 165msgpack/databind 54 796 1052 1848 233 146cbor-col/jackson/databind 54 732 1147 1879 251 165protobuf 121 1173 719 1891 239 149smile/jacksonafterburner/databind 54 913 1175 2088 352 252thrift-compact 97 1280 808 2088 240 148cbor/jackson+afterburner/databind 53 888 1239 2126 397 246flatbuffers 56 1417 758 2175 432 226thrift 95 1455 731 2186 349 197json-col/jackson/databind 53 887 1329 2216 293 178json/fastjson/databind 53 1058 1241 2299 486 262smile/jackson/databind 53 1011 1300 2311 338 241scala/sbinary 442 1311 1069 2381 255 147capnproto 55 1574 979 2553 400 204json/jackson+afterburner/databind 52 1094 1489 2584 485 261cbor/jackson/databind 53 1023 1561 2585 397 246json/protostuff-runtime 54 1353 1632 2986 469 243json/jackson/databind 54 1164 1866 3030 485 261json/jackson-jr/databind 53 1426 1962 3389 468 255xml/jackson/databind 54 2639 4720 7359 683 286json/gson/databind 56 4667 4403 9070 486 259bson/jackson/databind 54 4105 5449 9554 506 286xml/xstream+c 52 4383 9434 13817 487 244json/javax-tree/glassfish 1249 6818 10284 17102 485 263xml/exi-manual 54 11375 9891 21266 337 327java-built-in 53 5046 23279 28325 889 514scala/java-built-in 514 8280 36105 44385 1293 698json/protobuf 123 6630 56787 63417 488 253json/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:
- 代码:
/*** 需要注意的两点:* 1 对于接口必须继承Remote接口* 2 对于接口中的方法必须抛出RemoteException的异常**/
public interface IHello extends Remote {/*** 定义一个可以远程调用的方法* @param name 参数* @return* @throws RemoteException 必须标记有远程调用异常*/public String hello(String name) throws RemoteException;
}
rmi-server:
- pom.xml:
<dependencies><dependency><groupId>com.hesj.demo</groupId><artifactId>rmi-api</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
- 代码:
/** * 1 实现类* 注意: 实现类必须继承UnicastRemoteObject对象* 对于构造器 使用空参的构造器即可**/
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;}
}/** * 2 服务启动类**/
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
<dependencies><dependency><groupId>com.hesj.demo</groupId><artifactId>rmi-api</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
- 代码:
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 流程分析
- 启动服务端程序,暴露对应的端口和名称服务
- 启动客户端程序,通过对应的名称服务找到对应的代理对象
- 通过代理对象调用远程方法
- 对于调用参数和返回结果需要进行序列化操作
RMI 调用时序图:
RMI 类图 — 对象发布:
RMI 类图 — 远程调用:
Java RMI 源码分析
暂略…
【Java从0到架构师】分布式框架通信核心基础 - 序列化(JDK、Protobuf)、远程过程调用 RMI相关推荐
- 【Java从0到架构师】Linux 基础知识、常用命令
Linux 基础知识.常用命令 Linux 基础知识 内核和发行版 常见的 Linux 发行版 Linux 的应用领域 Linux 与 Windows 的区别 Linux 常用命令 *系统目录结构 s ...
- 【Java从0到架构师】git 核心原理和分支管理
git 核心原理和分支管理 核心原理 Git 数据存储结构 git add 流程 - 把数据添加到暂存区 git commit 流程 - 把数据提交到版本库 HEAD 关联关系处理 分支管理 常用命令 ...
- 【Java从0到架构师】Zookeeper 应用 - Java 客户端操作、服务器动态感知、分布式锁业务处理
分布式基石 Zookeeper 框架全面剖析 Java 客户端操作 Java 客户端 API 服务器的动态感知 服务注册 服务发现 分布式锁业务处理 单机环境(一个虚拟机中) 分布式环境_同名节点 分 ...
- 【Java从0到架构师】Zookeeper - 系统高可用、分布式的基本概念、Zookeeper 应用场景
分布式基石 Zookeeper 框架全面剖析 系统高可用 集群 - 主备集群.主从集群.普通集群 分布式(系统部署方式) 微服务(架构设计方式) 分布式的基本概念 分布式存储.分布式计算 分布式协调服 ...
- 【Java从0到架构师】交错的日志系统、SpringBoot 集成日志框架
交错的日志系统.SpringBoot 集成日志框架 交错复杂的日志系统① - 多个项目实现 SLF4J 门面 交错复杂的日志系统② - 统一底层实现为 Logback 交错复杂的日志系统③ - 统一底 ...
- 【Java从0到架构师】SpringCloud - Eureka、Ribbon、Feign
SpringCloud 分布式.微服务相关概念 微服务框架构选型 SpringCloud 概述 服务注册与发现 - Eureka 案例项目 Eureka 自我保护机制 微服务调用方式 - Ribbon ...
- 【Java从0到架构师】RocketMQ 基础 - 应用、核心组件、安装
RocketMQ 消息中间件 基础知识 消息中间件的应用 异步解耦 削峰填谷 消息分发 RocketMQ 核心组件 RocketMQ 安装 源码安装 修改配置参数 启动 管理控制台安装 测试项目 Ja ...
- 【Java从0到架构师】Dubbo 基础 - 设置启动时检查、直接提供者、线程模型、负载均衡、集群容错、服务降级
Dubbo 分布式 RPC 分布式核心基础 分布式概述 RPC Dubbo Dubbo 入门程序 - XML.注解 部署管理控制台 Dubbo Admin 修改绑定的注册 IP 地址 设置启动时检查 ...
- 【Java从0到架构师】Zookeeper - 安装、核心工作机制、基本命令
分布式基石 Zookeeper 框架全面剖析 Zookeeper 安装.配置.运行 Zookeeper 的核心工作机制 特性 数据结构.节点 基本操作命令 服务器的启动和监控 客户端连接 创建节点 查 ...
最新文章
- Linux命令之route - 显示和操作IP路由表
- 基于深度学习的物体抓取位置估计
- detime php_php试题及答案
- Android相机预览方向
- 如何理解卷积神经网络中的1*1卷积
- 含有负边的图的最短路径(Bellman_ford算法)
- 2019年“计算法学”夏令营即日起接收报名申请
- shiro框架_Shiro安全框架(下)
- Shell 命令大全Xhell入门
- 【 2013华为杯编程大赛成都第三组前两题试题及答案】
- ef6 mysql_VS2015 + EF6连接MYSQL5.6
- oracle11g32位安装流程_Oracle11g----Win7 32位安装图例
- fiddler如何伪造referrer_Fiddler抓包神器带你遨游网络,叱咤风云,为所欲为
- OCR应用场景:票总管发票管理系统
- 启动U盘更换背景图片和图标的方法
- MATLAB符号运算——极限
- java 九宫格数独,(完整)九宫格数独题目大全,推荐文档
- 贵阳市交通大数据中心
- 嵌入式STT-MRAM效应与流致反转
- android粘性广播何时结束,Android之粘性广播理解