Simple RPC - 02 通用高性能序列化和反序列化设计与实现

news2025/1/13 13:12:26

文章目录

  • 概述
  • 设计实现
    • 通用的序列化接口
    • 通用的序列化实现【推荐】 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> {
    @Override
    public int size(String entry) {
        return entry.getBytes(StandardCharsets.UTF_8).length;
    }
    @Override
    public 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);
    }
    @Override
    public String parse(byte[] bytes, int offset, int length) {
        return new String(bytes, offset, length, StandardCharsets.UTF_8);
    }
    @Override
    public byte type() {
        return Types.TYPE_STRING;
    }
    @Override
    public 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> {

    @Override
    public int size(Metadata entry) {
        return Short.BYTES +                   // Size of the map                  2 bytes
                entry.entrySet().stream()
                        .mapToInt(this::entrySize).sum();
    }

    @Override
    public 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 bytes
                e.getKey().getBytes().length +    // Serialized key bytes:   variable length
                Short.BYTES + // List size:              2 bytes
                e.getValue().stream() // Value list
                        .mapToInt(uri -> {
                            return Short.BYTES +       // Key string length:               2 bytes
                                    uri.toASCIIString().getBytes(StandardCharsets.UTF_8).length;    // Serialized key bytes:   variable length
                        }).sum();
    }

    @Override
    public 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;
    }

    @Override
    public byte type() {
        return Types.TYPE_METADATA;
    }

    @Override
    public Class<Metadata> getSerializeClass() {
        return Metadata.class;
    }

    private short toShortSafely(int v) {
        assert v < Short.MAX_VALUE;
        return (short) v;
    }
}

到这里序列化的部分就实现完成了。我们这个序列化的实现,对外提供服务的就只有一个 SerializeSupport 静态类,并且可以通过扩展支持序列化任何类型的数据,这样一个通用的实现,不仅可以用在我们这个 RPC 框架的例子中,完全可以把这部分直接拿过去用在业务代码中


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1112043.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RHCE---shell 基本知识

文章目录 目录 文章目录 前言 一.shell 概述 如何编写shell脚本 脚本开头 脚本内容 执行脚本 bash shell 基本功能 echo打印命令 printf 命令 history历史命令 命令别名 总结 前言 在UNIX和类UNIX操作系统中&#xff0c;Shell是一个非常重要的组件&#xff0c;为用户提供…

Jmeter —— jmeter利用取样器中http发送请求

使用Jmeter发送HTTP请求 取样器是用来模拟用户操作&#xff0c;向服务器发送请求以及接收服务器的响应数 据的一类元件&#xff0c;其中HTTP请求取样器是用来模拟常用的http请求的 步骤如下&#xff1a; 步骤一&#xff1a;添加线程组 右击测试计划——添加——线程&#x…

大数据学习(13)-join优化详解

&&大数据学习&& &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 承认自己的无知&#xff0c;乃是开启智慧的大门 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一下博>主哦&#x…

工业自动化控制通信协议Profinet系列-1、了解Profinet协议及收集资料

工业自动化控制通信协议Profinet系列-1、了解Profinet协议及收集资料 文章目录 工业自动化控制通信协议Profinet系列-1、了解Profinet协议及收集资料一、前言二、Profinet了解及资料收集1. 资料2. PROFINET到底是什么&#xff1f;3. 网络模型4. 应用场景 三、接下来 一、前言 …

最详细的Keycloak教程(建议收藏):Keycloak实现手机号、验证码登陆——(二)Keycloak与SpringBoot的集成

上一篇已经介绍了keycloak的下载与使用: Keycloak的下载与使用 本文章和大家分享keycloak怎么去集成springboot项目&#xff0c;以及怎么去做接口权限的校验。 PS&#xff1a;根据红帽的公告&#xff0c;目前在springboot 3.x版本已经不支持keycloak适配器&#xff0c;所以本章…

44岁的「老板」想变年轻

作者 | 辰纹 来源 | 洞见新研社 从村办集体企业余杭县红星五金厂起家&#xff0c;到生产贴牌油烟机&#xff0c;再到注册“老板”商标&#xff0c;改制有限公司&#xff0c;老板电器已经走过了44个春秋。 在这44年中&#xff0c;老板电器是首家登陆资本市场的高端厨电企业&am…

Java总结的question

Java 数据类型 Q1:基本类型和包装类型的区别&#xff1f; 用途&#xff1a;在对象属性中一般用包装类型&#xff0c;包装类型可用于泛型&#xff0c;基本类型不可以存储方式&#xff1a;包装类型一般放在堆&#xff0c;基本数据类型的局部变量一般放在栈中的局部变量表&…

页表置换算法之最佳置换算法(OPT),先进先出置换算法(FIFO),最近最久未使用置换算法(LRU),时钟置换算法(CLOCK)

请求分页存储管理与基本分页存储管理的主要区别: 在程序执行过程中&#xff0c;当所访问的信息不在内存时&#xff0c;由操作系统负责将所需信息从外存调入内存&#xff0c;然后继续执行程序。若内存空间不够&#xff0c;由操作系统负责将内存中暂时用不到的信息换出到外存。页…

