Hessian Lite序列化简析

dubbo是一个专注于服务治理的高性能rpc框架,支持服务之间的远程调用,Hessian序列化的存在是服务间沟通的桥梁,以下是本人阅读各种资料以及源码,对hessian序列化的一点整理与理解,如有错误请指出。

目录

  1. Hessian Lite序列化简介
  2. Hessian Lite源码解析
    2.1 Hessian Lite类结构
    2.2 Hessian Lite源码解析
    2.3 序列化器和反序列化器
  3. Hessian Lite序列化详述
    3.1 序列化类型
    3.2 序列化详述
  4. 序列化协议对比
  5. 参考


1. Hessian2序列化

hessian是一个动态类型,二进制序列化,也是网络协议,为了对象的定向传输,hessian协议有以下的设计目标:

  • 它必须支持描述序列化的类型,即不需要外部架构和接口定义;
  • 它必须是跨语言的,要支持包括脚本语言;
  • 它必须是可以通过单一方式进行读写;
  • 它要尽可能的简洁;
  • 它必须是简单的,它可以有效地测试和实施;
  • 尽可能的快;
  • 必须要支持Unicode编码;
  • 它必须支持八位二进制数据;
  • 它必须支持加密,压缩,签名,还有事务的上下文。

Hessian是二进制传输协议,不需要其他的附加信息,因此是一种很好的能够支持跨语言特性,但是不同的语言需要一套Hessian的实现。


2. Dubbo Hessian Lite源码解析


2.1 Hessian Lite类结构



以上类图是Dubbo Hessian序列化的类结构图,以下对核心类进行讲解:

Serialization:此接口定义了序列化方式的规格,Dubbo中每种序列化方式都需要实现此接口定义的方式,此接口持有四个方法:getContentTypeId是获取序列化类型id,此值在dubbo协议头里边也会用到; getContentType此方法是获取序列化类型,一种定义的字符串,比如Hessian是x-application/hessian2,在框架中也没有具体的调用地方; serialize此方法用于序列化; deserialize此方法用于反序列化。

Hessian2Serialization: 这个类是外部调用序列化的入口,这个类实现了Serialization接口, serialize和deserialize是返回新建序列化处理对象,比如return new Hessian2ObjectOutput(out);,通过此种方式保证线程安全调用。

Hessian2Input/Hessian2Output: 这两个类是序列化的核心调度类,所有类型的序列化都是通过该类的方法来实现的,此两个类会依赖Hessian2SerializerFactory此类去选择相应的Serializer和Deserializer,然后调用相应方法进行序列化或反序列化。

Hessian2SerializerFactory: 这个是Serializer和Deserializer的工厂类,继承了SerializerFactory,大部分实现都在SerializerFactory类中,类内部持有静态Map,以类型为KEY, 存储Serializer或Deserializer(value),并且对外提供了两个方法getSerializer和getDeserializer分别用于获取相应的Serializer和Deserializer。

AbstractSerializer/AbstractDeserializer: 这两个类分别实现了Serializer和Deserializer,是具体的序列化器和反序列化器的父抽象类,从此类衍生出了很多具体类型的序列化器和反序列化器,类图中未完全给出,包括一些java 8类型的序列化器和反序列化器。


2.2 Hessian Lite序列化和反序列化流程



Hessian Lite序列化流程如上时序图所述,之前的类图上没有加入DubboCodec,是因为这个不是本文所要分享的内容,与dubbo的rpc通信相关;时序图中描述序列化对象,对象只是一个抽象的称呼,可以是Object、List、Map等等,DubboCodec是对ExchangeCodec的一个继承,并重写了部分方法,其中对于协议头的序列化实现在ExchangeCodec中完成的,DubboCodec中会序列化一些附加信息;

通过Hessian2Serialization获取一个Hessian2ObjectOutput对象,并利用Hessian2ObjectOutput执行序列化操作,Hessian2ObjectOutput内部持有一个Hessian2Output变量,具体的序列化操作是实现在Hessian2Output, Hessian2Output是序列化的核心调度类,其内部会根据具体的类型通过Hessian2SerializerFactory选择相应的序列化器来完成Java对象到二进制数据的转化,二进制数据会保存在Hessian2Objectput内部的buffer中。



