Hello World
Spiga

浅谈字节序(Byte Order)及其相关操作

2010-02-10 23:05 by 老赵, 10691 visits

最近在为Tokyo Tyrant写一个.NET客户端类库。Tokyo Tyrant公开了一个基于TCP协议的二进制协议,于是我们的工作其实也只是按照协议发送和读取一些二进制数据流而已,并不麻烦。不过在其中涉及到了“字节序”的概念,这本是计算机体系结构/操作系统等课程的基础,不过我还是打算在这里进行简单说明,并且对.NET中部分类库在此类数据流处理时的注意事项进行些许记录与总结。

字节序(Byte Order)

说到程序间的通信,说到底便是发送数据流。我们一般把字节(byte)看作是数据的最小单位。当然,其实一个字节中还包含8个比特(bit)──有时候我奇怪为什么很多朋友会不知道bit或是它和byte的关系。当我们拿到一系列byte的时候,它本身其实是没有意义的,有意义的只是“识别字节的方式”。例如,同样4个字节的数据,我们可以把它看作是1个32位整数、2个Unicode、或者字符4个ASCII字符。

同样我们知道,在一个32位的CPU中“字长”为32个bit,也就是4个byte。在这样的CPU中,总是以4字节对齐的方式来读取或写入内存,那么同样这4个字节的数据是以什么顺序保存在内存中的呢?例如,现在我们要向内存地址为a的地方写入数据0x0A0B0C0D,那么这4个字节分别落在哪个地址的内存上呢?这就涉及到字节序的问题了。

每个数据都有所谓的“有效位(significant byte)”,它的意思是“表示这个数据所用的字节”。例如一个32位整数,它的有效位就是4个字节。而对于0x0A0B0C0D来说,它的有效位从高到低便是0A、0B、0C及0D——这里您可以把它作为一个256进制的数来看(相对于我们平时所用的10进制数)。

而所谓大字节序(big endian),便是指其“最高有效位(most significant byte)”落在低地址上的存储方式。例如像地址a写入0x0A0B0C0D之后,在内存中的数据便是:

Big Endian

而对于小字节序(little endian)来说就正好相反了,它把“最低有效位(least significant byte)”放在低地址上。例如:

Little Endian

对于我们常用的CPU架构,如Intel,AMD的CPU使用的都是小字节序,而例如Mac OS以前所使用的Power PC使用的便是大字节序(不过现在Mac OS也使用Intel的CPU了)。此外,除了大字节序和小字节序之外,还有一种很少见的中字节序(middle endian),它会以2143的方式来保存数据(相对于大字节序的1234及小字节序的4321)。

关于字节序的详细说明,您可以参考Wikipedia里的Endianness条目

相关.NET类库

BinaryWriter和BinaryReader

在.NET框架操作数据流的时候,我们往往会使用BinaryWriter和BinaryReader进行读写。这两个类中都有对应的WriteInt32或是ReadInt32方法,那么它们是如何处理字节序的呢?从MSDN上我们了解到BinaryReader使用小字节序读取数据。这意味着:

var stream = new MemoryStream(new byte[] { 4, 1, 0, 0 });
var reader = new BinaryReader(stream);
int i = reader.ReadInt32(); // i == 260

与之类似,自然BinaryWriter也是使用小字节序来写入数据。

BitConverter

有时候我们还会使用BitConverter来转化byte数组及一个32位整数(自然也包括其他类型),这也是涉及到字节序的操作,那么它们又是如何处理的呢?与BinaryWriter和BinaryReader的“固定策略”不同,BitConverter的行为是平台相关的。

首先,BitConverter有一个只读静态字段IsLittleEndian,它表示当前平台的字节序。由于我们为不同的CPU会安装不同的.NET类库,因此您现在如果通过.NET Reflector来查看这个字段会发现它被设置为一个常量true。那么接下来,BitConverter上的各个方便便会根据IsLittleEndian的值产生不同行为了,例如它的ToInt32方法:

public static unsafe int ToInt32(byte[] value, int startIndex)
{
    // ...

    fixed (byte* numRef = &(value[startIndex]))
    {
        if ((startIndex % 4) == 0)
        {
            return *(((int*)numRef));
        }
        if (IsLittleEndian)
        {
            return numRef[0] | (numRef[1] << 8) | (numRef[2] << 16) | (numRef[3] << 24);
        }

        return (numRef[0] << 24) | (numRef[1] << 16) | (numRef[2] << 8) | numRef[3];
    }
}

显然,这里会根据IsLittleEndian返回不同的值。

判断当前平台的字节序

在.NET Framework中BitConverter.IsLittleEndian字段是一个常量,也就是说它在编译期便写入了一个静态的值。那么我们如果想要通过代码来判断当前平台的字节序,又该怎么做呢?其实这很简单:

static unsafe bool IsLittleEndian()
{
    int i = 1;
    byte* b = (byte*)&i;
    return b[0] == 1;
}

这里我们通过检查32位整数1的第一个字节来确定当前平台的字节序。当然,我们也可以使用其他类型,例如:

static unsafe bool AmILittleEndian()
{
    // binary representations of 1.0:
    // big endian: 3f f0 00 00 00 00 00 00
    // little endian: 00 00 00 00 00 00 f0 3f
    // arm fpa little endian: 00 00 f0 3f 00 00 00 00
    double d = 1.0;
    byte* b = (byte*)&d;
    return (b[0] == 0);
}

这段代码来自mono的BitConverter类库,至于它为什么使用double而不是int,我也不是很清楚。

Buffer.BlockCopy方法

.NET类库中自带一个Buffer.BlockCopy方法,它的作用是将一个数组的字节——不是元素——复制到另一个数组中去。换句话说,一个长度为100的int数组经过完整的复制后,就变成了长度为50的long数组,因为一个int为4字节,而long为8字节。从文档上看,Buffer.BlockCopy是与字节序相关的,也就是说,同样的.NET代码在字节序不同的平台上得到的结果可能不同。因此,我建议在使用这个方法的时候多加小心。

面向特定字节序编程

我们知道,BitConverter的工作结果是和当前平台的字节序相关的,但是在很多时候,尤其是根据某个公开的协议进行通信编程的时候,是需要固定一个字节序的。例如Tokyo Tyrant便要求每个整数都以大字节序的方式来通信——无论是发送还是读取。为了保证.NET代码的平台无关性,我们不能直接使用BitConverter.GetBytes或ToInt32方法进行转化。那么我们该怎么办呢?最直观的方法自然是手动进行转换:

static int ReadInt32(Stream stream)
{
    var buffer = new byte[4];
    stream.Read(buffer, 0, 4);

    return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24);
}

由于我们可以通过BitConverter.IsLittleEndian来得到当前平台的字节序,我们也可以用它进行判断:

static int ReadInt32(Stream stream)
{
    var buffer = new byte[4];
    stream.Read(buffer, 0, 4);

    if (BitConverter.IsLittleEndian)
    {
        Array.Reverse(buffer);
    }

    return BitConverter.ToInt32(buffer, 0);
}

static void WriteInt32(Stream stream, int value)
{
    var buffer = BitConverter.GetBytes(value);

    if (BitConverter.IsLittleEndian)
    {
        Array.Reverse(buffer);
    }

    stream.Write(buffer, 0, buffer.Length);
}

此外,我们知道BinaryWriter和BinaryReader都是依据小字节序进行读写的,因此我们也可以利用这点来读写数据流。要不,接下来就由您试试看如何?

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

