JavaStruct 字节流的处理

转-http://blog.csdn.net/jazzsoldier/article/details/75669757

Java 结构体之 JavaStruct 使用教程<一> 初识 JavaStruct – JAZZSOLDIER的专栏 – CSDN博客

Javastruct 是什么

简而言之,Javastruct 是一个第三方库,用于像处理 C 或者 C++ 结构体那样处理 java 对象。也即利用 Javastruct 可以在 java 上实现类似于结构体的功能和操作。

Javastruct 的用途

在 java 或者 Android 应用程序与一些嵌入式设备通讯或者C、C++ 应用程序通讯时,比如网络、无线、蓝牙、串口通讯等场景,由于实际传输时我们希望在通讯时使用自定义的协议格式,这样就必须进行字节流的处理。比如:协议格式为:“包头 + 命令码 + 长度 + 数据 + 校验 + 包尾“ 这种格式,使用 java 默认的方法就需要每条协议设置或解析每个 byte,十分繁琐。这时,会很怀念 C 语言的结构体和指针,这回极大节省我们的代码逻辑和工作量。这就是我要使用并介绍 JavaStruct 的原因了。除此之外,该库还可以用作一个简单但空间利用高效的序列化方法。

JavaStruct 库支持原语、数组、C字符串及嵌套类等。同时支持大端和小端字节序。 Javastruct 也可以使用 ArrayLengthMarker 注释来处理其他域中定义长度的域。 在后面的例程和文档中会逐步涉及到。

Javastruct 的性能

对于简单的类,JavaStruct 要比 Java 序列化更快速,对于复杂和嵌套的类,通常与 Java 序列化性能相同。通常 JavaStruct 产生小2到3倍的输出结果。

JavaStruct 未来工作

1. 更好的命名和统一的外观类

2. 详细的文档

3. 基于 ByteBuffer 的结构体序列化(目前是基于stream流的方式)

4. 更好的性能

5. 更多单元测试

6. 数据对齐支持

7. 位域

8. Union 联合体

运行条件

任何具有 Java 5 或者更高的 JVM 的系统

其他类似项目

 Javolution 也有很好的结构体实现,Javolution 结构体使用特殊类来表示域,JavaStruct 有不同途径并且使用 POJO 和 Java 5 注释。

Java 结构体之 JavaStruct 使用教程<二> JavaStruct 用例分析 – JAZZSOLDIER的专栏 – CSDN博客

使用环境

前一篇在介绍 JavaStruct 类时指定了使用库使用环境为 Java 5 及以上,也即开发我们使用的 JDK 版本为1.5及以上就可以了。以下讲解的用例可以直接将 code 直接粘贴到 java 的 main 函数中执行就可以了,后面会给出测试用例和结果。

使用方法

JavaStruct 类用于打包和解包结构体,也即使用方法为用该类的 pack 与 unpack 方法将定义的 struct 类转换为字节流,或者将接收的字节流转换为我们定义的 struct 类。如下所示为一个简单的用于检查结构体类的单元测试方法。结构体成员变量前有一个排序数值,也即注解方式为  @StructField(order = 0) 这是因为 Java JVM 规范没有任何有关类成员排序的说明。使用此方式定义的结构体成员会按照具体实现中使用的order进行成员内存排序,因此每一个结构体成员必须提供一个 order 数值。如下所示:

	@StructClass 
	public class Foo {
		
		@StructField(order = 0)
		public byte b;
		
		@StructField(order = 1)
		public int i;
		
	}

注意,注解 @StructClass 以及 @StructField 不能省略。结构体定义完成后,使用 pack 与 unpack 方法进行类型转换,如下所示为完整示例:

public class test {
	
	@StructClass 
	public class Foo {
		
		@StructField(order = 0)
		public byte b;
		
		@StructField(order = 1)
		public int i;
		
	}
	
