最佳实践当然是使用框架啦, Nettyx提供了史上最快的C 结构体 序列化/反序列化器, 完爆阿里fury !!!
直接上依赖:
请从maven中央仓获取{lastest.version},最新版本号
<dependency>
<groupId>io.github.fbbzl</groupId>
<artifactId>nettyx</artifactId>
<version>{lastest.version}</version>
</dependency>
概述
Nettyx是基于netty 4.1.X.Final版本二次封装的框架,扩展了一些工具, 其中最重要的功能便是跨平台序列化, 接下来将展示如何接收并解析成C的结构体.
想要定义struct, 必须先从基础类型开始, Nettyx预定义了相当一部分的C/C++的基础类型, 例如Cint,Cfloat,Cuint等基础类型.
此处为了演示效果, 将展示如何自定义基础类型, 当Nettyx预定义的基础类型无法满足需求时, 各位即可根据这个例子自行扩展基础类型
那就先从 定义C中的long类型开始吧.
C语言中的long,一般用作用户id, 序列号什么的
import org.fz.nettyx.serializer.typed.Basic;
/**
* 所有用来 映射C基础类型的 java类 必须实现Basic并重写对应的方法
*
* @author fengbinbin
* @since 2021-10-19 19:58
* @apinote Clong在Nettyx已经预定义了, 此处的clong只是为了演示如何定义基础类型用, 真正开发的时候还是建议使用Nettyx内置的基础类型
*
**/
public class clong extends Basic<Integer> {
//所有basic的子类都必须提供此构造方法, 用来初始化java中的值
public clong(Integer value) {
super(value);
}
// 是否有符号, C语言中有无符号类型, 这样同样的bit位可以表示多一倍的数
// 当然浮点数一般都是有符号的, 毕竟无符号的浮点数也没啥意义
// 没有默认值, 所有子类必须实现
@Override
public boolean hasSinged() {
return true;
}
// 字节顺序, C是小端, 这是和Java不一样的一个地方, 此处要注意
// 没有提供默认的字节顺序, 所有子类必须实现
@Override
public ByteOrder order() {
// C是小端, 这是另一个和Java不同的地方
return ByteOrder.LITTLE_ENDIAN;
}
// 将Java中的值, 转换为ByteBuf, 所有子类必须实现
@Override
protected ByteBuf toByteBuf(Integer value) {
return Unpooled.buffer(size()).writeIntLE(value);
}
// 将ByteBuf转换为Java中的值, 所有子类必须实现
@Override
protected Integer toValue(ByteBuf byteBuf) {
return byteBuf.readIntLE();
}
// 此基础类型所占用的字节数量, 所有子类必须实现
@Override
public int size() {
return 4;
}
}
在定义完基础类型之后, 我们就可以开始定义struct了.仅需注意一点, 所有的struct类都需要加上@Struct注解
// **所有的struct类都要加上@Struct注解!! 类似持久层框架中@Entity注解**
@Struct
public class User {
// 用户名, 这里用了@ToCustomString这个注解, 会在之后说明如何自定义解析注解
@ToCustomString(length=4)
private String name;
// clong就是上面自定义的一个基础类型
private clong uid;
// Cchar数组
//数组必须使用@ToArray来指定长度
//Cchar为nettyx内置C基础类型之一
@ToArray(2)
private Cchar[] gfNames;
//此处省略getter和getter
}
至此, 基础类型和struct的实体类都已经编写完成.
接下来我们进行序列化操作, Nettyx提供了StructSerializer来进行序列化和反序列化, 如果存在泛型, 可以使用TypeRefer来指定, 当然为了宣传国产框架hutool, 同样支持hutool的TypeReference
// 这里硬编码了一段字节数组, 实际上这部分数据可能来自socket, 文件等
// HexKit为Nettyx中操作16机制工具类, 如果不知道 表示方式和存储方式 的不同, 这里可能要去补习下,谁让16进制能干这么多事
byte[] bytes = HexKit.decode("122adf3547f8d65e24ff81aa3478ff4678122adf3547f8d65e24ff81aa3478ff4678");
// StructSerializer的read方法将 字节数组反序列化成struct对象, read方法有很多重载, 选择最合适的使用
User demo = StructSerializer.read(Unpooled.wrappedBuffer(bytes), User.class);
// 同样我们可以使用StructSerializer的write来,来把对象序列化成字节数组, write同样重载了很多方法, 选择最合适的使用
ByteBuf userWriteBytes = StructSerializer.write(user);
// 带泛型的话, 可以使用Nettyx的TypeRefer
TypeRefer<User<Son>> typeRefer = new TypeRefer<User<Son>>() {};
User user = StructSerializer.read(Unpooled.wrappedBuffer(bytes), typeRefer);
ByteBuf userWriteBytes = StructSerializer.write(user, typeRefer );
至此我们完成了最基础的java跨平台解析C struct的操作.
自定义注解
上面提到可以通过定义注解的方式来扩展解析逻辑, 我这波抄…借鉴了jsr303. 如果你想自定义字段的解析逻辑, 你可以实现StructPropHandler.ReadWriteHandler<A extends Annotation>来指定字段解析器, 并在泛型中指定自己定义的注解, 如下图, 我自定义了一个@ToCustomString, 并通过ToCustomStringHandler类实现了 StructPropHandler.ReadWriteHandler<ToCustomString>, 使用时只需要在指定的field上加上@ToCustomString 注解即可
@Documented
@Target(FIELD)
@Retention(RUNTIME)
public @interface ToCustomString {
/**
* Charset string.
* 老生常谈, 字节数组转字符需要提供字符集, 这里默认了utf-8
* @return the legal charset
* @see StandardCharsets
*/
String charset() default "UTF-8";
/**
* Buffer length int.
* 此字符串的字节长度
* @return the buffer occupied by this char sequence
*/
int bufferLength();
/**
* 光定义注解ToCustomString还不行, 还需要定义注解关联的解析器, 在解析器的泛型中指定ToCustomString为目标注解,
* 更多解析器接口请至StructFieldHandler中查看
*/
class ToCustomStringHandler implements StructFieldHandler.ReadWriteHandler<ToCustomString> {
@Override
public Object doRead(StructSerializer serializer, Field field, ToCustomString toString) {
String charset = toString.charset();
if (!Charset.isSupported(charset)) throw new UnsupportedCharsetException("do not support charset [" + charset + "]");
ByteBuf byteBuf = serializer.getByteBuf();
if (!byteBuf.isReadable()) {
throw new IllegalArgumentException(
"buffer is not readable please check [" + ByteBufUtil.hexDump(byteBuf) + "], field is [" + field
+ "]");
}
byte[] bytes;
byteBuf.readBytes(bytes = new byte[toString.bufferLength()]);
return new String(bytes, Charset.forName(charset));
}
@Override
public void doWrite(StructSerializer serializer, Field field, Object value, ToCustomString toString, ByteBuf writing) {
int bufferLength = toString.bufferLength();
String charset = toString.charset();
if (value != null) writing.writeBytes(value.toString().getBytes(Charset.forName(charset)));
else writing.writeBytes(new byte[bufferLength]);
}
}
}
关于循环引用
个人认为循环引用是模型设计的问题, StructSerializer暂时没有对循环引用进行检测,也没有规避, 模型设计时还请各位开发大佬尽量规避, 不然是会出现死循环的哟
如下图
@Data
class A {
private clong id;
private clong phoneNumber;
// 第一种, 自己引用自己. 解析时会死循环
private A selfTypeField;
// 第二种, 循环引用. 解析时同样会死循环
private B b;
}
@Data
class B {
// A和B相互引用
private A a;
}
末尾附上Nettyx内置的C和C++语言类型, 实际开发直接使用这些内置的基础类型构建你的struct即可