42 条回复

  1. JimLiu
    *.*.*.*
    链接

    JimLiu 2010-02-10 23:14:00

    沙发?
    为什么都是“ox”,不是应该是“0x”吗…………

    我2了,原来这个字体是这么奇葩

  2. Jacky Song
    *.*.*.*
    链接

    Jacky Song 2010-02-10 23:14:00

    复习基础知识啦
    1大B=8小b
    B:Byte是也
    b:bit是也

  3. 老赵
    admin
    链接

    老赵 2010-02-10 23:47:00

    @JimLiu
    靠,原来这个字体这么不和谐……

  4. Springfield
    *.*.*.*
    链接

    Springfield 2010-02-11 00:01:00

    学习知识了,字节序这个概念以前还真没接触过,以前认为只有一种排序方法了。 谢谢老赵传授经验了 呵呵

  5. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2010-02-11 00:14:00

    春节后阅读

  6. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-02-11 00:22:00

    字节序本质上是一种编码规则,它负责解释任意一个二进制值如何保存为一段字节。所以事实上,一个数值保存到以字节为单位的物理设备(磁盘、内存)上,至少有两重编码规则,一是如何用二进制流表示这个值,二是二进制流如何变为字节流。当然对于整型值而言,我们可以直观的得到如何用二进制流来表示,但对于复杂的浮点型,你就能看到第一种编码规则(如何用二进制流表示浮点型数值)的存在。

    我们也可以很显而易见的发现,如果一个数值用二进制流表示,其二进制位数不超过8位(即一个字节的大小),就没有什么大尾序小尾序。字节序表示的是需要用超过一个字节表示单一数值的方法,而不是数值在字节中如何对齐,或是多个连续数值如何排列。


    好像还是没说清楚,我再想想。。。。

  7. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2010-02-11 00:43:00

  8. Milo Yip
    *.*.*.*
    链接

    Milo Yip 2010-02-11 00:51:00

    小小的問題, "Unicode" 不是 16-bit 的。不過這個要說得對也很麻煩。應該說兩個UTF-16字符?

  9. Milo Yip
    *.*.*.*
    链接

    Milo Yip 2010-02-11 00:57:00

    整篇寫得很好, 圖也很好. 就是最後有點敗筆, 不配趙大風格 :)

  10. bigbigdotnet
    *.*.*.*
    链接

    bigbigdotnet 2010-02-11 01:35:00

    又弄懂了些东西!

  11. 非空
    *.*.*.*
    链接

    非空 2010-02-11 08:20:00

    哎 把Tokyo Tyrant 看成Tokyo Hot了,咳咳。。。和谐第一 和谐第一

  12. 老赵
    admin
    链接

    老赵 2010-02-11 09:07:00

    @Milo Yip
    其实我很弱的……你说的“最后”是指Unicode部分吗?我先划掉再去看看……

  13. iceboundrock
    *.*.*.*
    链接

    iceboundrock 2010-02-11 09:24:00

    老赵讲了字节序的物理类型,其实字节序有另外一种逻辑分类:网络字节序(big endian)和本机字节序(由处理器架构决定)。一般平台都提供了相应的API来做这两者之间的转换,例如windows的ntohl,htonl等。.net把这俩操作作为IPAddress类的静态方法提供了:HostToNetworkOrder和NetworkToHostOrder。所以与Tokyo Tyrant这类分布式系统通讯,必然是用网络字节序的。

  14. 老赵
    admin
    链接

    老赵 2010-02-11 09:32:00

    @iceboundrock
    这样?协议难道不是人定的吗?Tokyo Tyrant这种完全也可以定成little endian的啊。

  15. JimLiu
    *.*.*.*
    链接

    JimLiu 2010-02-11 09:34:00

    @Jeffrey Zhao
    这……好吧

  16. iceboundrock
    *.*.*.*
    链接

    iceboundrock 2010-02-11 09:37:00

    没错,协议是人订的。可能我说“必然”有点绝对。但是对于正常的网络应用来说,协议都是采用网络字节序的,算是惯例吧。通讯协议必须统一一种物理字节序对吧,为啥要跟惯例对着干呢?呵呵

  17. 老赵
    admin
    链接

    老赵 2010-02-11 09:40:00

    @iceboundrock
    喔……

  18. 老赵
    admin
    链接

    老赵 2010-02-11 09:47:00

    @JimLiu
    我换了个字体,呵呵。

  19. Milo Yip
    *.*.*.*
    链接

    Milo Yip 2010-02-11 10:25:00

    Jeffrey Zhao:
    @Milo Yip
    其实我很弱的……你说的“最后”是指Unicode部分吗?我先划掉再去看看……


    「要不,接下来就由您试试看如何?」 我說笑的 :)

  20. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-02-11 10:47:00

    Jeffrey Zhao:
    @Milo Yip
    其实我很弱的……你说的“最后”是指Unicode部分吗?我先划掉再去看看……



    Unicode的部分可以参考我的文章:

    http://www.cnblogs.com/Ivony/archive/2009/10/14/1583221.html
    http://www.cnblogs.com/Ivony/archive/2009/10/15/1583797.html

    描述成两个UTF-16或者一个UTF-32就不会有歧义。但事实上Unicode就是UTF-16的别称(但貌似就微软喜欢混同这两者),所以,很麻烦啊。。。

  21. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-02-11 10:49:00

    “Unicode编码”到底指什么?
    “Unicode编码”又是一个容易搞混的概念。大体上,Unicode编码可以指:
    1、字符在Unicode字符集内的序列编码,这是一个32位的二进制数(不过包括规划的字符,24位都足够了)。以前这是一个16位的二进制数(古老的事情)。
    2、UTF-16编码方案,这是一种使用16位到32位二进制数值表示Unicode字符的编码方案(因为Unicode字符现在连24位二进制的表都摆不满)。

  22. 韦恩卑鄙 v-zhewg @waynebab…
    *.*.*.*
    链接

    韦恩卑鄙 v-zhewg @waynebaby 2010-02-11 10:52:00

    @Ivony...
    不提unicode就不麻烦了 "两个16位长的Char"

  23. 老赵
    admin
    链接

    老赵 2010-02-11 10:55:00

    Milo Yip:
    「要不,接下来就由您试试看如何?」 我說笑的 :)


    话说我真的是因为懒了,嘿嘿……

  24. 老赵
    admin
    链接

    老赵 2010-02-11 10:55:00

    @韦恩卑鄙 v-zhewg @waynebaby
    这……其实说“两个16位长的任何东西”也行……

  25. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-02-11 10:58:00

    Jeffrey Zhao:
    @韦恩卑鄙 v-zhewg @waynebaby
    这……其实说“两个16位长的任何东西”也行……




    其实我的阴暗目的是借老赵的帖子来做广告的。。。。

  26. 韦恩卑鄙 v-zhewg @waynebab…
    *.*.*.*
    链接

    韦恩卑鄙 v-zhewg @waynebaby 2010-02-11 11:03:00

    @Ivony...
    我是来拆台的,,,

  27. 幸存者
    *.*.*.*
    链接

    幸存者 2010-02-11 12:43:00

    iceboundrock:老赵讲了字节序的物理类型,其实字节序有另外一种逻辑分类:网络字节序(big endian)和本机字节序(由处理器架构决定)。一般平台都提供了相应的API来做这两者之间的转换,例如windows的ntohl,htonl等。.net把这俩操作作为IPAddress类的静态方法提供了:HostToNetworkOrder和NetworkToHostOrder。所以与Tokyo Tyrant这类分布式系统通讯,必然是用网络字节序的。


    老实说,我有点糊涂了。
    网络字节序不就是网络传输中的字节序吗?它和本机字节序有什么区别?实质应该是同一个概念吧,只不过在两个异构环境中的字节序有可能不同而已。

    一般来说,字节流是不需要转换字节序的,只有涉及IP地址、端口号或者一些整型参数时才需要转换字节序。

  28. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-02-11 13:17:00

    幸存者:

    iceboundrock:老赵讲了字节序的物理类型,其实字节序有另外一种逻辑分类:网络字节序(big endian)和本机字节序(由处理器架构决定)。一般平台都提供了相应的API来做这两者之间的转换,例如windows的ntohl,htonl等。.net把这俩操作作为IPAddress类的静态方法提供了:HostToNetworkOrder和NetworkToHostOrder。所以与Tokyo Tyrant这类分布式系统通讯,必然是用网络字节序的。


    老实说,我有点糊涂了。
    网络字节序不就是网络传输中的字节序吗?它和本机字节序有什么区别?实质应该是同一个概念吧,只不过在两个异构环境中的字节序有可能不同而已。

    一般来说,字节流是不需要转换字节序的,只有涉及IP地址、端口号或者一些整型参数时才需要转换字节序。




    应该是事实上没区别,我也不知道如此区分的道理何在,事实上网络和CPU总线有区别么?没看出有什么本质的区别。

    当我们说IP报文的时候,都是用位而不是字节来描述的,这也恰恰说明了在网络传输中,最小的单位是报文或位而不是字节,字节属于高层次的概念。

    同困惑。。。。

  29. 韦恩卑鄙 v-zhewg @waynebab…
    *.*.*.*
    链接

    韦恩卑鄙 v-zhewg @waynebaby 2010-02-11 13:24:00

    我来吐槽:这篇文章没引起什么争论 无聊 BS LZ.

    而且看起来不是某大坑的基石 无聊 BS LZ.

  30. 老赵
    admin
    链接

    老赵 2010-02-11 13:28:00

    @韦恩卑鄙 v-zhewg @waynebaby
    推特好玩吧?
    话说这TT的协议,返回的Response Code有些古怪,我想写一个好一点的客户端搞的心里不踏实,估计要看代码去了……

  31. 韦恩卑鄙 v-zhewg @waynebab…
    *.*.*.*
    链接

    韦恩卑鄙 v-zhewg @waynebaby 2010-02-11 13:31:00

    @Jeffrey Zhao
    推特已经影响我工作进度了 强烈bs 坏人

  32. 老赵
    admin
    链接

    老赵 2010-02-11 13:34:00

    @韦恩卑鄙 v-zhewg @waynebaby
    话说我想不出有啥值得写的东西了,或者就是一些想法,懒得写出来。

  33. 韦恩卑鄙 v-zhewg @waynebab…
    *.*.*.*
    链接

    韦恩卑鄙 v-zhewg @waynebaby 2010-02-11 13:39:00

    懒得全写出来 写到闪存吧 灭哈哈

  34. 诺贝尔
    *.*.*.*
    链接

    诺贝尔 2010-02-11 14:46:00

    @Ivony...

    是约定。没有必然性。

    既然有两种方式,网络必然要取其中之一做标准。

  35. 老赵
    admin
    链接

    老赵 2010-02-11 14:57:00

    @诺贝尔
    这里的“网络”究竟是指什么啊?
    对于程序来说,传输自己的时候完全就是没有意义的二进制数据,不是吗?
    至于它的含义就是由接收方来读取了。
    所以这个规范总是“协议”来定的吧。

  36. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-02-11 15:52:00

    Jeffrey Zhao:
    @诺贝尔
    这里的“网络”究竟是指什么啊?
    对于程序来说,传输自己的时候完全就是没有意义的二进制数据,不是吗?
    至于它的含义就是由接收方来读取了。
    所以这个规范总是“协议”来定的吧。




    我查了一些资料,似乎这个所谓的网络字节序,连个协议都不是,而是一种共识。从网上零碎的资料来看,我推测大概是TCP/IP协议族的应用协议的共识或者公共规范。

    如这里所说的:
    http://www.cnblogs.com/jacktu/archive/2008/11/24/1339789.html

    网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。


    事实上,一直到TCP协议,其传输的数据都是字节流,也就不存在字节序,只有在TCP之上的应用层协议,才可能有字节序。


    维基百科中说到:

    网络传输一般采用大端序,也被称之为网络字节序,或网络序。IP协议中定义大端序为网络字节序。

    这更令人困惑,IP协议是网络层协议,其面向的数据是报文,连字节的概念都没有,哪来的字节序?

  37. 老赵
    admin
    链接

    老赵 2010-02-11 16:42:00

    @Ivony...
    我说的“协议”是指应用协议,你这个是传输协议(如TCP)上的约定吧。
    对于应用协议来说,传输协议上的东西是完全透明的,应用协议可以自己随意制定。

    举个例子:

    我有个应用协议要传输4个32位整数1,2,3,4,于是我用Little Endian传输16个字节:01000000 02000000 03000000 04000000。
    但是在传输协议上,它要传输这16个字节,可能会打成2个包,每个包8个字节,使用Big Endian,这样传输时其实是:0000000200000001 0000000400000003。

    不知道这样理解的对不对,基本靠猜……

  38. 幸存者
    *.*.*.*
    链接

    幸存者 2010-02-11 17:04:00

    Ivony...:
    维基百科中说到:

    网络传输一般采用大端序,也被称之为网络字节序,或网络序。IP协议中定义大端序为网络字节序。

    这更令人困惑,IP协议是网络层协议,其面向的数据是报文,连字节的概念都没有,哪来的字节序?


    可以看一下英文版wikipedia的说法:

    In fact, the Internet Protocol defines a standard big-endian network byte order. This byte order is used for all numeric values in the packet headers and by many higher level protocols and file formats that are designed for use over IP.


    IP协议里的字节序实际上是用在分组头里的数值上的,例如每个分组头会包含源IP地址和目标IP地址,在寻址和路由的时候是需要用到这些值的。

  39. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2010-02-11 17:15:00

    很基础的东西,不注意还真不知道。
    PS. 原文有个笔误
    也就是说,同样的.NET代码在/自己/序不同的平台上得到的结果可能不同。

  40. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-02-11 17:37:00

    @幸存者
    @Jeffrey Zhao


    莫非在数据链路层就已经是以字节为最小单位?

    我觉得不现实,链路层协议就是这些什么GPRS都是链路层协议啊。

    而且我看IP协议的时候,一直都是说多少位到多少位是什么,没看到有字节的概念过。IP协议也没听说必须跑在某种支持字节的链路层协议上。

    就算说,IP协议需要保存IP地址或是包长度这样的数值,但由于IP包头本来就是一个值,而不是多个值拆散到字节(从IP包头有半字节的数据就能看出来)。与其说IP协议是采用大尾序,还不如说就是直接将一个二进制数按顺序摆呢。


    我们假设IP报头是一个n位的二进制数据。
    那么我们说这个二进制数据的第96位到128位是代表源IP地址。和我们说某个字节的高四位代表什么什么,有什么区别呢?

    IP协议报头的大小是32的正整数倍,所以IP协议报头本来就是以32位二进制而不是8位二进制(字节)作为单位的。

    这样说来,当初用大尾序描述的这个人,真是该死,把简单的问题复杂化。


    我好好看看IP协议先。

  41. iceboundrock
    *.*.*.*
    链接

    iceboundrock 2010-02-12 00:38:00

    @Ivony...
    首先我们要理解什么数据需要做字节序转换。
    如果传输的数据最小逻辑单元为byte,例如:字符串(无论ASCII Char串或者已经编码过的unicode串)字节流或者是二进制文件流,字节序转换当然是没必要的,因为解析时不会根据前后两个bytes来解析出一个值,而构造字节流时也不会从一个逻辑值单元(比如一个ASCII 字符,或者二进制文件中的一个字节)产生2个或以上的Bytes。
    但是对于short/long类型的数据,由于他们的长度超过1个字节,也就是前后2或者4bytes的顺序是影响从字节流恢复到正确数值的,所以必须在传输时有统一的字节序,否则在跨异构系统传输时就可能会发生问题。其实IP报文中的很多数据都是需要做字节序转换的,比如包长度、check sum等,这些值大都是short(16bit)或者long(32bit)型,所以解析IP报文时也需要做网络->本机字节序转换,而生成报文字节流时则需要进行本机->网络字节序转换

  42. colin
    218.5.2.*
    链接

    colin 2010-04-19 20:00:40

    @Ivony

    这样说来,当初用大尾序描述的这个人,真是该死,把简单的问题复杂化

    估计你是遗忘了有些历史,当初约定TCP/IP协议族大端字节序的时候,host的字节序都是大端...

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我