Hessian Lite反序列化流程如上时序图所述,DubboCodec需要区分是请求还是响应,对于请求利用DecodeableRpcInvocation来做处理,对于响应利用DecodeableRpcResult来做处理,其实序列化也是区分响应还是请求的,对于序列化DubboCodec是通过两个不同的函数实现的;

在DecodeableRpcInvocation或DecodeableRpcResult中还是一样的,获取到Hessian2ObjectInput, 通过Hessian2ObjectInput内部持有的Hessian2Input完成序列化, Hessian2Input是Hessian2反序列化的一个核心调度类,其内部会根据具体的类型通过Hessian2SerializerFactory选择相应的反序列化器来进行反序列化的,DubboCodec最后会将DecodeableRpcInvocation或DecodeableRpcResult放入向上返回的Request或者Response中;

有必要提一下无论是序列化还是反序列化获取ObjectOutput或ObjectInput都是通过一个CodecSupport的工具类完成的,CodecSupport先确定Serialization(hessian或者其他),然后再通过Serialization来获取ObjectOutput或ObjectInput。


2.3 序列化器和反序列化器

2.3.1 选择序列化器或反序列化器

当序列化或反序列化的时候会选择与该类型匹配的序列化器,此过程主要是在SerializerFactory该类中完成,此类继承抽象类AbstractSerializerFactory,对外有两个方法getSerializer和getDeserializer,此二方法的作用是通过Class<?>来获取相应的序列化器或反序列化器, 流程如下,此流程只是图形化一下Dubbo中的代码逻辑,并非一定要按照如此优先级进行。



静态Map在类初始化时,以Class为key,Serializer为value,进行了一系列的put操作,其中涉及类型void,Boolean,Byte,Short,Integer,Long,Float,Double,Character, String, Object, java.util.Date, boolean, byte, short, int, long, float, double, char, boolean, byte, short, int, long, float, double, char, String, Object, Class, BigDecimal, File, ObjectName, java.sql.Date, java.sql.Time, java.sql.Timestamp , java.io.InputStream, java.time.LocalTime, java.time.LocalDate, java.time.LocalDateTime, java.time.Instant, java.time.Duration, java.time.Period, java.time.Year, java.time.YearMonth, java.time.MonthDay, java.time.OffsetDateTime, java.time.ZoneOffset, java.time.OffsetTime, java.time.ZonedDateTime,此步操作是通过具体类型来获取序列化器;

  • 缓存Map是当从静态Map中获取不到时,会后续的操作,确定序列化器后会将之放入缓存Map当中,此操作可以看成是一种提高性能的做法。
  • 其他工厂是加载其他的工厂类来获取相应的序列化器,不过debug到这个地方,装载工厂类的Map是空,具体作用待定。

writeReplace与readResolve的作用一样,如果序列化的对象具有此方法,会利用此方法返回的实例来代替序列化后实例,用以保证对象的单例性。

  • 后续的其他类型会判断传入的类型是否为与其类型一致,或者为其子类,来获取相应的序列化器。

默认的序列化器JavaSerializer是用以处理以上类型未包含的类型,比如自定义的对象类型,这个官方的Hessian有点区别,官方的Hessian的默认序列化器是UnsafeSerializer(具体的类不包含writeReplace方法),这个与JavaSerializer的区别是先将对象序列化成Map,同样反序列化时候UnsafeDeserializer先将二进制数据序列化成Map,然后再将Map转化成对象,而JavaDeserializer会新建一个对象然后再把属性设置进去。



  • 选择反序列化器之前的步骤和上述差不多,值得一提的反序列化Class类型时为何不将之放入静态Map当中,是因为改反序列化器需要获取当前的类加载器,而且在流程中还少了一些类型,是因为在初始化静态Map时,将其及其子类已经放入了静态Map当中。