	public void TestFoo() {
		try { 
			// Pack the class as a byte buffer 
			Foo f = new Foo();
			f.b = (byte)1;
			f.i = 2; 
			byte[] b = JavaStruct.pack(f);
			for (int i = 0; i < b.length; i++) {
				System.out.printf("b[%d]: %d\n", i, b[i]);
			}
			
			// Unpack it into an object
			Foo f2 = new Foo();
			JavaStruct.unpack(f2, b);
			System.out.println("f2.b: " + f2.b);
			System.out.println("f2.i: " + f2.i);
			
		} catch(StructException e) { 
			e.printStackTrace();
		} 
	}
	
	public static void main(String args[]) {
		test t = new test();
		t.TestFoo();
	}
}

直接观察输出结果:

从输出结果可以看到,我们定义的结构体被转换成了 5 个字节的 byte 数组(int 占 4 个字节),可以看出来 int 数据的地字节保存在了 byte 数组的高地址,可见使用 pack 打包时为大端排序。当然,实际应用时我们需要根据需求决定是使用大端还是小端排序。在 pack 与 unpack 方法指定就可以了,具体 pack 默认为大端还是小端排序和处理器架构及编译器版本都有关系,因此要在项目应用中以真实结果为准。如改成小端:

byte[] b = JavaStruct.pack(f, ByteOrder.LITTLE_ENDIAN); 

如果运行中发生错误,结构体操作会抛出 StructException 异常。

Struct 类也可以直接与 Stream 流一起使用。可以参考 Photoshop ACB 文件读取 example,这里就不作详细分析了。片段如下:

	public void TestACB() {
		public void read(String acbFile) { 
			try { 
				FileInputStream fis = new FileInputStream(new File(acbFile)); 
				header = new ACBHeader(); 
				StructUnpacker up = JavaStruct.getUnpacker(fis, ByteOrder.BIG_ENDIAN); 
				up.readObject(header); 
			}
		}
	}

原型相关

对于使用原型,要注意对于 private 与 protected 成员需要用相应的gettersetter 方法。Transient 成员会被自动排除。如下所示:

	@StructClass 
	public class PublicPrimitives implements Serializable { 
		
		@StructField(order = 0) 
		public byte b;
	
		@StructField(order = 1)
		public char c;
	
		@StructField(order = 2)
		public short s;
	
		@StructField(order = 3)
		public int i;
	
		@StructField(order = 4)
		public long lo;
	
		@StructField(order = 5)
		protected float f;
	
		@StructField(order = 6)
		private double d;
	
		transient int blah;
		transient double foo;
	
		public float getF() {
		    return f;
		}
	
		public void setF(float f) {
		    this.f = f;
		}
	
		public double getD() {
		    return d;
		}
	
		public void setD(double d) {
		    this.d = d;
		}
	
		public boolean equals(Object o){
		    PublicPrimitives other = (PublicPrimitives)o;
		    return (this.b == other.b 
		        && this.c == other.c
		        && this.s == other.s
		        && this.i == other.i
		        && this.lo == other.lo
		        && this.f == other.f
		        && this.d == other.d);
		}
	}

数组相关

使用数组有一些先决条件。当解包时,数组一定要分配充足的空间。只有那些在另一个字段中使用ArrayLengthMarker(见下文) 定义长度的数组可以为 null,这些数组在解包时会自动分配空间。除此之外的数组定义不能为空和未初始化状态。使用如下所示:

	@StructClass 
	public class PublicPrimitiveArrays { 
		@StructField(order = 0) 
		public byte[] b = new byte[5];

		@StructField(order = 1)
		public char[] c = new char[10];
	
		@StructField(order = 2)
		public short[] s;
	
		@StructField(order = 3)
		public int[] i;
	}

数组长度标记相关

数组长度标记(Array Length Markers)对于长度在另一个字段中定义的字段十分有用。参见以下示例,这是个特殊的字符串结构体,其有一个长度字段以及紧跟其后的对应这个长度的 16 位字符。也即结构为:

| Length | UTF-16 Characters … |

为了处理这种情况,必须把这些字符串表示为一个特殊的结构体类。长度字段应该注解为“ArrayLengthMarker”。通过这种方式,javastruct 可以在打包及解包操作中操作数组字段时自动使用长度字段中的值。示例如下:

	@StructClass
	public class AString {
		
		@StructField (order = 0 )
		@ArrayLengthMarker (fieldName = "chars")
		public int length;

		@StructField (order = 1)
		public char[] chars;

		public AString(String content){
		    this.length = content.length();
		    this.chars = content.toCharArray();
		}
	}

 

 

 

 

Java 结构体之 JavaStruct 使用教程<三> JavaStruct 数组进阶 – JAZZSOLDIER的专栏 – CSDN博客

经过前面两篇博客的介绍,相信对于 JavaStruct 的认识以及编程使用,读者已经有一定的基础了。只要理解和实践结合起来,掌握还是很容易的。下面进行一些数组使用方面的实例说明及演示。

在结构体类中使用数组有几种方式,可以使用静态指定大小的方式也可以通过使用 ArrayLengthMaker 进行动态分配。数组的大小既可以在类中定义,也可以在定义对象后通过对象进行指定。

静态指定方法

如下所示:

	@StructClass 
	public class ArrayStruct { 
		@StructField(order = 0) 
		public byte[] b = new byte[4];

		@StructField(order = 1)
		public char[] c = new char[2];
	
		@StructField(order = 2)
		public short[] s;
	
		@StructField(order = 3)
		public int[] i;
	}
	
	public void ArraysTest() {
		ArrayStruct arr = new ArrayStruct();
		Arrays.fill(arr.b, (byte) 1);
		Arrays.fill(arr.c, (new String("我")).charAt(0));
		arr.s = new short[0];
		arr.i = new int[0];
		try {
			byte[] b = JavaStruct.pack(arr);
			for (int i = 0; i < b.length; i++) {
				System.out.printf("b[%d]: %d\n", i, b[i]);
			}
			ArrayStruct arr2 = new ArrayStruct();
			arr2.s = new short[0];
			arr2.i = new int[0];
			JavaStruct.unpack(arr2, b);
			System.out.println("arr2.b: " + Arrays.toString(arr2.b));
			System.out.println("arr2.c: " + Arrays.toString(arr2.c));
		} catch (StructException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void main(String args[]) {
		test t = new test();
		t.ArraysTest();
	}

输出结果如下:

Note:在数组中未分配空间的数组一定要在定义对象后分配空间,不然会报空指针错误。其次解析与反解析要注意定义的数组大小匹配,这一点也要切记,不然会报错。与 C 中结构体不同的是,类中指定大小后,可以创建对象后,再重新分配数组大小。这时,数组大小就以重分配大小为准,这也是一种灵活性的体现。当然,原理就是类定义是不占用内存空间的,只有定义对象后才有对应空间存在。

动态分配方法

通过使用 ArrayLengthMaker 的方式,javastruct 可以在打包及解包操作中操作数组字段时自动使用长度字段中的值。直接看如下实例:

	@StructClass
	public class AString {
		
		@StructField (order = 0 )
		@ArrayLengthMarker (fieldName = "chars")
		public int length;

		@StructField (order = 1)
		public char[] chars;

		public AString(String content){
		    this.length = content.length();
		    this.chars = content.toCharArray();
		}
	}
	
	public void TestAString() {
		//构造 str 对象时,执行相应构造方法后,长度字段为4。
		AString str = new AString("我爱中国");
		try {
			byte[] b = JavaStruct.pack(str, ByteOrder.BIG_ENDIAN);
			for (int i = 0; i < b.length; i++) {
				System.out.printf("b[%d]: %d\n", i, b[i]);
			}
			AString str2 = new AString("");
			JavaStruct.unpack(str2, b, ByteOrder.BIG_ENDIAN);
			System.out.println("str2: " + str2.length);
			System.out.println("str2: " + Arrays.toString(str2.chars));
		} catch (StructException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void main(String args[]) {
		test t = new test();
		t.TestAString();
	}

输出结果如下:

可以看到对象共占有12个字节的空间,长度为int型,占4个字节。后面的 char 型数组共占8个字节,可确认其长度为4。以上示例即为完整的打包与解包过程了。