amf的解析

感觉好久没写东西了,上次写了c++利用json与flex通信后,其中一个错误,现在自己发现了,以前对AMF了解不够深刻,惭愧之至。上次说C++中接收到数据后要下移四位,其实前面那四位是AMF类型标记,准确的来说,并不一定是四位,所以这次,跟大家来讨论下AMF的数据结构。可能大家在网上找了后,发现关于AMF解析很少,甚至没有,再或者解析不够准确,那么这次,大家可参考这片文章了。

AMF是AS中的一种数据格式,非常简洁,那为什么简洁呢,以前一直不清楚,看了AMF的数据格式后,才发现原来真的很简洁,数据量小。。呵呵,废话了这么多,下面我们来看下吧。

AMF一共有13种数据类型,每种类型都用一个字节来表示

undefined =  0

null      1

false     2

true      3

int       4

double    5

string    6

xml-doc   7

date      8

array     9

object    10

xml       11

byte-array=  12

以上类型一目了然,其实as中ByteArray.writeObject的时候,都是以AMF格式写入字节数组中,那么下面我们来逐个利用程序分析下吧,每种数据类型定义好后,都利用ByteArray.writObject写入数据,再输出。

undefined 就不说了,遇到不认识的类型,就0吧。

null:字节数组={1},很简单,null类型为1,所以后面不带任何东西。

false:字节数组={2},同上。

true:字节数组={3},同上。

int:这时候我们要真正开始讲AMF了,我们分别以10,100,1000来分析下。

1、var byteArray:ByteArray = newByteArray();

  byteArray.writeObject(a);

   a= 10 -> {4,10}

   a= 10000 -> {4,206,16}

   a= 100000 -> {4,134,141,32}

   a= 10000000 -> {4,130,177,150,128}

每次都是以类型字节开始,4表示是int型。int最大占4个字节,超出后AMF就会以double类型发送。后续每个字节的最高位,表示下一字节是否有效。

{4,10},10转成二进制 = 00001010,由于最高位为0,所以后一字节无效,那么直接返回该int值为10

{4,206,16},206转成二进制 = 11001110,由于最高位为1,所以后一字节仍然有效,此时将1100 1110最高位置0并左移7位,变成0010 0111 00000000。再看16 = 00010 000,由于最高位为0,故下一字节无效,两字节做"|"运算0010 0111 0000 0000| 00010 000便得出结果10000。

其他2个数依次类推大家自己算吧,余下的明天再说吧。今天太累了


昨天写了一点,那今天继续吧。
double:在AS中大于int的都需要用double表示,当然也包含浮点型了。我们以10.123来举例,通过AS写入字节数组,10.123的字节={5,64,36,62,249,219,34,208,229},一共8个字节,第一个字节5当然是表示类型了。其实AMF里的double就一算法,我们直接上c++代码看吧。
byte byteArray[] ={5,64,36,62,249,219,34,208,229};
LONG64 value = 0;
for(int i = 0;i < 8;i++)
value +=((LONG64)byteArray[position++] & 255)<< (8 * (7 - i));
int s = (value>> 63) == 0 ? 1 : -1;
int e = (int)((value>> 52) &0x7ffL);
LONG64 m =  (e == 0)? (value & 0xfffffffffffffL)<< 1 : (value &0xfffffffffffffL) | 0x10000000000000L;
double ret = s * m * pow((double)2,e- 1075);
通过以上计算后,就得到我们的结果了。
 
string:接下来我们来看下字符串的解析,这个我们要着重来讲下。我们以"abcdefg"与"程序员"2个字符串为例。首先也通过as写入字节数组,得到字节数组。
1、"abcdefg"={6,15,97,98,99,100,101,102,103}
第一个字节6当然也是类型啦。接下来,肯定是字符串的长度了,不知道长度怎么知道字符串呢。利用昨天讲的解析int的方法,把字符串长度解析出来,就是len=15,这时候我们发现长度不对啊。是的,我们要将读到的int右移1位,15/ 2 =7,这就是我们的长度。有些人可能会问,为什么要右移1位,那一位有什么用处呢?还记得我们之前讲过AMF格式很简洁,那为什么简洁呢,其实就在这里了。
AMF中,会将一个数据包中读到的string按顺序保存到一个列表中,记住,这个列表只保存string。以便再出现一样的字符串,可以直接从列表中读,不需要重复发送,这样,便可以减少发送的字节数了。比如
obj.name = "vigour";
obj.school = "vigour";
我们将这个obj通过socket发送,其实"vigour"这个字符串只是通过name发送了一次,那school怎么办?当然就是标记下"vigour"是第几个字符串发送的。这时候,我们便知道了那移掉的一位有什么用处了。假设我们读到的int是a
if((a & 1) == 0)表示最后位如果是0,则表示该字符串前面已发送过,是第a / 2次发送的。
if((a & 1) == 1)表示最后位如果是1,则表示该字符串是全新发送,长度 = a / 2
这时候,我们知道了长度是怎么读取的。接下来,就根据长度读取后面15 / 2= 7位字节,利用ascii码转换成相应的字符就行了。
 