详细实现请参见:com.alibaba.com.caucho.hessian.io.SerializerFactory

2.3.2 部分序列化器和反序列化器介绍

BasicSerializer/BasicSerializer

该序列化器和反序列化器负责序列化的类型是:null, Boolean/boolean, byte/Byte, short/Short, int/Integer, long/Long, float/Float, double/Double, char/Character, String, 以上类型的数组,Number, Object, Date等,其中byte/Byte, short/Short, int/Integer都是按照整型来序列化与反序列化,float/Float, double/Double都是按照双精度浮点型来序列化与反序列化。

ClassDeserializer/ClassSerializer

该序列化器和反序列化器负责序列化的类型是:Class, 对此执行序列化时,是将java.lang.Class以及对象的全类名以字符串的序列化,当反序列化时会根据类的全类名利用Class.forName来加载Class。

StringValueSerializer/StringValueDeserializer

该序列化器和反序列化器负责序列化的类型是:BigDecimal, ObjectName, 执行序列化时,会将BigDecimal格式化成字符串, 反序列化时再将字符串格式化成BigDecimal,这也是BigDecimal序列化精度不丢失的原因,不过相对而言序列化性能会稍微差点,ObjectName是JMX MBean要注册的对象名称,此处不做过多介绍。

SqlDateSerializer/SqlDateDeserializer

该序列化器和反序列化器负责序列化的类型是:java.sql.Date, 与Date序列化一样,会将通过long型时间戳作为中间变量进行序列化和反序列化。

InputStreamSerializer/InputStreamDeserializer

该序列化器和反序列化器负责序列化的类型是:InputStream, 具体的序列化过程是将InputStream中分块将写入Hessian2Output中的buffer当中,在反序列化时候再分块读取。

MapSerializer/MapDeserializer

该序列化器和反序列化器负责序列化的类型是:Map及其子类, 具体序列化时候会写入Map头,有类型的写入标识M, 无类型的写入标识H,然后再迭代序列化每个对象,最后再Map的尾标识Z。

CollectionSerializer/CollectionDeserializer

该序列化器和反序列化器负责序列化的类型是:Collection及其子类, 比如List、Set等等,具体序列化时候会写入Collection头标识,然后再迭代序列化每个对象,最后再Collection的尾标识,具体序列化细节会在下边详述。

EnumSerializer/EnumDeserializer

该序列化器和反序列化器负责序列化的类型是:Enum及其子类,具体序列化过程会将枚举类全类名以及枚举值以字符串形式序列化,在反序列化过程中会用反射的方式来构建枚举对象。

ArraySerializer/ArrayDeserializer

该序列化器和反序列化器负责序列化数组类型,其序列化过程是Collection序列化的一种情况,是按照定长列表形式来序列化的。

JavaSerializer/JavaDeserializer

该序列化器和反序列化器是默认的,负责序列化的类型是:自定义的对象类型、异常类型等等,其内部持有相应的FieldSerializer数组用来序列化对象中的基本类型字段,每种FieldSerializer内部序列化以及其他对象类型还是调用Hessian2Output相应的序列化方法,反序列化也有类似的过程。


3 Hessian Lite序列化详述


3.1 序列化类型

hessian对象序列化有8个基本类型:

  • 原始二进制数据
  • 布尔
  • 64位的毫秒日期类型
  • 64位double类型
  • 32位int类型
  • 64位long类型
  • null
  • UTF-8编码的字符串

有3中可遍历的类型:

  • 列表(List):lists and arrays
  • 映射(Map):maps and dictionaries
  • 对象(Object): objects

同时还有3个内置的引用映射:

  • 对象/列表引用映射
  • 类定义引用映射
  • 类型引用映射(类名)


3.2 序列化详述

语法是hessian标准的语法

二进制数据(byte array)

Hessian2语法

binary ::= b b1 b0 <binary-data> binary
       ::= B b1 b0 <binary-data>
       ::= [x20-x2f] <binary-data>

Hessian Lite

二进制数据是以块进行编码的,B表示最后一块,b表示非最后一块,b1表示长度的高八位,b0表示长度的低八位, 长度为一个字节是用l表示:

  • 非最后一块:利用A标识,这是dubbo与标准不同的地方,并用两个字节表示长度, 占用3个字节.
  • 最后一块: 利用B标识,并用两个字节表示长度,占用3个字节.
  • [0, 0x0f]: 当前非最后一块切分剩下的部分的长度在此范围内,长度形式为0x20+b0,占用一个字节
  • [0x10, 0x3ff]: 当前非最后一块切分剩下的部分的长度在此范围内,长度形式为0x34+b1 b0,占用两个字节
序列化实现详细代码请阅读com.alibaba.com.caucho.hessian.io.Hessian2Output#writeBytes(byte[], int, int)

布尔

Hessian2语法

boolean ::= T
        ::= F

Hessian Lite

F表示false, T表示true,直接利用字节序列化.

序列化实现详细代码请阅读com.alibaba.com.caucho.hessian.io.Hessian2Output#writeBoolean

日期

Hessian2语法

date ::= x4a b7 b6 b5 b4 b3 b2 b1 b0
     ::= x4b b4 b3 b2 b1 b0

用距离1970-01-01 00:00:00的一个64位长的毫秒值来表示日期

Hessian Lite

日期的序列化方式有两种,如果能够精确到分,可以用32位整型来表示,否则只能用64位整型来表示,0x4b表示32位形式,0x4a表示64位形式, 然后将值移位序列化为二进制.

序列化实现详细代码请阅读com.alibaba.com.caucho.hessian.io.Hessian2Output#writeUTCDate

Double

Hessian2语法

double ::= D b7 b6 b5 b4 b3 b2 b1 b0
       ::= x5b
       ::= x5c
       ::= x5d b0
       ::= x5e b1 b0
       ::= x5f b3 b2 b1 b0

Hessian Lite

在序列化Double时,是分了几种类型,每种序列化的方式都是要将Double转成整型,然后利用移位填充每个字节:

  • 0x5b代表Double.Zero
  • 0x5c代表Double.One
  • 0x5d代表大小精确到一个字节表示的Double类型
  • 0x5e代表大小精确到两个个字节表示的Double类型
  • 0x5f代表只有3位小数以内的Double类型
  • 默认是8个字节表示,D位Double的标识
为什么要分这么多种方式,是一种提高性能的方式,如果数值很小的话,用8个字节表示,首先会增大传输数据长度,其次8个字节是遵循ieee754标准的,运算过程会相对复杂一点, 并且dubbo中float类型利用序列双精度浮点数类型来处理,序列化实现详细代码请阅读com.alibaba.com.caucho.hessian.io.Hessian2Output#writeDouble

IEEE754 双精度浮点数

Double.doubleToLongBits这个函数底层调用的是java.lang.Double#doubleToRawLongBits,这是一个native方法,看不到具体的实现方式,但是是ieee745标准的一个实现,以下对ieee754标准的双精度浮点数做一个原理做一个概述性的介绍。 双精度浮点数的IEEE754格式如下:

-----------------------------------------------------------------------------
| 符号 1bit |     幂 11bit       |              分数  52 bit                 |
-----------------------------------------------------------------------------
  • 符号1位:0表示此值为正,1表示此值为负数
  • 幂11位:指数域的编码值为指数的实际值加上某个固定的值,该固定值为 2^(e-1)-1
  • 分数52位:也就是指转化为科学计数法后的小数部分

举个例子,18.9转换成此种形式:

  • 18.9为正数,所以符号位为0
  • log2(18.9)=4.24,则指数为可以看做是二进制的4次方,也就是1023+4=1027为10000000011
  • 18.9/2^4=1.18125,注意默认将1去掉,则将0.18125转化为小数二进制为0010111001100110011001100110011001100110011001100110(怎么小数转换成二进制情百度)
  • 然后拼起来便是一个完整的64位双精度的表示方式
还有一些特殊形式,比如+0,-0,NaN等,此处不做多讲述,想了解的可以戳小标题(维基百科,需要翻墙)

