分布式框架通信核心基础

  • 序列化
    • 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 {//序列化操作, 把一个对象转换为字节数组(二进制数组)<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 环境搭建与操作

  1. 安装 IDEA 的插件 Protobuf Support
  2. 导入 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>
  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 原理分析

数据传输方式:在 proto 中,数据的存储形式是 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_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 流程分析

  1. 启动服务端程序,暴露对应的端口和名称服务
  2. 启动客户端程序,通过对应的名称服务找到对应的代理对象
  3. 通过代理对象调用远程方法
  4. 对于调用参数和返回结果需要进行序列化操作

RMI 调用时序图:

RMI 类图 — 对象发布:

RMI 类图 — 远程调用:

Java RMI 源码分析

暂略…

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

  1. 【Java从0到架构师】Linux 基础知识、常用命令

    Linux 基础知识.常用命令 Linux 基础知识 内核和发行版 常见的 Linux 发行版 Linux 的应用领域 Linux 与 Windows 的区别 Linux 常用命令 *系统目录结构 s ...

  2. 【Java从0到架构师】git 核心原理和分支管理

    git 核心原理和分支管理 核心原理 Git 数据存储结构 git add 流程 - 把数据添加到暂存区 git commit 流程 - 把数据提交到版本库 HEAD 关联关系处理 分支管理 常用命令 ...

  3. 【Java从0到架构师】Zookeeper 应用 - Java 客户端操作、服务器动态感知、分布式锁业务处理

    分布式基石 Zookeeper 框架全面剖析 Java 客户端操作 Java 客户端 API 服务器的动态感知 服务注册 服务发现 分布式锁业务处理 单机环境(一个虚拟机中) 分布式环境_同名节点 分 ...

  4. 【Java从0到架构师】Zookeeper - 系统高可用、分布式的基本概念、Zookeeper 应用场景

    分布式基石 Zookeeper 框架全面剖析 系统高可用 集群 - 主备集群.主从集群.普通集群 分布式(系统部署方式) 微服务(架构设计方式) 分布式的基本概念 分布式存储.分布式计算 分布式协调服 ...

  5. 【Java从0到架构师】交错的日志系统、SpringBoot 集成日志框架

    交错的日志系统.SpringBoot 集成日志框架 交错复杂的日志系统① - 多个项目实现 SLF4J 门面 交错复杂的日志系统② - 统一底层实现为 Logback 交错复杂的日志系统③ - 统一底 ...

  6. 【Java从0到架构师】SpringCloud - Eureka、Ribbon、Feign

    SpringCloud 分布式.微服务相关概念 微服务框架构选型 SpringCloud 概述 服务注册与发现 - Eureka 案例项目 Eureka 自我保护机制 微服务调用方式 - Ribbon ...

  7. 【Java从0到架构师】RocketMQ 基础 - 应用、核心组件、安装

    RocketMQ 消息中间件 基础知识 消息中间件的应用 异步解耦 削峰填谷 消息分发 RocketMQ 核心组件 RocketMQ 安装 源码安装 修改配置参数 启动 管理控制台安装 测试项目 Ja ...

  8. 【Java从0到架构师】Dubbo 基础 - 设置启动时检查、直接提供者、线程模型、负载均衡、集群容错、服务降级

    Dubbo 分布式 RPC 分布式核心基础 分布式概述 RPC Dubbo Dubbo 入门程序 - XML.注解 部署管理控制台 Dubbo Admin 修改绑定的注册 IP 地址 设置启动时检查 ...

  9. 【Java从0到架构师】Zookeeper - 安装、核心工作机制、基本命令

    分布式基石 Zookeeper 框架全面剖析 Zookeeper 安装.配置.运行 Zookeeper 的核心工作机制 特性 数据结构.节点 基本操作命令 服务器的启动和监控 客户端连接 创建节点 查 ...

最新文章

  1. Linux命令之route - 显示和操作IP路由表
  2. 基于深度学习的物体抓取位置估计
  3. detime php_php试题及答案
  4. Android相机预览方向
  5. 如何理解卷积神经网络中的1*1卷积
  6. 含有负边的图的最短路径(Bellman_ford算法)
  7. 2019年“计算法学”夏令营即日起接收报名申请
  8. shiro框架_Shiro安全框架(下)
  9. Shell 命令大全Xhell入门
  10. 【 2013华为杯编程大赛成都第三组前两题试题及答案】
  11. ef6 mysql_VS2015 + EF6连接MYSQL5.6
  12. oracle11g32位安装流程_Oracle11g----Win7 32位安装图例
  13. fiddler如何伪造referrer_Fiddler抓包神器带你遨游网络,叱咤风云,为所欲为
  14. OCR应用场景:票总管发票管理系统
  15. 启动U盘更换背景图片和图标的方法
  16. MATLAB符号运算——极限
  17. java 九宫格数独,(完整)九宫格数独题目大全,推荐文档
  18. 贵阳市交通大数据中心
  19. 嵌入式STT-MRAM效应与流致反转
  20. android粘性广播何时结束,Android之粘性广播理解

热门文章

  1. 普通人有必要学新媒体吗?
  2. 手机开启热点给其他设备上网和用插卡随身路由给其他设备上网有何区别呢?
  3. 手机运行内存6+128跟8+128有什么区别?
  4. go net/http包
  5. c语言全局变量默认值
  6. java mysql结果集_Java JDBC结果集的处理
  7. SQL OUTER JOIN概述和示例
  8. 什么是SQL Server事务日志中的虚拟日志文件?
  9. Scrapy框架的介绍和基本使用
  10. python-元组,列表,字典常用方法