字节顺序的问题令人沮丧,我想让你免除我经历的悲痛。这是关键: 问题:计算机使用不同的语言,就像人一样。 有些人将数据“从左到右”,其他人从“从右到左”。 机器可以很好地读取自己的数据 - 当一台计算机存储数据而另一种类型尝试读取数据时会出现问题。 解决方案 同意通用格式(即,所有网络流量遵循单一格式),或 始终包含描述数据格式的标头。如果标题向后显示,则表示数据以其他格式存储,需要转换。 数字与数据 最重要的概念是识别数字与表示数字的数据之间的差异。 一个数是一个抽象的概念,如东西计数。你有十个手指。无论您使用什么样的表示,“十”的概念都不会改变:十,十,迪兹(西班牙语),ju(日语),1010(二进制),X(罗马数字)......这些表示都指向同样的“十”概念。 将其与数据进行对比。数据是物理概念,是存储在计算机上的原始比特和字节序列。数据没有固有的含义,必须由阅读它的人解释。 数据就像人类写作,这只是纸上的标记。这些标记没有固有的含义。如果我们看到一条线和一条圆(如:| O),我们可能会将其解释为“十”。 但是我们假设标记是指一个数字。它们可能是木星的“IO”字母。或者也许是希腊女神。或者也许是输入/输出的缩写。或某人的名字缩写。或二进制数字2(“10”)。可能性列表继续。 关键是单个数据(| O)可以用多种方式解释,并且在有人澄清作者的意图之前,其含义尚不清楚。 计算机面临同样的问题。它们存储数据,而不是抽象概念,并使用1和0的序列来存储数据。之后,他们回读了1和0,并尝试从原始数据中重新创建抽象概念。根据所做的假设,1和0可能意味着非常不同的东西。 为什么会出现这个问题?嗯,没有规则所有计算机必须使用相同的语言,就像所有人都不需要的规则一样。每种类型的计算机都是内部一致的(它可以读回自己的数据),但不能保证其他类型的计算机如何解释它创建的数据。 基本概念 数据(位和字节,或纸上的标记)毫无意义; 它必须被解释为创建一个抽象概念,如数字。 与人类一样,计算机有不同的方式来存储相同的抽象概念。(也就是说,我们有很多方法可以说“十”:十,十,二等) 将数字存储为数据 值得庆幸的是,大多数计算机都同意一些基本的数据格式(情况并非总是这样)。这为我们提供了一个共同的起点,使我们的生活更轻松: 有两个值(开或关,1或0) 一个字节是一个8位的序列 字节中“最左边”的位是最大的。因此,二进制序列00001001是十进制数9. 00001001 =(2 3 + 2 0 = 8 + 1 = 9)。 位从右到左编号。位0是最右边和最小的; 第7位是最左边和最大的。 我们可以将这些基本协议用作交换数据的构建块。如果我们一次存储和读取一个字节的数据,它将适用于任何计算机。字节的概念在所有机器上都是相同的,并且在所有机器上,哪个字节是第一个,第二个,第三个(字节0,字节1,字节2 ......)的想法是相同的。 如果计算机同意每个字节的顺序,那么问题是什么? 嗯,这适用于单字节数据,如ASCII文本。但是,需要使用多个字节存储大量数据,如整数或浮点数。并且没有就如何存储这些序列达成一致。 字节示例 考虑一个4字节的序列,名为WXY和Z - 我避免将它们命名为ABCD,因为它们是十六进制数字,这会令人困惑。因此,每个字节都有一个值,由8位组成。 字节名称:WXYZ 地点:0 1 2 3 值(十六进制):0x12 0x34 0x56 0x78 例如,W是整个字节,十六进制为0x12或二进制为00010010。如果W被解释为一个数字,它将是十进制的“18”(顺便说一下,没有什么可以说我们必须把它解释为一个数字 - 它可能是一个ASCII字符或完全不同的东西)。 和我一起到目前为止?我们有4个字节,WXY和Z,每个字节都有不同的值。 理解指针 指针是编程的关键部分,尤其是C编程语言。指针是引用内存位置的数字。由我们(程序员)来解释该位置的数据。 在C中,当您将指针强制转换为某种类型(例如char *或int *)时,它会告诉计算机如何解释该位置的数据。例如,让我们声明 void * p = 0; // p是指向未知数据类型的指针 // p是一个NULL指针 - 不要取消引用 char * c; // c是指向char的指针,通常是单个字节 请注意,我们无法从p获取数据,因为我们不知道它的类型。p可以指向一个数字,一个字母,一个字符串的开头,你的星座,一个图像 - 我们只是不知道要读取多少字节,或者如何解释那里有什么。 现在,假设我们写 c =(char *)p; 啊 - 现在这个语句告诉计算机指向与p相同的位置,并将数据解释为单个字符(char通常是单个字节,uint8_t如果在您的机器上使用,则使用)。在这种情况下,c将指向存储器位置0或字节W.如果我们打印c,我们将得到W中的值,即十六进制0x12(记住W是整个字节)。 这个例子不依赖于我们拥有的计算机类型 - 同样,所有计算机都同意单个字节是什么(过去不是这种情况)。 这个例子很有用,即使它在所有计算机上都是一样的 - 如果我们有一个指向单个字节的指针(char *,一个字节),我们可以遍历内存,一次读取一个字节。我们可以检查任何内存位置和计算机的字节顺序无关紧要 - 每台计算机都会返回相同的信息。 所以有什么问题? 计算机尝试读取多个字节时出现问题。某些数据类型包含多个字节,如长整数或浮点数。单个字节只有256个值,因此可以存储0 - 255。 现在出现问题 - 当您读取多字节数据时,最大字节出现在哪里? 大端机:用于存储数据的第一大终端。查看多个字节时,第一个字节(最低地址)是最大的。 小端机:首先存储数据小端。查看多个字节时,第一个字节最小。 命名是有道理的,是吗?Big-endian认为大端是第一位的。(顺便说一句,big-endian / little-endian命名来自Gulliver's Travels,Lilliputans争论是否要在小端或大端打破鸡蛋。有时计算机辩论同样有意义:-)) 同样,如果您有一个字节,则endian-ness无关紧要。如果你有一个字节,它是你读取的唯一数据,所以只有一种方法可以解释它(同样,因为计算机同意一个字节是什么)。 现在假设我们将4个字节(WXYZ)以相同的方式存储在大端和小端机器上。也就是说,两台机器上的内存位置0都是W,内存位置1是X等。 我们可以通过记住字节与机器无关来创建这种安排。我们可以一次走一段内存,然后设置我们需要的值。这适用于任何机器: c = 0; //指向位置0(不适用于真机!) * c = 0x12; //设置W的值 c = 1; //指向位置1 * c = 0x34; //设置X的值 ... //重复Y和Z; 细节留给读者 此代码适用于任何机器,我们在位置0,1,2和3中都设置了字节W,X,Y和Z. 解释数据 现在让我们用多字节数据做一个例子(最后!)。快速回顾:“短整数”是一个2字节(16位)的数字,范围从0到65535(如果是无符号)。我们在一个例子中使用它: 短* s; //指向short int的指针(2个字节) s = 0; //指向位置0; * s是值 因此,s是指向short的指针,现在正在查看字节位置0(具有W)。当我们读取s的值时会发生什么? Big endian机器:我认为short是两个字节,所以我将把它们读掉:位置s是地址0(W或0x12),位置s + 1是地址1(X或0x34)。由于第一个字节最大(我是大端!),数字必须是256 *字节0 +字节1,或256 * W + X或0x1234。我将第一个字节乘以256(2 ^ 8)因为我需要将它移位8位。 小端机:我不知道Big Endian先生在吸烟。是的,我同意短路是2个字节,我会像他一样读取它们:位置s是0x12,位置s + 1是0x34。但在我的世界里,第一个字节是最小的!short的值是字节0 + 256 *字节1,或256 * X + W或0x3412。 请记住,两台机器都从位置开始,读取内存向上。关于位置0和位置1的含义没有混淆。短路是2个字节没有混淆。 但是你看到了这个问题吗?big-endian机器认为s = 0x1234并且little-endian机器认为s = 0x3412。相同的确切数据给出了两个不同的数字。可能不是一件好事。 又一个例子 让我们用另一个4字节整数的例子来表示“有趣”: int * i; //指向int的指针(32位机器上的4个字节) i = 0; //指向位置零,所以* i是那里的值 我们再次问:我的价值是什么? 大端机器:int是4个字节,第一个是最大的。我读了4个字节(WXYZ),W是最大的。号码是0x12345678。 小端机器:当然,int是4个字节,但第一个是最小的。我也读过WXYZ,但W属于后面 - 这是最小的。号码是0x78563412。 相同的数据,不同的结果 - 不是一件好事。这是一个使用上面数字的交互式示例,随意插入您自己的: NUXI问题 字节顺序的问题有时被称为NUXI问题:存储在big-endian机器上的UNIX可以在小端机器上显示为NUXI。 假设我们要将4个字节(U,N,I和X)存储为两个短路:UN和IX。每个字母都是整个字节,就像我们上面的WXYZ例子一样。要存储这两条短裤我们会写: 短* s; //设置短裤的指针 s = 0; //指向位置0 * s =联合国; //存储第一个短:U * 256 + N(虚构代码) s = 2; //指向下一个位置 * s = IX; //存储第二个短片:I * 256 + X. 此代码不是特定于计算机的。如果我们将“UN”存储在一台机器上并要求将其读回来,最好是“UN”!我不关心端序问题,如果我们在一台机器上存储一个值并在同一台机器上读回它,它必须是相同的值。 但是,如果我们一次查看一个字节的内存(使用我们的char *技巧),顺序可能会有所不同。在大端机器上,我们看到: 字节:UNIX 地点:0 1 2 3 哪个有意义。U是“UN”中的最大字节,首先存储。对于IX来说也是如此:我是最大的,并且首先存储。 在小端机器上我们会看到: 字节:NUXI 地点:0 1 2 3 这也是有道理的。“N”是“UN”中最小的字节,首先存储。同样,即使字节在存储器中“向后”存储,小端机器也知道它是小端,并在读取值时正确解释它们。另外,请注意我们可以在任何机器上指定十六进制数字,例如x = 0x1234。即使是一个小端机器也知道你写0x1234时的意思,并且不会强迫你自己交换这些值(你指定要写入的十六进制数,它会计算出细节并交换内存中的字节,在覆盖。棘手。)。 这种情况称为“NUXI”问题,因为字节序列UNIX在另一种类型的机器上被解释为NUXI。同样,如果您交换数据,这只是一个问题 - 每台机器都是内部一致的。 在Endian机器之间交换数据 计算机是连接在一起的 - 机器只需要担心阅读自己的数据的日子就消失了。大端和小端机器需要交谈和相处。他们如何做到这一点? 解决方案1:使用通用格式 最简单的方法是同意通过网络发送数据的通用格式。标准的网络秩序实际上是大端的,但是有些人认为小端没有获胜...我们只是称之为“网络秩序”。 要将数据转换为网络顺序,计算机将调用函数hton(主机到网络)。在大端机器上,这实际上不会做任何事情,但我们不会在这里讨论(小端可能会生气)。 但是在发送数据之前使用hton很重要,即使你是big-endian也是如此。你的程序可能很受欢迎,它在不同的机器上编译,你希望你的代码是可移植的(不是吗?)。 类似地,有一个用于从网络读取数据的功能ntoh(网络到主机)。您需要这样做以确保正确地将网络数据解释为主机的格式。您需要知道正在接收的数据类型才能正确解码,转换功能如下: htons() - “主机到网络短” htonl() - “主机到网络长” ntohs() - “网络主机短” ntohl() - “网络主机长” 请记住,单个字节是单个字节,顺序无关紧要。 执行低级网络时,这些功能至关重要,例如验证IP数据包中的校验和。如果你不能正确理解端序问题,你的生活将会很痛苦 - 请接受我的话。使用翻译功能,并了解他们需要的原因。 解决方案2:使用字节订单标记(BOM) 另一种方法是在每个数据之前包括一个幻数,例如0xFEFF。如果您读取幻数并且它是0xFEFF,则表示数据与您的机器格式相同,一切都很好。 如果您读取幻数并且它是0xFFFE(它是向后),则表示数据是以不同于您自己的格式写入的。你必须翻译它。 有几点需要注意。首先,这个数字并不是真正的魔术,但程序员经常使用这个术语来描述任意数字的选择(BOM可能是任何不同字节的序列)。它被称为字节顺序标记,因为它表示存储数据的字节顺序。 其次,BOM会增加所有传输数据的开销。即使您只发送2个字节的数据,也需要包含2个字节的BOM。哎哟! Unicode 在存储多字节数据时使用BOM(某些Unicode字符编码每个字符可以有2个,3个甚至4个字节)。XML通过默认情况下以UTF -8 存储数据来避免这种混乱,它一次存储一个字节的Unicode信息。为什么这很酷? (第56次重复)“因为字节序问题对单个字节无关紧要”。 你是对的。 同样,BOM也会出现其他问题。如果您忘记包含BOM,该怎么办?您是否认为数据的格式与您自己的格式相同?您是否阅读了数据并查看它是否“向后”(无论这意味着什么)并尝试翻译它?如果常规数据巧合地包含BOM会怎么样?这些情况并不好玩。 为什么会出现Endian问题?我们不能相处吗? 啊,这是一个多么哲学的问题。 每个字节顺序系统都有其优点。Little-endian机器允许您首先读取最低字节,而不读取其他字节。你可以很容易地检查一个数字是奇数还是偶数(最后一位是0),如果你是这样的话,这很酷。Big-endian系统将数据存储在内存中的方式与我们人类对数据的思考方式(从左到右)相同,这使得低级调试更容易。 但为什么不是每个人都同意一个系统呢?为什么某些计算机必须尝试与众不同? 让我回答一个问题:为什么不是每个人都说同一种语言?为什么有些语言从左到右书写,有些语言从右到左书写? 有时通信系统是独立开发的,后来需要进行交互。 结语:分手思考 Endian问题是一般编码问题的一个例子 - 数据需要表示抽象概念,后来需要从数据创建概念。这个主题值得拥有自己的文章(或系列),但你应该更好地理解endian问题。更多信息:
|
|