整型(int)

Hessian2语法

int ::= 'I' b3 b2 b1 b0
    ::= [x80-xbf]
    ::= [xc0-xcf] b0
    ::= [xd0-xd7] b1 b0

Hessian Lite

为了提高序列化效率,整型分类几种类型去处理, 用v来表示要序列化的值, 形式并按字节顺序排列, b3, b2, b1, b0分别表示长度的各个8位:

  • [-0x10, 0x2f]: 如果值大小位于此区间,占用1个字节,形式为b0+0x90.
  • [-0x800, 0x7ff]: 如果值大小位于此区间,占用2个字节,形式为b1+0xc8 b0.
  • [-0x40000, 0x3ffff]: 如果值大小位于此区间,占用3个字节, 形式为b2+0xd4 b1 b0.
  • 除上述以上情况外,序列化需要占用9个字节,形式I b3 b2 b1 b0, I是整型的标识.
dubbo中byte,short类型利用序列化整型处理,序列化实现详细代码请阅读com.alibaba.com.caucho.hessian.io.Hessian2Output#writeInt

长整型(long)

Hessian2语法

long ::= L b7 b6 b5 b4 b3 b2 b1 b0
     ::= [xd8-xef]
     ::= [xf0-xff] b0
     ::= [x38-x3f] b1 b0
     ::= x4c b3 b2 b1 b0

Hessian Lite

长整型在序列化时候与双精度浮点型类似,为了提高序列化效率,也是分类几种类型去处理, b7, b6, b5, b4, b3, b2, b1, b0分别表示长度的各个8位:

  • [-0x08, 0x0f]: 如果值大小位于此区间,占用1个字节,形式为b0+0xe0.
  • [-0x800, 0x7ff]: 如果值大小位于此区间,占用2个字节,形式为b1+0xf8 b0.
  • [-0x40000, 0x3ffff]: 如果值大小位于此区间,占用3个字节, 形式为b2+0x3c b1 b0.
  • [-0x80000000, 0x7fffffff]: 如果值大小位于此区间,占用5个字节,形式0x59 b3 b2 b1 b0 , 此处的前缀0x59是dubbo与标准hessian的一个区别
  • 除上述以上情况外,序列化需要占用9个字节,形式L b7 b6 b5 b4 b3 b2 b1 b0, L是长整型的标识.
序列化实现详细代码请阅读com.alibaba.com.caucho.hessian.io.Hessian2Output#writeLong

null

Hessian2语法

null ::= N

Hessian Lite

null的语法与boolean都很简单利用字符N占用一个字节来表示.

序列化实现详细代码请阅读com.alibaba.com.caucho.hessian.io.Hessian2Output#writeNull

UTF-8编码的字符串

Hessian2语法

string ::= x52 b1 b0 <utf8-data> string
       ::= S b1 b0 <utf8-data>
       ::= [x00-x1f] <utf8-data>
       ::= [x30-x33] b0 <utf8-data>

Hessian Lite

字符串会按照块来序列化,如果是最后一块用S来标识,如果不是最后一块利用R来标识, b1, b0分表表示长度高低8位:

  • 非最后一块:利用R标识,并用两个字节来表示长度
  • [0x00-0x1f]: 如果划分完所有的非最后一块,剩下的部分长度在此范围值内,则不用S来标识,而是直接序列化长度值.
  • [0x20-0x3ff]: 如果划分完所有的非最后一块,剩下的部分长度在此范围值内,则不用S来标识, 序列化形式为0x30+b1 b0,所以长度第一个字节范围[x30-x33]
  • 否则序列化利用S来标识最后一块,并用两个字节来标识长度
dubbo中char类型利用序列化字符串方式来处理,序列化实现详细代码请阅读com.alibaba.com.caucho.hessian.io.Hessian2Output#writeString(java.lang.String)

列表(List)

Hessian2语法

list ::= x55 type value* 'Z'   # variable-length list
     ::= 'V' type int value*   # fixed-length list
     ::= x57 value* 'Z'        # variable-length untyped list
     ::= x58 int value*        # fixed-length untyped list
     ::= [x70-77] type value*  # fixed-length typed list
     ::= [x78-7f] value*       # fixed-length   list

