对于一个网络通信,首先需要解决的就是对数据的序列化和反序列化处理,在ZooKeeper中,使用了Jute这以序列化组件来进行数据的序列化和反序列化操作。
使用Jute进行序列化
下面我们通过一个例子来看看如何使用Jute来完成Java对象的序列化和反序列化。假设我们有一个实体类MockReqHeader(代表了一个简单的请求头),其定义如下所示。
public class MockReqHeader implements Record {
private long sessionId;
private String type;
public MockReqHeader() {
}
public MockReqHeader(long sessionId, String type) {
this.sessionId = sessionId;
this.type = type;
}
public long getSessionId() {
return sessionId;
}
public void setSessionId(long sessionId) {
this.sessionId = sessionId;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public void deserialize(InputArchive a_, String tag) throws IOException {
a_.startRecord(tag);
sessionId = a_.readLong("sessionId");
type = a_.readString("type");
a_.endRecord(tag);
}
public void serialize(OutputArchive a_, String tag) throws IOException {
a_.startRecord(this, tag);
a_.writeLong(sessionId, "sessionId");
a_.writeString(type, "type");
a_.endRecord(this, tag);
}
}
上面即为一个非常简单的请求头定义,包含了两个成员变量:sessionId和type。接下来我们看看如何使用Jute来进行序列化和反序列化。
public class MockReqHeaderTest {
public static void main(String[] args) throws Exception {
// 开始序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
new MockReqHeader(0x34221eccb92a34el, "ping").serialize(boa, "header");
// 这里通常是TCP网络传输对象
ByteBuffer bb = ByteBuffer.wrap(baos.toByteArray());
// 开始反序列化
ByteBufferInputStream bbis = new ByteBufferInputStream(bb);
BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
MockReqHeader header2 = new MockReqHeader();
header2.deserialize(bbia, "header");
bbis.close();
baos.close();
}
}
上面这个代码片段演示了如何使用Jute来对MockReqHeader对象进行序列化和反序列化,总的来说,大体可以分为4步。
- 实体类需要实现Record接口的serialize和deserialize方法。
- 构建一个序列化器BinaryOutputArchive。
- 序列化
调用实体类的serialize方法,将对象序列化到指定tag中去。例如在本例中就将MockReqHeader对象序列化到header中去。
- 反序列化
调用实体类的deserialize,从指定的tag中反序列化出数据内容。
深入Jute
从上面的讲解中可以看出,使用Jute来进行Java对象的序列化和反序列化是非常简单的。接下来我们再通过Record序列化接口、序列化器和Jute配置文件三方面来深入了解下Jute。
Record接口
Jute定义了自己独特的序列化格式Record,ZooKeeper中所有需要进行网络传输或是本地磁盘存储的类型定义,都实现了该接口,其结构简单明了,操作灵活可变,是Jute序列化的核心。Record接口定义了两个最基本的方法,分别是serialize和deserialize,分别用于序列化和反序列化:
- public void serialize(OutputArchive archive, String tag) throws IOException;
- public void deserialize(InputArchive archive, String tag) throws IOException;
所有实体类通过实现Record接口的这个方法,来定义自己将如何被序列化和反序列化。其中archive是底层真正的序列化器和反序列化器,并且每个archive中可以包含对多个对象的序列化和反序列化,因此两个接口方法中都标记了参数tag,用于向序列化器和反序列化器标识对象自己的标记。
我们重点来看下面中对序列化和反序列化接口的实现:
public void deserialize(InputArchive a_, String tag) throws IOException {
a_.startRecord(tag);
sessionId = a_.readLong("sessionId");
type = a_.readString("type");
a_.endRecord(tag);
}
public void serialize(OutputArchive a_, String tag) throws IOException {
a_.startRecord(this, tag);
a_.writeLong(sessionId, "sessionId");
a_.writeString(type, "type");
a_.endRecord(this, tag);
}
我们可以看到,在这个样例实现中,serialize和deserialize的过程基本上是两个相反的过程,serialize过程就是将当前对象的各个成员变量以一定的标记(tag)写入到序列化器中去;而deserialize过程则正好相反,是从反序列化器中根据指定的标记(tag)将数据读取出来,并赋值给相应的成员变量。
OutputArchive和InputArchive
OutputArchive和InputArchive分别是Jute底层的序列化器和反序列化器接口定义。在最新版本的Jute中,分别有BinaryOutputArchive/BinaryInputArchive、CsvOutputArchive/CsvInputArchive和XmlOutputArchive和XmlOutputArchive/XmlInputArchive三种实现。无论哪种实现,都是基于OutputStream和InputStream进行操作。
BinaryOutputArchive对数据对象的序列化和反序列化,主要用于进行网络传输和本地磁盘的存储,是ZooKeeper底层最主要的序列化方式。CsvOutputArchive对数据的序列化,则更多的是方便数据对象的可视化展示,因此被使用在toString方法中。最后一种XmlOutputArchive,则是为了将数据对象以XML格式保存和还原,但是目前在ZooKeeper中基本没有被使用到。
zookeeper.jute
很多读者在阅读ZooKeeper的代码的过程中,都会发现一个有趣的现象,那就是在很多ZooKeeper类的说明中,都写着“File generated by hadoop record compiler. Do not edit.”,这时因为该类并不是ZooKeeper的开发人员编写的,而是通过Jute组件在编译过程中动态生成的。在ZooKeeper的src目录下,有一个名叫zookeeper.jute的文件:
在这个文件中定义了所有实体类的所属包名、类名以及该类的所有成员变量及其类型。例如上面的代码片段就分别定义了org.apache.zookeeper.data.Id和org.apache.zookeeper.data.ACL两个类。
有了这个定义文件后,在源代码编译阶段,Jute会使用不同的代码生成器来为这些类定义生成实际编程语言(Java或C/C++)的类文件。以Java语言为例,Jute会使用JavaGenerator来生成相应的类文件,这些类文件都会被存放在src\java\generated目录下。需要注意的一点是,使用这种方式生成的类,都会实现Record接口。