很多人竟然不知道不同语言数据类型的底层存储方式不同,不知道网络字节序的概念。再次写下做个记录。
一 让自己温习一下概念
二 给不知道的人做个参考
给予Intel X86架构上的语言都是Litle-Endian.而像sun等基于虚拟机的语言都是Big-Endian的。网络字节序是Big-Endian
如何记忆这两个编码方式的不同呢?主要记住两个参考点就可以搞定了。
所有的编码方式都是针对内存低位的。
所以Litle就是说明十六进制的低位存放在内存的地位。而Big 就是十六进制的高位存在内存的低位。
对于Big
高 -> 低
低 -> 高
对于Litle
高 -> 高
低 -> 低
所以参考上面有下面的理论了我们以内存的模型作为统一的参考的标准,也就是以上面箭头右侧作为对比。
所以big 到litle
先从内存的地字节开始,big中内存低位代表高字节,那么在Lite中高字节是存储在内存的高位。也就是说我们先从上面的Big图中右侧开始进行反向查找(内存到数据的模型) 低-> 高,然后在Litle中正象查找(数据到内存的模型) 发现是 高 ->高。所以Big 到Litle 的转换图形为(内存到内存的模型)
低 -> 高
高 -> 低
然后我们在根据上面的原理来分析Litle到Big的转化图
高 -> 低
低 -> 高
也许说了这么多,很多人仍然不知道中编码的区别。甚至在多种语言中写代码仍然没有感觉到这种编码的差异。更有人还用编码的方式证明了所有语言的编码方式都是一样的。
在不同语言中的网络通信的时候,这种编码的方式就很显然的看出来了。
在每种语言的独立层面上 将各种基本类型转化为byte数组,去验证编码的方式,是不可取的。因为各种基本类型的byte数组仍然按照十六进制的方式由低到高的组织。
所以需要用与网络流相应的类库去验证。才能看出编码的差异。下面拿JAVA和C#中的INT 编码来确定JAVA和C#的编码方式。
JAVA:
public static void main(String args[]) throws IOException{
int a = 42008;
for(int i=0;i<4;i++){
System.out.println((byte)(a>>(i*8)));
}
ByteBuffer bb = ByteBuffer.allocate(4);
bb.putInt(a);
byte[] t = bb.array();
for(int i=0;i<t.length;i++){
System.out.println(t[i]);
}
}
打印出来的结果如下:
24
-92
0
0
0
0
-92
24
通过上面的结果可以看出来int 到byte数组 完全按照十六进制的低位到高位进行的。但当我们将int写入网络流中后发现底层字节存储的方式发生了变化。高字节存放在低位中,低字节存放在高位中。
C#:
static void Main(string[] args)
{
int a = 42008;
for (int i = 0; i < 4; i++)
{
Console.WriteLine((byte)(a >> i * 8));
}
MemoryStream ms = new MemoryStream();
BinaryWriter bw = new BinaryWriter(ms);
bw.Write(a);
bw.Flush();
ms.Close();
byte[] b = ms.ToArray();
for (int i = 0; i < b.Length; i++) {
Console.WriteLine(b[i]);
}
}
打印结果:
24
164
0
0
24
164
0
0
通过这个结果中我们发现值不一样了(-92 和164),其实他们的二进制是一样的,主要原因是JAVA所有类型都是有符号类型,而C#byte表示无符号类型。通过数据发现C#低字节在低位,高字节在高位 和JAVA 完全不同。
通过两者的结果发现两者byte组织形式是一摸一样的。都是按照十六进制由低位到高位的存储。
在啰嗦一下JAVA、C# 中移位操作的 ,二进制细节问题。
JAVA、C#中 short、char、byte 的移位时,都会先将short、char、byte转化为int后,在进行移位,移动后的默认类型是int
byte a =12;
short b = a<<2;(编译时就不通过,因为移位后默认是int类型,int不能隐性转化为short)
int c = a<<24;
我们在看看两个语言中提供的类库,就更能验证上面的理论了
我们在JAVA中将一个int、short、char、byte 转化为二进制表示的字符串时,只能用一个函数就是Integer.toBinaryString 。Long 用Long.toBinaryString
在C#中我们将各种类型转化为二进制字符串 都通过Convert.ToString(xxx,2) 的方式。但我们通过打印出来的结果可以看出 int、short、char、byte、sbyte 打印出从来的二进制串都是32位。Long 打印出来的是64位的。
通过以上类库和数据结果,我们可以看出来在二进制转化的时候都是以32位为单位。
通过上面的理论,我们在编码转化时,特别是对一个类型中的byte字节出现负数的情况要特别注意细节了。如下一个Litle-Endian 的字节内存模型
低位 -12 类型的低字节
7
0
高位 0 类型的高字节
二进制字符串是0111 1000 1100
而要在Big-Endian编码中完全还原这个数值,就需要进行为转化了,转化后的内存模型应该是
低位 0 类型的高字节
0
7
高位 -12 类型的低字节
二进制字符串是0111 1000 1100
上面的说明只是在内存结构上和byte[] 的组织上,还没有暴露出细节问题,下面要将byte[] 转化为int时就出现细节问题了,请仔细观察
big-Endian将byte[] 转化为Int 要遵循如下模式
byte[4] | byte[3] | byte[2] | byte[1]
有了这个模型我们应该得出如下的二进制 | 操作
0000 0000 0000 0000 0000 0000 1111 0100(补码) |
0000 0000 0000 0000 0000 0000 0111 0000 |
0000 0000 0000 0000 0000 0000 0000 0000 |
0000 0000 0000 0000 0000 0000 0000 0000 |
上面的二进制 | 是正确解析的结果。细节就出在那个补码的地方。
如果们简单做如下操作
byte[4]<<0 | byte[3]<<8 | byte[2]<<16 | byte[3]<<24 就出问题了,因为byte[4]<<0 的结果是
1111 1111 1111 1111 1111 1111 1111 0100(补码)
将这个结果替换上面的结果,就发现二进制高位发生了变化。发生这个原因就是由于在移位时 byte[4] 被先转化为了int类型,而不是我们简单的将byte的二进制1111 0100 做移位,而是将1111 1111 1111 1111 1111 1111 1111 0100 做移位。
所以为了Litle-Endian 和Big-Endian转化的时候 我们应该严格按照如下公式进行
(byte[4]&0xFF)<<0 | (byte[3]&0xFF)<<8 | (byte[2]&0xFF)<<16 | (byte[1]&0xFF)<<24
通过&0xFF 自己主动转化为Int同时却掉由于补码的问题导致符号位填充问题。
JAVA 中的实现:
(byte[4]&0xFF)<<0 | (byte[3]&0xFF)<<8 | (byte[2]&0xFF)<<16 | (byte[1]&0xFF)<<24
C# 中的实现
(byte[4])<<0 | (byte[3])<<8 | (byte[2])<<16 | (byte[1])<<24
因为C#中byte默认的是无符号类型,所以不会造成补码表示,而是源码表示。就不会发生隐性转化到Int类型时的符号位填充问题。