Simple RPC - 02 通用高性能序列化和反序列化设计与实现
文章目录
- 概述
- 设计实现
- 通用的序列化接口
- 通用的序列化实现【推荐】 vs 专用的序列化实现
- 专用序列化接口定义
- 序列化实现
概述
网络传输和序列化这两部分的功能相对来说是非常通用并且独立的,在设计的时候,只要能做到比较好的抽象,这两部的实现,它的通用性是非常强的。不仅可以用于 RPC 框架中,同样可以直接拿去用于实现消息队列,或者其他需要互相通信的分布式系统中。
我们先来实现序列化和反序列化部分,因为后面的部分会用到序列化和反序列化。
设计实现
通用的序列化接口
首先我们需要设计一个可扩展的,通用的序列化接口,为了方便使用,我们直接使用静态类的方式来定义这个接口(严格来说这并不是一个接口)
public class SerializeSupport {public static <E> E parse(byte [] buffer) {// ...}public static <E> byte [] serialize(E entry) {// ...}
}
- parse 方法用于反序列化
- serialize 方法用于序列化
比如
// 序列化
MyClass myClassObject = new MyClass();
byte [] bytes = SerializeSupport.serialize(myClassObject);
// 反序列化
MyClass myClassObject1 = SerializeSupport.parse(bytes);
通用的序列化实现【推荐】 vs 专用的序列化实现
在讲解序列化和反序列化的时候说过,可以使用通用的序列化实现,也可以自己来定义专用的序列化实现。
- 专用的序列化性能最好,但缺点是实现起来比较复杂,你要为每一种类型的数据专门编写序列化和反序列化方法。
- 一般的 RPC 框架采用的都是通用的序列化实现,比如 gRPC 采用的是 Protobuf 序列化实现,Dubbo 支持 hession2 等好几种序列化实现
为什么这些 RPC 框架不像消息队列一样,采用性能更好的专用的序列化实现呢?这个原因很简单,消息队列它需要序列化数据的类型是固定的,只是它自己的内部通信的一些命令。但 RPC 框架,它需要序列化的数据是,用户调用远程方法的参数,这些参数可能是各种数据类型,所以必须使用通用的序列化实现,确保各种类型的数据都能被正确的序列化和反序列化。
我们这里还是采用专用的序列化实现,主要的目的是一起来实践一下,如何来实现序列化和反序列化
专用序列化接口定义
public interface Serializer<T> {/*** 计算对象序列化后的长度,主要用于申请存放序列化数据的字节数组* @param entry 待序列化的对象* @return 对象序列化后的长度*/int size(T entry);/*** 序列化对象。将给定的对象序列化成字节数组* @param entry 待序列化的对象* @param bytes 存放序列化数据的字节数组* @param offset 数组的偏移量,从这个位置开始写入序列化数据* @param length 对象序列化后的长度,也就是{@link Serializer#size(java.lang.Object)}方法的返回值。*/void serialize(T entry, byte[] bytes, int offset, int length);/*** 反序列化对象* @param bytes 存放序列化数据的字节数组* @param offset 数组的偏移量,从这个位置开始写入序列化数据* @param length 对象序列化后的长度* @return 反序列化之后生成的对象*/T parse(byte[] bytes, int offset, int length);/*** 用一个字节标识对象类型,每种类型的数据应该具有不同的类型值*/byte type();/*** 返回序列化对象类型的Class对象。*/Class<T> getSerializeClass();
}
这个接口中,除了 serialize 和 parse 这两个序列化和反序列化两个方法以外,还定义了下面这几个方法:
- size 方法计算序列化之后的数据长度,用于事先来申请存放序列化数据的字节数组;
- type 方法定义每种序列化实现的类型,这个类型值也会写入到序列化之后的数据中,主要的作用是在反序列化的时候,能够识别是什么数据类型的,以便找到对应的反序列化实现类;
- getSerializeClass 这个方法返回这个序列化实现类对应的对象类型,目的是,在执行序列化的时候,通过被序列化的对象类型找到对应序列化实现类
序列化实现
利用这个 Serializer 接口,我们就可以来实现 SerializeSupport 这个支持任何对象类型序列化的通用静态类了。
首先我们定义两个 Map,这两个 Map 中存放着所有实现 Serializer 接口的序列化实现类
private static Map<Class<?>/*序列化对象类型*/, Serializer<?>/*序列化实现*/> serializerMap = new HashMap<>();
private static Map<Byte/*序列化实现类型*/, Class<?>/*序列化对象类型*/> typeMap = new HashMap<>();
- serializerMap 中的 key 是序列化实现类对应的序列化对象的类型,它的用途是在序列化的时候,通过被序列化的对象类型,找到对应的序列化实现类。
- typeMap 的作用和 serializerMap 是类似的,它的 key 是序列化实现类的类型,用于在反序列化的时候,从序列化的数据中读出对象类型,然后找到对应的序列化实现类
理解了这两个 Map 的作用,实现序列化和反序列化这两个方法就很容易了。这两个方法的实现思路是一样的,都是通过一个类型在这两个 Map 中进行查找,查找的结果就是对应的序列化实现类的实例,也就是 Serializer 接口的实现,然后调用对应的序列化或者反序列化方法就可以了。
public class SerializeSupport {private static final Logger logger = LoggerFactory.getLogger(SerializeSupport.class);private static Map<Class<?>/*序列化对象类型*/, Serializer<?>/*序列化实现*/> serializerMap = new HashMap<>();private static Map<Byte/*序列化实现类型*/, Class<?>/*序列化对象类型*/> typeMap = new HashMap<>();static {for (Serializer serializer : ServiceSupport.loadAll(Serializer.class)) {registerType(serializer.type(), serializer.getSerializeClass(), serializer);logger.info("Found serializer, class: {}, type: {}.",serializer.getSerializeClass().getCanonicalName(),serializer.type());}}private static byte parseEntryType(byte[] buffer) {return buffer[0];}private static <E> void registerType(byte type, Class<E> eClass, Serializer<E> serializer) {serializerMap.put(eClass, serializer);typeMap.put(type, eClass);}@SuppressWarnings("unchecked")private static <E> E parse(byte [] buffer, int offset, int length, Class<E> eClass) {Object entry = serializerMap.get(eClass).parse(buffer, offset, length);if (eClass.isAssignableFrom(entry.getClass())) {return (E) entry;} else {throw new SerializeException("Type mismatch!");}}public static <E> E parse(byte [] buffer) {return parse(buffer, 0, buffer.length);}private static <E> E parse(byte[] buffer, int offset, int length) {byte type = parseEntryType(buffer);@SuppressWarnings("unchecked")Class<E> eClass = (Class<E> )typeMap.get(type);if(null == eClass) {throw new SerializeException(String.format("Unknown entry type: %d!", type));} else {return parse(buffer, offset + 1, length - 1,eClass);}}public static <E> byte [] serialize(E entry) {@SuppressWarnings("unchecked")Serializer<E> serializer = (Serializer<E>) serializerMap.get(entry.getClass());if(serializer == null) {throw new SerializeException(String.format("Unknown entry class type: %s", entry.getClass().toString()));}byte [] bytes = new byte [serializer.size(entry) + 1];bytes[0] = serializer.type();serializer.serialize(entry, bytes, 1, bytes.length - 1);return bytes;}
}
所有的 Serializer 的实现类是怎么加载到 SerializeSupport 的那两个 Map 中的呢?这里面利用了 Java 的一个 SPI 类加载机制
public class ServiceSupport {private final static Map<String, Object> singletonServices = new HashMap<>();public synchronized static <S> S load(Class<S> service) {return StreamSupport.stream(ServiceLoader.load(service).spliterator(), false).map(ServiceSupport::singletonFilter).findFirst().orElseThrow(ServiceLoadException::new);}public synchronized static <S> Collection<S> loadAll(Class<S> service) {return StreamSupport.stream(ServiceLoader.load(service).spliterator(), false).map(ServiceSupport::singletonFilter).collect(Collectors.toList());}@SuppressWarnings("unchecked")private static <S> S singletonFilter(S service) {if(service.getClass().isAnnotationPresent(Singleton.class)) {String className = service.getClass().getCanonicalName();Object singletonInstance = singletonServices.putIfAbsent(className, service);return singletonInstance == null ? service : (S) singletonInstance;} else {return service;}}
}
到这里,我们就封装好了一个通用的序列化的接口,
-
对于使用序列化的模块来说,它只要依赖 SerializeSupport 这个静态类,调用它的序列化和反序列化方法就可以了,不需要依赖任何序列化实现类。
-
对于序列化实现的提供者来说,也只需要依赖并实现 Serializer 这个接口就可以了。
比如,我们的 HelloService 例子中的参数是一个 String 类型的数据,我们需要实现一个支持 String 类型的序列化实现
public class StringSerializer implements Serializer<String> {@Overridepublic int size(String entry) {return entry.getBytes(StandardCharsets.UTF_8).length;}@Overridepublic void serialize(String entry, byte[] bytes, int offset, int length) {byte [] strBytes = entry.getBytes(StandardCharsets.UTF_8);System.arraycopy(strBytes, 0, bytes, offset, strBytes.length);}@Overridepublic String parse(byte[] bytes, int offset, int length) {return new String(bytes, offset, length, StandardCharsets.UTF_8);}@Overridepublic byte type() {return Types.TYPE_STRING;}@Overridepublic Class<String> getSerializeClass() {return String.class;}
}
在把 String 和 byte 数组做转换的时候,一定要指定编码方式,确保序列化和反序列化的时候都使用一致的编码,我们这里面统一使用 UTF8 编码。否则,如果遇到执行序列化和反序列化的两台服务器默认编码不一样,就会出现乱码。我们在开发过程用遇到的很多中文乱码问题,绝大部分都是这个原因
还有一个更复杂的序列化实现 MetadataSerializer,用于将注册中心的数据持久化到文件中
/*** Size of the map 2 bytes* Map entry:* Key string:* Length: 2 bytes* Serialized key bytes: variable length* Value list* List size: 2 bytes* item(URI):* Length: 2 bytes* serialized uri: variable length* item(URI):* ...* Map entry:* ...**/
public class MetadataSerializer implements Serializer<Metadata> {@Overridepublic int size(Metadata entry) {return Short.BYTES + // Size of the map 2 bytesentry.entrySet().stream().mapToInt(this::entrySize).sum();}@Overridepublic void serialize(Metadata entry, byte[] bytes, int offset, int length) {ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, length);buffer.putShort(toShortSafely(entry.size()));entry.forEach((k,v) -> {byte [] keyBytes = k.getBytes(StandardCharsets.UTF_8);buffer.putShort(toShortSafely(keyBytes.length));buffer.put(keyBytes);buffer.putShort(toShortSafely(v.size()));for (URI uri : v) {byte [] uriBytes = uri.toASCIIString().getBytes(StandardCharsets.UTF_8);buffer.putShort(toShortSafely(uriBytes.length));buffer.put(uriBytes);}});}private int entrySize(Map.Entry<String, List<URI>> e) {// Map entry:return Short.BYTES + // Key string length: 2 bytese.getKey().getBytes().length + // Serialized key bytes: variable lengthShort.BYTES + // List size: 2 bytese.getValue().stream() // Value list.mapToInt(uri -> {return Short.BYTES + // Key string length: 2 bytesuri.toASCIIString().getBytes(StandardCharsets.UTF_8).length; // Serialized key bytes: variable length}).sum();}@Overridepublic Metadata parse(byte[] bytes, int offset, int length) {ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, length);Metadata metadata = new Metadata();int sizeOfMap = buffer.getShort();for (int i = 0; i < sizeOfMap; i++) {int keyLength = buffer.getShort();byte [] keyBytes = new byte [keyLength];buffer.get(keyBytes);String key = new String(keyBytes, StandardCharsets.UTF_8);int uriListSize = buffer.getShort();List<URI> uriList = new ArrayList<>(uriListSize);for (int j = 0; j < uriListSize; j++) {int uriLength = buffer.getShort();byte [] uriBytes = new byte [uriLength];buffer.get(uriBytes);URI uri = URI.create(new String(uriBytes, StandardCharsets.UTF_8));uriList.add(uri);}metadata.put(key, uriList);}return metadata;}@Overridepublic byte type() {return Types.TYPE_METADATA;}@Overridepublic Class<Metadata> getSerializeClass() {return Metadata.class;}private short toShortSafely(int v) {assert v < Short.MAX_VALUE;return (short) v;}
}
到这里序列化的部分就实现完成了。我们这个序列化的实现,对外提供服务的就只有一个 SerializeSupport 静态类,并且可以通过扩展支持序列化任何类型的数据,这样一个通用的实现,不仅可以用在我们这个 RPC 框架的例子中,完全可以把这部分直接拿过去用在业务代码中
相关文章:

Simple RPC - 02 通用高性能序列化和反序列化设计与实现
文章目录 概述设计实现通用的序列化接口通用的序列化实现【推荐】 vs 专用的序列化实现专用序列化接口定义序列化实现 概述 网络传输和序列化这两部分的功能相对来说是非常通用并且独立的,在设计的时候,只要能做到比较好的抽象,这两部的实现…...

简单秒表设计仿真verilog跑表,源码/视频
名称:简单秒表设计仿真 软件:Quartus 语言:Verilog 代码功能: 秒表显示最低计时为10ms,最大为59:99,超出返回00:00 具有复位、启动、暂停三个按键 四个数码管分别显示4个时间数字。 演示…...

【发布】Photoshop ICO 文件格式插件 3.0
备注:本文原文首发于博客园: https://www.cnblogs.com/hoodlum1980/p/17766287.html 【简介】 Photoshop ICO 插件是为 Photoshop 开发的功能扩展插件,使得 Photoshop 可以直接读写 ICO 格式文件。由于 Photoshop 具有强大的像素位图编辑功…...

负载均衡、代理和动静分离的战略
一、Nginx简介 1.1 概述 Nginx (“engine x”) 是一个高性能的 HTTP 和 反向代理服务器,特点是占有内存少,并发能力强,能经受高负载的考验,有报告表明能支持高达 50,000 个并发连接数 。 1.2正向代理与反向代理 1.2.1正向代理 正向代理:如果把局域网外的 Internet 想象…...

Gitlab用户角色权限Guest、Reporter、Developer、Master、Owner
Gitlab用户在组中有角色权限:Guest、Reporter、Developer、Master、Owner Gitlab权限管理 Guest:可以创建issue、发表评论,不能读写版本库 Reporter:可以克隆代码,不能提交,QA、PM可以赋予这个权限 Deve…...

C#上位机序列9: 批量读写+事件广播+数据类型处理
一、源码结构: 二、运行效果: 三、源码解析 1. 读取配置文件及创建变量信息(点位名称,地址,数据类型(bool/short/int/float/long/double)) 2. 异步任务处理:读任务&…...

科技资讯|2023全球智能手表预估出货1.3亿块,智能穿戴提升AI功能
根据集邦咨询公布的最新报告,受全球经济低迷影响,2023 年全球智能手表出货量预估为 1.3 亿块。苹果以超过 30% 的份额领先,其次是三星(接近 10%)、华为、Garmin、Fitbit 等。 报告认为苹果、三星和华为等主要智能手表…...

技术架构之术
架构特征 1、结构性特征 易理解、可复用、可移植、可扩展、可配置、可维护、可测试 2、运行时特征 可靠性、稳定性、高安全、可伸缩、易用性、可用性、高性能、可观测 3、交付性特征 高效率、高适配、标准化、灵活性、易定制、统一性、开放性 如何开展我们的架构工作 价值分…...

【自用重要】概率论中θ和θ尖的区别【计算时的一般方法】
θ就相当于x,是一个值。 θ尖就相当于X,是一个量。 在做分布函数的时候,最好把θ尖换成Z的形式,因为他们都是量,这样比较好看。 在做不等式的时候,一般把量放在中间进行计算,因为随机变量有分…...

Redis设计与实现笔记 - 数据结构篇
Redis设计与实现笔记 - 数据结构篇 相信在我们日常使用中,会经常跟 Redis 打交道。数据结构 String、Hash、List、Set 和 ZSet 都是常用的数据类型。对于使用场景,我们可以滔滔不绝地说很多,但是我们从来就没有关心过它们的底层实现…...

线性代数-Python-01:向量的基本运算 -手写Vector -学习numpy的基本用法
文章目录 代码目录结构Vector.py_globals.pymain_vector.pymain_numpy_vector.py 一、创建属于自己的向量1.1 在控制台测试__repr__和__str__方法1.2 创建实例测试代码 二、向量的基本运算2.1 加法2.2 数量乘法2.3 向量运算的基本性质2.4 零向量2.5 向量的长度2.6 单位向量2.7 …...

数字图像处理实验记录二(直方图和直方图均衡化)
文章目录 一、基础知识1,什么是直方图2,直方图有什么用3,直方图均衡化4、原理代码实现 二、实验要求任务1:任务2: 三、实验记录任务1:任务2: 四、结果展示任务1:任务2: 五…...

大数据Flink(九十九):SQL 函数的解析顺序和系统内置函数
文章目录 SQL 函数的解析顺序和系统内置函数 一、SQL 函数...

TODO Vue typescript forEach的bug,需要再核實
forEach 一個string[],只有最後一個匹配條件有效,其它條件無效。 所以,只能替換成普通的for循環。 console.log(taskList)// for (const _task of taskList.value) {// if (_task invoiceSendEmail) {// form.value.invoiceSendEmail…...

简记一个错误
简记一个Flutter错误: Using hardware rendering with device sdk gphone64 x86 64. If you notice graphics artifacts, consider enabling software rendering with “–enable-software-rendering”. Launching lib\main.dart on sdk gphone64 x86 64 in debug …...

第四次作业
1.打印各种图形 A.矩形 a int(input("请输入行数: ")) i 0 while i < a:print("*"*10)i1 结果: B.直角三角形 a int(input("请输入行数: ")) i 0 while i<a:print("*"*(i1))i1 结果: C.反直角三角形 …...

面试问题整理总结
1.自我介绍 2.为什么想转测试 想换一个方向,测试开发在一定程度上也是属于开发,而且站在测试的角度能看到全局的东西更多,对需求的理解需要更深”,之前的开发工作比较专一,测试的视野更加开阔,想要站在更高…...

进阶JAVA篇- Collection 类的常用的API与 Collection 集合的遍历方式
目录 1.0 Collection 类的说明 1.1 Collection 类中的实例方法 2.0 Collection 集合的遍历方式(重点) 2.1 使用迭代器( Iterator )进行遍历 2.2 使用增强型 for 循环进行遍历 2.3 使用 Java 8的 Stream API 进行遍历(使…...

CentOS | 添加普通用户并授权sudo
sudo -i adduser peter passwd peter whereis sudoers nano /etc/sudoers添加一行新用户到root组 ## Allow root to run any commands anywhere root ALL(ALL) ALL peter ALL(ALL) ALL如果提升权限后无法cd到其他目录等,修改 /etc/passwd 文件&…...

【MyBatis】mybatis工具类迭代
目录 MyBatis工具类的迭代 ThreadLocal使用 mybatis工具类终极版: MyBatis工具类的迭代 public class MyBatisUtil {//工具类构造方法私有化private void MyBatisUtil() {}//方法一public static SqlSession getSqlSession(){try {SqlSessionFactoryBuilder sql…...

MSQL系列(六) Mysql实战-SQL语句优化
Mysql实战-SQL语句优化 前面我们讲解了索引的存储结构,BTree的索引结构,以及索引最左侧匹配原则,Explain的用法,可以看到是否使用了索引,今天我们讲解一下SQL语句的优化及如何优化 文章目录 Mysql实战-SQL语句优化1.…...

kaggle新赛:UBC卵巢癌亚型分类和异常检测大赛【图像分类】
赛题名称:UBC Ovarian Cancer Subtype Classification and Outlier Detection (UBC-OCEAN) 赛题链接:https://www.kaggle.com/competitions/UBC-OCEAN 赛题背景 卵巢癌是女性生殖系统最致命的癌症。目前,卵巢癌诊断依赖病理学家评估亚型。…...

基于nodejs+vue云旅青城系统
目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性:…...

《孙哥说Spring5》笔记汇总
时隔两个多月,终于将《孙哥说Spring5》的笔记文章全部整理完了,在这里做个汇总。孙哥的Spring课讲的非常好,深度和广度都有所兼顾,推荐大家去看 点击学习《孙哥说Spring5》 基础铺垫 1️⃣ Spring5应用之基础扫盲2️⃣ Spring5应…...

在使用了spring-cloud-starter-gateway后,为什么还会发生cors问题
//1.需要配置类 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; import org.sp…...

CentOS7安装MySQL8.0.28
CentOS7安装MySQL8.0.28 一、下载MySQL安装包二、安装配置mysql 一、下载MySQL安装包 点击以下链接可以自动跳转:MySQL官网 接下来按如图所示依次点击进入。 选择自己所需要版本 此处如需下载历史版本可以点击 二、安装配置mysql 1、登录ssh或其他相关软件上…...

AutoSAR入门:应用背景及简介
1、应用背景 在我们现在的汽车行业里面,汽车电子的发展过程中,我们发现有一些新的趋势汽车电子系统的复杂性不断增长。 我们现在可以看到车辆有越来越多的功能,那么这些功能呢,也在往这个控制器上进行集中,比如说我们现…...

C++初阶(三)
文章目录 一、auto关键字(C11)1、auto简介2、auto使用规则1、 auto与指针和引用结合起来使用2、 在同一行定义多个变量 3、auto不能推导的场景1、 auto不能作为函数的参数2、 auto不能直接用来声明数组3、特性总结 二、基于范围的for循环(C11)1、范围for的语法2、 范围for的使用…...

PHP的学习入门建议
学习入门PHP的步骤如下: 确定学习PHP的目的和需求,例如是为了开发网站还是为了与数据库交互等。学习PHP的基础语法和程序结构,包括变量、数据类型、循环、条件等。学习PHP的面向对象编程(OOP)概念和技术。学习与MySQL…...

骰子涂色(Cube painting, UVa 253)rust解法
输入两个骰子,判断二者是否等价。每个骰子用6个字母表示,如图4-7所示。 例如rbgggr和rggbgr分别表示如图4-8所示的两个骰子。二者是等价的,因为图4-8(a)所示的骰子沿着竖直轴旋转90之后就可以得到图4-8(b&a…...