Hessian Lite

列表序列化,其实Collection和数组都会按照此形式进行序列化,会写入一个头和一个尾,中间的的对象按照对象来序列化,序列化过程如下:

  • 写入开始标识, 开始标识会分为几种形式,列表长度用len表示
    • 如果列表的长度为-1,类型确定为0x58, 并写入当前类型; 类型不确定为0x57, 此种形式在EnumerationIterator序列化中用到,不过目前我还不清楚这两种迭代器的序列化有何用;
    • 如果列表的长度小于7,类型确定为0x70+len,类型不确定为0x78+len, 此种处理即将标识和长度放在一起,可以减小序列化后的size;
    • 否则类型确定为V,并写入当前类型以及长度; 类型不确定为0x58,并写入长度。


  • 循环序列化对象
  • 写入结束标识Z
  • 定长列表序列化过程:序列化写入标识V,序列化写入类的全类名,序列化写入长度,循环序列化写入各个对象,最后写入Z
  • 无类型定长列表序列化过程:先写入标识0x58,序列化写入长度,循环序列化写入各个对象,最后写入Z
详细代码请阅读com.alibaba.com.caucho.hessian.io.CollectionSerializer

映射(Map)

Hessian2语法

map  ::= M type (value value)* Z

Hessian Lite

Map的序列化要相对简单一些,序列化过程如下:

  • 写入开始标识,分为两种类型
    • 确定类型为M,不确定类型为H


  • 循环写入key和value
  • 写入结束标识Z
详细代码请阅读 com.alibaba.com.caucho.hessian.io.MapSerializer#writeObject

对象(Object)

Hessian2语法

class-def  ::= 'C' string int string*

object     ::= 'O' int value*
           ::= [x60-x6f] value*

Hessian Lite

序列化对象过程如下:

  • 写入开始标识,分以下几种类型
    • 如果之前已经写入该对象了,此时不会再次序列化类名称,会以类名引用方式写入,目的还是减小序列化size, 如果存放引用的列表size小于等于15,则标识为0x60+列表size; 否则标识为O,并接着写入列表size;
    • 如果之前没有写入该对象,则标识为'C',并接着写入全类名;


  • 分别序列化各个字段,虽然基本类型JavaSerializer中会持有FieldSerilizer,但序列化各个字段具体还是会调用Hessian2Output中的各个方法。
详细代码请阅读 com.alibaba.com.caucho.hessian.io.JavaSerializer#writeObject

引用

Hessian2语法

ref ::= x51 int

Hessian Lite

对于引用类型有三种,分别为Object/Collection/Map的值引用,类定义的引用(类定义是指全类名以及类字段的名称),全类名的引用,主要目的是复用,用以提高序列化的效率;引用主要是在调用方完成,先把之前读取的缓存起来,提供方会按照顺序写入引用标识以及顺序值,调用方在遇到引用标识后会根据顺序值从相应列表中读取来完成。


4. 序列化协议对比

此部分是参考论文Smart Grid Serialization Comparison,其中一些图也是源于此paper。

本部分将要对比的涉及26中序列化协议,以下对此26中序列化协议进行简单介绍,对比的标准如下:

  • 序列化时间
  • 反序列化时间
  • 压缩时间
  • 解压缩时间
  • 序列化使用内存
  • 压缩使用内存
  • 序列化后的Size
  • 压缩后的Size

为了保证对比的合理性,对时间的取值为1000次的平均值,对于内存的取值为1000次垃圾回收器消耗的内存平均值,对于Size是对IEC-61850数据模型类进行操作,用于合理准确的评估,测试机器配置:Win10,Jdk-1.8 64位,CPU i7-4600U,8GB内存,具体对比如下图:







根据对比结果可知,protobuf性能已经达到令人发指的碾压程度,如果dubbo能够支持protobuf的话,序列化性能能够提升两倍。


5. 参考

发布于 2018-09-18