22-数据结构-内部排序-选择排序

简介&#xff1a;每一趟选择最小或最大的一个&#xff0c;排在前面或后面。主要右简单选择排序和堆排序 一、简单选择排序 1.1简介&#xff1a; 每趟选择最小的&#xff0c;放在前面&#xff0c;一次类推&#xff0c;代码思想&#xff1a;两个循环&#xff0c;外循环是趟数&a…

IDEA提高工作效率的实用技巧

IDEA是一款备受开发者喜爱的集成开发环境&#xff0c;它提供了许多实用的功能&#xff0c;可以帮助我们更快速、更高效地编写代码。本文将介绍一些IDEA的使用技巧提高工作效率的实用技巧。 验证正则表达式 要验证编写的正则表达式是否正确&#xff0c;只需将光标放在要检查的…

【单片机毕业设计】【hj-006-6】天然气、有害混合气体检测 | 空气质量检测 | 有害气体检测

一、基本介绍 项目名&#xff1a; 基于单片机的天然气、有害混合气体检测系统设计 基于单片机的空气质量检测系统设计 基于单片机的 有害气体检测系统设计 项目编号&#xff1a;mcuclub-hj-006-6 单片机类型&#xff1a;STC89C52 具体功能&#xff1a; 1、通过MQ-5检测天然气…

C语言——二周目——输入输出辨析

一、对输入输出的理解 1.明确输入的意义 以往的输入为默认形式&#xff08;标准输入流——stdin——键盘&#xff09;。但是输入的形式不止此一种。可以从键盘上敲出输入的数据&#xff0c;同时也可以将文件中、某个字符串甚至结构体的数据作为输入内容进行输入。 输入&#x…

Spring boot 集成 xxl-job

文章目录 xxl-job 简介引入xxl-job依赖配置xxl-job config添加properties文件配置BEAN模式&#xff08;方法形式&#xff09;步骤一&#xff1a;执行器项目中&#xff0c;开发Job方法&#xff1a;步骤二&#xff1a;调度中心&#xff0c;新建调度任务 xxl-job 简介 官网:https:…

【虹科干货】Redis Enterprise vs ElastiCache——如何选择缓存解决方案?

使用Redis 或 Amazon ElastiCache 来作为缓存加速已经是业界主流的解决方案&#xff0c;二者各有什么优势&#xff1f;又有哪些区别呢&#xff1f; 文况速览&#xff1a; - Redis 是什么&#xff1f; - Redis Enterprise 是什么&#xff1f; - Amazon ElastiCache 是什么&…

阿里云高庆瑞:高弹性、高可用、低成本的云上资源管理最佳实践

云布道师 为了更好地帮助用户在借助 DevOps 工具缩短开发周期、提升业务效率的同时&#xff0c;也能让业务保持稳定、安全、可靠&#xff0c;且低成本地持续运营&#xff0c;阿里云弹性计算团队独家出品的【弹性计算技术公开课_CloudOps 云上运维季】正式启动。阿里云弹性计算团…

提升服装门店管理效率与顾客体验的RFID智慧门店解决方案

随着科技的不断进步&#xff0c;传统的服装门店在管理过程中面临着一些瓶颈和挑战&#xff0c;条码管理费时费力&#xff0c;服装查找耗时长&#xff0c;库存盘点不准确&#xff0c;销售管理不科学&#xff0c;顾客体验性较差等问题已经成为制约门店发展的难题&#xff0c;为了…

小程序之实例会议OA的首页 (3)

⭐⭐ 小程序专栏&#xff1a;小程序开发专栏 ⭐⭐ 个人主页&#xff1a;个人主页 目录 ​编辑 一.前言 二.flex弹性布局 flex属性 2.1 display: flex 弹性布局属性 2.2 flex-direction属性 2.3 flex-wrap属性 2.4 flex-flow属性 2.5 justify-content属性 三.首页轮播…

12.Bilinear Forms

Bilinear Forms 双线性形式 Metric Tensor 度量张量是双线性形式的一种。 在学习张量积之前&#xff0c;先讨论一般的线性形式 回顾一下上一节学的 Metric Tensor 度量张量所表示的矩阵 是个 对称矩阵。&#xff0c; 度量张量的性质&#xff1a; 双线性形式 定义&#xf…

《算法通关村第二关——终于学会链表反转了》

《算法通关村第二关——终于学会链表反转了》 今天学习链表反转 为什么反转这么重要呢&#xff1f;因为反转链表涉及结点的增加、删除等多种操作&#xff0c;能非常有效考察思维能力和代码驾驭能力。另外很多题目也都要用它来做基础&#xff0c; 例如指定区间反转、链表K个一…

大数据之LibrA数据库系统服务部署原则及运行环境要求

服务部署原则 FusionInsight LibrA集群由多种服务按照一定的逻辑架构组合而成&#xff0c;每个服务包含一个或多个角色&#xff0c;每个角色可以部署一个或多个实例。 服务&#xff1a;服务对外表现为集群提供的组件业务能力&#xff0c;集群中的每个组件对应一个服务名&…