2、"程序员"={6,19,231,168,139,229,186,143,229,145,152}
我们上面分析了字符的发送,作为我们,当然还有如何发送中文字符。哎,由于计算机语言都使用英文字符,使得我们讲中文看中文的人屡屡吃亏,常常出现什么中文乱码。所以,国人努力,发明的计算机或计算机语言都使用中文的(E语言就不提了。。)。
言归正转,上面我们解析了英语字符,其实这边就比较容易了。前面长度都一样的算法,就是在利用ascii码转换的时候要加判断。因为,最开始,ascii码只有128位,故此处,若小于128就可认为是英文字符,可直接转换。那如果大于128咋办呢?这里判断是否大于128,是因为要将字节右移4位,看代码。
switch(c>> 4)
 
   {
 
     //ASCII码小于127
 
     case 0:
 
     case 1:
 
     case 2:
 
     case 3:
 
     case 4:
 
     case 5:
 
     case 6:
 
     case 7:
 
     {
      处理ascii码
 
      }
   case12:
 
     case 13:
 
    {
 
         if((count += 2) > len) break;
      char char2 = byteArray[count - 1];
      if((char2 & 192) != 128) break;
      chArr[chCount] = (wchar_t)((c & 31)<< 6 | char2 &63);
      break;
 
    }
   case14:
 
     {
 
       if((count += 3) > len) break;
    charchar2 = byteArray[count - 2];
    charchar3 = byteArray[count - 1];
    if((char2& 192) != 128 || (char3 & 192) !=128) break;
    chArr[chCount]= (wchar_t)((c & 15)<< 12 | (char2 & 63)<< 6 | (char3 & 63)<< 0);
    break;
 
    }
   case 8:
 
     case 9:
 
     case 10:
 
     case 11:
 
     default:break;
其实这里我们有必要说下utf8这种编码,在AS中发送的编码格式都是utf8,所以,了解utf8有助于我们知道为什么AMF是这样编码字符串的,为什么要右移4位呢,不是3位,也不是2位。
utf8的编码规则很简单,使用1-4个字节来表示。2条规则:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,utf8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
我们来看下,1-4个字节编码规则的第一个字节。
1字节:0xxxxxxx,当第一位是0时,便表示这是单字节字符。
2字节:110xxxxx,当前2位是1的时候,便表示这是俩字节字符。
3字节:1110xxxx,当前3位是1的时候,便表示这是三字节字符。
4字节:11110xxx,当前4位是1的时候,便表示这是四字节字符。
所以,上面我们为什么要右移四位知道了吧。是为了判断是几字节字符,当是1字节时,0xxx最大表示7,最小为0。当是2字节时,110x范围是12-13。当是3字节时,1110便是14。因为只有极少数字符是使用4字节,所以AMF里不考虑4字节的。我们搞清楚utf8编码规则后,上面的代码便容易理解了。我们分析下双字节
if((count += 2) > len)break;      来判断下一字节是否有效
if((char2 & 192) !=128)break;    这个是来判断第二个字节是否以10开头
((c & 31)<< 6 | char2 &63);      这里是将第一字节的前三位与第二字节的前两位去掉并连接,因为我们讲过除了那几位,余下的才是unicode码,也就是我们需要的进制。
其实三字节的也是一样的道理,留给大家自己去解析吧,到这里,我们基本把string解析完了。
有点小累,剩下的,改天再打吧。如果有看不懂的,可能是我表达不清楚,大家可以自己试下,或者跟我讨论也行。
 

今天我们来继续AMF解析,首先,我们来讲下XML。

XML(readXML):关于AMF中发送XML,都是将XML-DOC转换成字符串来发送。那解析的时候,也就成了解析字符串了。我们将XML的字符串解析出来后,利用W3C标准将字符串转换成XML(具体转换方法自己google下吧)。

 

DATE(readDate):假若一个日期利用AMF二进制表示是{8,1,66,114,-27,-59,-62,-26,80,0},那么我们就将它解析成日期,看下是什么时候。其实一般计算机里的日期也就是相对于1970年1月1日起经过的毫秒数,AMF中会将该毫秒数以double的形式发送,这样我们就简单了,根据前面讲的double解析方式,就可以知道日期了。但,这里,我们要注意点,DATE也会被认为是Object,就跟String,我们要先读取int来判断该日期前面是否已经发送,是第几次发送的。

int a = readInt();

if(a & 1 == 0)//若低位是0,则表示该Date与前面发送的Date是一样的,第a>>1次发送的对象

else //读取double,然后转换成Date。

 

Object(readMyObject):我们先来讲下object类型,因为后面的array与byteArray都要用到Object的读法。下面我们一共讲下两种发送Object与自定义类的两种方式。

1、Object类

  下面是一段as代码:

              var obj:Object = new Object();

              obj.name = "yokia";

              obj.id = 1;

  我们定义上面这样一个对象,拥有name与id两个属性。我们利用byteArray转换成二进制。

  {10,11,1,9,110,97,109,101,6,11,121,111,107,105,97,5,105,100,4,1,1},接下来我们就分析这段二进制是什么意思。

首先,第一个字节10表示Object的类型,这个没什么好讲的。然后我们再按int的读取方式,读取一个int值ref,这个ref包含了很多信息,分为低4位与其余位。此时我们回忆上次讲的string,amf会每次将接受或发送的对象保存到一个列表中,假如再次遇到要发送或接收相同数据,则直接用第X次发送来表示这次的数据。

第1位:若为0,则表示该对象在此次发送的整个数据中已经存在,是在第ref>>1次发送的。

第2位:我们先放下等会讲。

第3位:若为1表示需要自定义序列化方式,利用true或flash来表示,设为externalizable。这个具体什么用,我还没有搞清楚,等下次搞清楚了再补上。

第4位:是否自定义类。若为0则表示发送的数据为自定义对象。否则就是普通的Object,也用true或false,用dynamic来表示。

第4-32位:表示自定义对象中成员变量的个数,用count=ref>>4来表示。

那我们根据上面的规则,来看下ref=11时的情况。11={00001011},由于为第一次发送该数据,所以第1位自然是1,表示前面没有发送过。第3位是0,表示利用默认的序列化方式,第4位是1,表示不是自定义对象。如果不是自定义对象,当然也不存在成员变量的说法了,所在成员变量的个数为0。

上面读取一个int后,我们再读取一个string类型的变量,表示as中发送自定义对象与后台对应类的路径,若发送的是普通Object,则该字符为"",即空字符,我们用className来表示。此时我们将上面二进制的第3位1读出来,我们可以知道该字符为空。

那么我们再回头看下,第2位是什么意思,其实这里,我们要把externalizable、dynamic、count和className四个变量组成一个自定义类TraitsInfo,那么第2位就表示这个自定义类前面是否发送过,假如发送过,则直接读取第ref>>2个对象就行了,不需要再根据其余字节来确定。

这里,我们就将这个ref给解析了,一个整型变量包含了这么多信息,真是不容易啊。

接下来,我们根据上面的TraitsInfo来判断,这个是否是自定义类,如果是自定义类,我们再根据count成员变量的个数,循环读取变量名称,即读取string,保存到TraitsInfo对象中。

我们知道了该数据不是自定义类,而是普通Object,则接下来我们读取string,即key值,二进制9,110,97,109,101转变成string后是name,此时我们判断读取到的字符串是否为空,空则表示读取结束。然后我们再读取object,即递归调用readObject的方法,二进制{6,11,121,111,107,105,97}中6表示是字符串类型,其余转换后便是"yokia"字符串了。然后我们一直用同样的方式读取id=1。当我们读取到最后一个字节1时,我们知道字符串为空,则表示读取结束。

通过上面,我们把as发送普通Object类解析完了。接下来,我们来看下自定义类是如何发送的。

 

2、自定义类

   package
 
      {
 
           [RemoteClass(alias="com.test.Student")]
 
            public class student
 
           {
 
                public var id:int;
 
 
 
                public var name:String;
 
 
 
                public function student(id:int,name:String)
 
                {
 
                      this.id = id;
 
  
 
                      this.name = name;
 
                 }

         }
 
      }

我们定义一个student类,并利用RemoteClass与后台类对应。com.test.Student为后台类的路径。

     var stu:student = new student(1,"yokia");

我们将stu利用AMF转换成二进制后是{10,35,33,99,111,109,46,116,101,115,116,46,83,116,117,100,101,110,116,5,105,100,9,110,97,109,101,4,1,6,11,121,111,107,105,97}

当我们利用上面方法读取ref后,externalizable=false,dynamic=false(即定义类),count=2

然后我们再读取{33,99,111,109,46,116,101,115,116,46,83,116,117,100,101,110,116}为字符串"com.test.Student",便是后台对应类的路径。然后我们根据成员变量的个数,循环调用readString的方法,读取各个成员变量名称,{5,105,100}="id",{9,110,97,109,101}="name"。

完成后,我们再根据成员变量的个数,递归调用readObject的方法,即解析AMF的方法{4,1}=1,{6,11,121,111,107,105,97}="yokia"。

那么上面,我们就将Object给解析完了。

今天我们最后再读下Array与byteArray这两种类型的解析,有了上次Object的解析方法,这次就简单了。
Array(readArray):关于Array我们也分两种情况,因为Array继承自Object,所以也具有动态赋值的特性。首先,我们讲具有正常下标的数组。
1、有以下AS代码:
       vararr:Array = new Array("aaa","bbb");
我们将其写入ByteArray后变成二进制={9,5,1,6,7,97,97,97,6,7,98,98,98}
首先,第1个字节9很简单,当然表示Array类型了,接下来我们再读取一个int,即ref=5,由于Array为对象型,所以前面讲过,只要是对象型,都要判断前面是否已经发送过,那么我们以如下来判断:
if(ref & 1 == 0)则第ref>>1次发送过
else 这个数组的长度是len = ref>> 1
然后,我们再读取一个String,至于为什么要读取String我们等下再讲,这里我们要判断下,如果读到的String是空,则结束读取String。那么我们读到1后,可以判断String是空的,所以停止读取String。
最后,我们便要根据上面数组的长度,进行循环读取值了。{6,7,97,97,97,6,7,98,98,98},循环两次,6表示是值的类型,就是2个String了。注意,这里可以是其他类型,比如自定义类型都要吧,只要调用readObject方法就行了。
接下来我们再来看下,由于Array继承Object,所以可以以键值对的方式出现。
2、以下AS代码:
      var arr:Array = new Array("aaa","bbb");
      arr["name"] = "yokia";
利用AMF变成二进制={9,5,9,110,97,109,101,6,11,121,111,107,105,97,1,6,7,97,97,97,6,7,98,98,98}
首先,第1个字节9与5跟上面意思相同也表示类型与长度len=2。这里长度为什么是2,不是3吗?我们有必要解释下,这里的长度是指,按正常数组下标的长度,并不算那些动态赋值的,所以这里是2。
我们先读取动态赋值的数组,当然要读key值先了,{9,110,97,109,101}读取String后变成"name",再读取Object,即调用readObject()方法,{6,11,121,111,107,105,97}变成成"yokia"。我们循环读取String-Object,值到String为空。{1}读取后,String便是空的了。所以停止。
接下来,我们就根据数组长度,来循环读取下面的值了。{6,7,97,97,97,6,7,98,98,98}读取后,变成了"aaa"与"bbb"两个字符,这两个字符便是我们数组里的值。
以上两种方法,我们就把Array解析完了。
 
ByteArray(readByteArray):字节数组解析起来就更简单了,因为从流中读取过来,本来就是字节了,所以后面的不需要额外的解析,只要解析头就行了。假设,我们将一个长度为9的字节利用AMF格式变成二进制={12,19,0,0,0,10,0,3,97,97,97},第1个字节12表示类型,由于ByteArray也是对象,所以再读取一个intref=19,表示是否发送过,我们知道并未发送,再将ref>>1就表示字节的长度了,然后根据长度将剩余的字节读过来就行了,由于全是字节,所以不需要额外转换,只要读取便好。
 
至此,我们将AMF的12种类型都解析完了,如果大家觉得哪里不对,可以和我讨论的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值