FastProto:一款Java编写的二进制数据处理工具

590 阅读6分钟

Fast Protocol

项目地址:github.com/indunet/fas…

从事了多年物联网开发,同时也围绕着设备与服务器之间的数据交换做了很多工作。所接触的项目中,设备端软件通常采用C语言开发,而服务器端更多使用Java开发。受技术方面和商务方面的限制,设备和服务器之间一般并不使用JSON格式交换数据,而是采用自定义协议的二进制格式。

比如这样一个场景,气象监测设备实时采集气象数据,并以二进制格式发送数据到气象站,数据报文固定长度20字节,具体如下:

65 00 7F 69 3D 84 7A 01 00 00 55 00 F1 FF 0D 00 00 00 07 00

服务器端接收到数据报文后,需要按照特定的协议(字典)解析,具体如下:

字节偏移位偏移数据类型(C/C++)信号名称单位换算公式
0unsigned char设备编号
1预留
2-9long时间戳ms
10-11unsigned short湿度%RH
12-13short温度
14-17unsigned int气压Pap * 0.1
180bool温度有效标识
181bool湿度有效标识
182bool气压有效标识
183-7预留
19预留

也许你能够快速地实现上述过程,但是在实际的项目中,数据报文种类繁多,且每个解析协议更是包含几十个甚至几百个信号,导致开发过程枯燥,也极易出错。 FastProto就是用来解决上述问题的工具,开发者并不用过多的关注数据的解析和数据封包。

功能

  • 二进制数据解析 & 封包
  • 支持基本数据类型、无符号类型、字符串类型、时间类型、数组类型和集合类型等
  • 支持反向寻址,适用于非固定长度二进制数据
  • 自定义开端字节顺序
  • 自定义编码公式 & 解码公式,支持Lambda表达式

Maven

<dependency>
    <groupId>org.indunet</groupId>
    <artifactId>fastproto</artifactId>
    <version>3.8.1</version>
</dependency>

快速入门

有这样一个应用场景,一台气象监测设备实时采集气象数据,并以二进制格式发送数据到气象站,数据报文20字节固定长度:

65 00 7F 69 3D 84 7A 01 00 00 55 00 F1 FF 0D 00 00 00 07 00

数据报文包含8种不同类型的信号,具体协议如下:

字节偏移位偏移数据类型(C/C++)信号名称单位换算公式
0unsigned char设备编号
1预留
2-9long时间戳ms
10-11unsigned short湿度%RH
12-13short温度
14-17unsigned int气压Pap * 0.1
180bool温度有效标识
181bool湿度有效标识
182bool气压有效标识
183-7预留
19预留
  1. 解析 & 封包

气象站接收到数据后,需要将其反序列化成Java数据对象,以便后续的业务功能开发。 首先,按照协议定义Java数据对象Weather,然后使用FastProto数据类型注解修饰各个属性,通过注解的offset属性指定信号的字节偏移量。

import org.indunet.fastproto.annotation.*;

public class Weather {
    @UInt8Type(offset = 0)
    int id;

    @TimeType(offset = 2)
    Timestamp time;

    @UInt16Type(offset = 10)
    int humidity;

    @Int16Type(offset = 12)
    int temperature;

    @UInt32Type(offset = 14)
    long pressure;

    @BoolType(byteOffset = 18, bitOffset = 0)
    boolean temperatureValid;

    @BoolType(byteOffset = 18, bitOffset = 1)
    boolean humidityValid;

    @BoolType(byteOffset = 18, bitOffset = 2)
    boolean pressureValid;
}

调用FastProto::parse()方法将二进制数据反序列化成Java数据对象Weather

// datagram sent by monitoring device.
byte[] datagram = ...   
        
Weather weather = FastProto.parse(datagram, Weather.class);

调用FastProto::toBytes()方法将Java数据对象Weather序列成二进制数据,方法的第二个参数是字节数组长度,如果用户不指定,那么FastProto会自动推测。

byte[] datagram = FastProto.toBytes(weather, 20);
  1. 公式

也许你已经注意到压力信号对应一个换算公式,通常需要用户自行将序列化后的结果乘以0.1,这是物联网数据交换时极其常见的操作。 为了帮助用户减少中间步骤,FastProto引入了编码公式注解@EncodingFormula和解码公式注解@DecodingFormula,上述简单的公式变换可以通过Lambda表达式实现。

import org.indunet.fastproto.annotation.DecodingFormula;
import org.indunet.fastproto.annotation.EncodingFormula;

public class Weather {
    ...

    @UInt32Type(offset = 14)
    @DecodingFormula(lambda = "x -> x * 0.1")
    @EncodingFormula(lambda = "x -> (long) (x * 10)")
    double pressure;
}

注解

  1. 基本类型注解 FastProto支持Java基础数据类型、时间类型、字符串类型、枚举类型和字节数组等,考虑到跨语言跨平台的数据交换,FastProto还引入了无符号类型。
注解JavaC/C++大小
@BoolTypeBoolean/booleanbool1 位
@CharType(仅ASCII)Character/charchar1 字节
@Int32TypeInteger/intint4 字节
@Int64TypeLong/longlong8 字节
@FloatTypeFloat/floatfloat4 字节
@DoubleTypeDouble/doubledouble8 字节
@Int8TypeByte/byte/Integer/intchar1 字节
@Int16TypeShort/short/Integer/intshort2 字节
@UInt8TypeInteger/intunsigned char1 字节
@UInt16TypeInteger/intunsigned short2 字节
@UInt32TypeLong/longunsigned int4 字节
@UInt64TypeBigIntegerunsigned long8 字节
@StringTypeString/ StringBuilder/StringBuffer--N 字节
@TimeTypeTimestamp/Date/Calendar/Instantlong8 字节
@EnumTypeenumenum1 字节
  1. 数组类型注解
注解JavaC/C++
@BinaryTypeByte[]/byte[]/Collectionchar[]
@Int8ArrayTypeByte[]/byte[]/Integer[]/int[]/Collection/Collectionchar[]
@Int16ArrayTypeShort[]/short[]/Integer[]/int[]/Collection/Collectionshort[]
@Int32ArrayTypeInteger[]/int[]/Collectionint[]
@Int64ArrayTypeLong[]/long[]/Collectionlong[]
@UInt8ArrayTypeInteger[]/int[]/Collectionunsigned char[]
@UInt16ArrayTypeInteger[]/int[]/Collectionunsigned short[]
@UInt32ArrayTypeLong[]/long[]/Collectionunsigned int[]
@UInt64ArrayTypeBigInteger[]/Collectionunsigned long[]
@FloatArrayTypeFloat[]/float[]/Collectionfloat[]
@DoubleArrayTypeDouble[]/double[]/Collectiondouble[]
  1. 其它注解 FastProto还提供了一些辅助注解,帮助用户进一步自定义二进制格式、解码和编码流程。
注解作用域描述
@DefaultEndianClass数据开端,默认小开端
@DecodingIgnoreField反序列化时忽略该字段
@EncodingIgnoreField序列化时忽略该字段
@FixedLengthClass启动固定报文长度
@DecodingFormulaField解码公式
@EncodingFormulaField编码公式

3.1 大小开端 FastProto默认使用小开端,可以通过@DefaultEndian注解修改全局开端类型,也可以通过endian属性修改特定字段开端,后者优先级更高。

import org.indunet.fastproto.EndianPolicy;
import org.indunet.fastproto.annotation.DefaultEndian;

@DefaultEndian(EndianPolicy.BIG)
public class Weather {
    @UInt16Type(offset = 10, endian = EndianPolicy.LITTLE)
    int humidity;

    @UInt32Type(offset = 14)
    long pressure;
}

3.2 解码 & 编码公式

用户可以通过两种方式自定义公式,形式较为简单的公式建议使用Lambda表达式,形式较为复杂的公式建议自定义公式类并实现java.lang.function.Function接口。

  • Lambda表达式
import org.indunet.fastproto.annotation.DecodingFormula;
import org.indunet.fastproto.annotation.EncodingFormula;

public class Weather {
    ...

    @UInt32Type(offset = 14)
    @DecodingFormula(lambda = "x -> x * 0.1")
    @EncodingFormula(lambda = "x -> (long) (x * 10)")
    double pressure;
}
  • 自定义公式类
import java.util.function.Function;

public class PressureDecodeFormula implements Function<Long, Double> {
    @Override
    public Double apply(Long value) {
        return value * 0.1;
    }
}
import java.util.function.Function;

public class PressureEncodeFormula implements Function<Double, Long> {
    @Override
    public Long apply(Double value) {
        return (long) (value * 10);
    }
}
import org.indunet.fastproto.annotation.DecodingFormula;
import org.indunet.fastproto.annotation.EncodingFormula;

public class Weather {
    ...

    @UInt32Type(offset = 14)
    @DecodingFormula(PressureDecodeFormula.class)
    @EncodingFormula(PressureEncodeFormula.class)
    double pressure;
}

用户可以根据需要仅指定编码公式,或者仅指定解码公式,如果同时指定Lambda表达式和自定义公式类,后者有更高的优先级。

3.3 自动类型

如果字段被@AutoType修饰,那么FastProto会自动推测类型。

import org.indunet.fastproto.annotation.AutoType;

public class Weather {
    @AutoType(offset = 10, endian = EndianPolicy.LITTLE)
    int humidity;

    @AutoType(offset = 14)
    long pressure;
}

Scala

FastProto支持case class,但是Scala并不完全兼容Java注解,所以请使用如下方式引用FastProto。

import org.indunet.fastproto.annotation.scala._

基准测试

  • windows 11, i7 11th, 32gb
  • openjdk 1.8.0_292
  • 二进制数据固定大小60字节,数据对象共包含13个不同类型的字段
Benchmark模式样本数量评分误差单位
FastProto::parse吞吐量10240± 4.6次/毫秒
FastProto::toBytes吞吐量10317± 11.9次/毫秒