Sunday, October 19, 2008

深入浅出AMF3

深入浅出AMF3

By Oscar

1 不同的序列化方式

让我们先考虑这样一个需求:客户端发送两个整数给服务器,服务器接收数据,计算它们的和,然后把结果传回给客户端。

我们如何序列化这两个整型数据呢?

1.1 传统的序列化方式

首先,我们考虑传统的序列化方式,一个报头,加上具体数据,大致如下:

两个字节消息类型

两个字节消息长度

四个字节value1

四个字节value2

也就是说,以这种包装数据的方式,我们需要发送12字节。

1.2 Java对象序列化方式

接下来,我们尝试一下Java对象序列化方式,我们需要定义这样的类:

public class SumUpMessage implements Serializable

{

private static final long serialVersionUID = 3357976281271608025L;

private int value1;

private int value2;

public int getValue1()

{

return value1;

}

public void setValue1(int value1)

{

this.value1 = value1;

}

public int getValue2()

{

return value2;

}

public void setValue2(int value2)

{

this.value2 = value2;

}

}

测试发现,我们需要发送69字节。

测试程序如下:

SumUpMessage data = new SumUpMessage();

ByteArrayOutputStream baos = new ByteArrayOutputStream();

ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.writeObject(data);

int storedSize = baos.size();

System.out.println(String.format("SumUpMessage: 需要占用%d字节", storedSize));

1.3 AMF3方式

最后,让我们AMF3的封包方式,我们使用BlazeDS中的amf3相关API实现如下的测试程序:

SerializationContext serializationContext = SerializationContext.getSerializationContext();

ByteArrayOutputStream baos = new ByteArrayOutputStream();

Amf3Output amf3Output = new Amf3Output(serializationContext);

AmfTrace trace = new AmfTrace();

amf3Output.setDebugTrace(trace);

amf3Output.setOutputStream(baos);

try

{

amf3Output.writeObject(new SumUpMessage());

amf3Output.flush();

int storedSize = baos.size();

System.out.println(String.format("SumUpMessage.amf3: 需要占用%d字节", storedSize));

amf3Output.close();

}

catch (IOException e)

{

e.printStackTrace();

}

我们需要发送43字节。

1.4 总结

通过上述的粗糙测试,我们对于AMF3的封包有个大概的了解。AMF3的『压缩率』对于特别在乎网络带宽的公司,可能不是那么满意。但是它能提高我们日常的开发效率。因为amf3flash默认的封包方式,对于flash程序员,可以忽略字节流的概念,所有input/output都是对象或者原子数据(primitive type)。

让我继续吧。下一章我们将深入SumUpMessage被封装的过程。

2 AMF3详细分析

上面的测试,我们并没有实现Externalizable接口,也就是说,整个封装过程都是按照默认方式进行的。

接下来,我们实现Externalizable接口。

public void writeExternal(ObjectOutput out) throws IOException

{

out.writeInt(value1);

out.writeInt(value2);

}

现在只需要33字节。

接下来,我们用writeObject替代writeInt,即

public void writeExternal(ObjectOutput out) throws IOException

{

out.writeObject(value1);

out.writeObject(value2);

}

此时,只需占用29字节。

现在的问题是,为什么这三种不同的实现方式,而带来完全不同的效果呢?

好的,让我们继续分析吧。

2.1 默认的实现


0x0A: object marker

0x23: trait信息,包含是否externalizabledynamic以及SumUpMessage类的属性数量。

XXXXXXXX XXXXXXXX XXXXXXXX XXXXDE11

E: 表示是否实现了Externalizable接口

D: 是否是动态的

X: 表示所含属性的数量

整个整型数据以29-bit编码方式进行优化处理,所以只占用一个字节。

0x2D: 类名所占字节数,2*类名的字符数目+1

0x63 6F 6D 2E 6F 73 63 61 72 2E 53 75 6D 55 70 4D 65 73 73 61 67 65: 类名的字符串

0x0D: 属性名1所占字节数,也是2*属性名的字符数目+1

0x76 61 6C 75 65 31: 属性名1的字符串

0x0D: 属性名2所占字节数,也是2*属性名的字符数目+1

0x76 61 6C 75 65 32: 属性名2的字符串

0x04: integer marker

0x00: 属性1的值

0x04: integer marker

0x00: 属性2的值

2.2 实现Externalizable接口,使用writeInt输出


0x0A: object marker

0x07: trait信息

0x2D: 类名所占字节数

0x63 6F 6D 2E 6F 73 63 61 72 2E 53 75 6D 55 70 4D 65 73 73 61 67 65: 类名的字符串

0x00 00 00 00: 我们在writeExternal中实现的,写属性1的值

0x00 00 00 00: 我们在writeExternal中实现的,写属性2的值

2.3 实现Externalizable接口,使用writeObject输出


0x0A: object marker

0x07: trait信息

0x2D: 类名所占字节数

0x63 6F 6D 2E 6F 73 63 61 72 2E 53 75 6D 55 70 4D 65 73 73 61 67 65: 类名的字符串

0x04: 我们在writeExternal中实现的,integer marker

0x00: 我们在writeExternal中实现的,属性1的值

0x04: 我们在writeExternal中实现的,integer marker

0x00: 我们在writeExternal中实现的,属性2的值

2.4 29bit编码

29bit编码的目的是尽可能使用少量的内存来表示一个integer数据。它把一个byte的最高bit用来标记下一个byte是否属于这个integer 32bit中去掉用作标记的3bit,就剩下29bit用作存放实际数据。

3 下一步

下一步,我将把AMF3序列化和反序列化功能添加到Grizzly ARP中,另外实现wireshark插件解析AMF3格式协议。敬请期待。

4 参考资料

No comments: