剝雞蛋的故事
《格列佛遊記》中記載了兩個征戰的強國,你不會想到的是,他們打仗竟然和剝雞蛋的姿勢有關。
很多人認為,剝雞蛋時應該打破雞蛋較大的一端,這群人被稱作“大端(Big endian)派”。可是當今皇帝的祖父小時候吃雞蛋的時候碰巧將一個手指弄破了。所以,他的父親(當時的皇帝)就下令剝雞蛋必須打破雞蛋較小的一端,違令者重罰,由此產生了“小端(Little endian)派”。
老百姓們對這項命令極其反感,由此引發了6次叛亂,其中一個皇帝送了命,另一個丟了王位。據估計,先後幾次有11000人情願受死也不肯去打破雞蛋較小的一端!
話題扯遠了,不過關於位元組序的big endian和little endian的命名確實來源於此典故;
在理解位元組序前,先來說說我們平時不那麼留意的閱讀習慣常識,如果我不說,你還真的反應不過來,反正我是這樣的。
1.數位閱讀習慣
人類在閱讀數字的時候一般的認知是從左到右這樣閱讀的,比如256,讀作“二百五十六”,大的數位在左邊小的依次往右,這是人類的閱讀習慣
2.文章閱讀習慣
人類在閱讀文章的時候一般的認知也是從左到右閱讀的,比如你正在閱讀的這段文字,我打賭你不會從最後一個字往回閱讀,這又是一個人類的閱讀習慣
開始了,我們都清楚,計算機世界裡面,最小的儲存是位元組(byte),就好像閱讀數字一樣,我們會把大的位數放在左邊
比如:
00010010
它等於十進位制的18,很明顯這沒有任何問題,所以說,當一段文字它只是包含一個數字的情況下,是不會出現位元組序問題的,人類都公認越往左邊就應該儲存位數越高的值,例如UTF-8,解析程式每次只會取一個位元組出來進行解析,便不會存在位元組序的問題。
問題出現在當一段文字包含好幾個數字或者更多個數字的時候,打個比方,
UTF-16編碼,它是一種由兩個位元組構成的Unicode字元編碼方式,也就是說,無論儲存任何字元,它都要用到兩個位元組:
UTF-16編碼 4E2D 對應的是中文的“中”字,很明顯,要儲存這個“中”字,必須動用兩個位元組,於是問題來了,我是先儲存4E在左邊呢,還是先儲存2D在左邊呢?
就是這丁點事兒,不同計算機廠商和各個計算機技術協會幾乎打起來了,而且一直沒有解決問題,存在著爭議;
好啦,來看看我們剛才提到的兩個人類閱讀習慣常識,來結合UTF-16的“中文”兩個字,就能說明問題!
很明顯“中文”不是一個文字,而是一段文字,按照人類對文章的閱讀習慣,肯定是先讀“中”後讀“文”,這個是沒有異議的,全世界都沒有異議,
所以,計算機的記憶體地址是從左往右排序的,左邊起是第一個然後是第二個。。。
big endian:
大端序認為,按照人類對數字的閱讀習慣,把大的數字儲存在左邊是最合適的
little endian:
小端序認為,這個單元式的位元組段壓根就跟數字不是一回事,應該按照人類閱讀文章的習慣,把小的數字儲存在左邊
公說公有理婆說婆有理,貌似不同的CPU廠商並沒有達成一致:
- x86,MOS Technology 6502,Z80,VAX,PDP-11等處理器為Little endian。
- Motorola 6800,Motorola 68000,PowerPC 970,System/370,SPARC(除V9外)等處理器為Big endian。
- ARM, PowerPC (除PowerPC 970外), DEC Alpha, SPARC V9, MIPS, PA-RISC and IA64的位元組序是可配置的。
大端也好,小端也罷,就權當是個人愛好吧,只要你不影響別人就行,對不?
網路位元組序
前面的大端和小端都是在說計算機自己,也被稱作主機位元組序。其實,只要自己能夠自圓其說是沒啥問題的。問題是,網路的出現使得計算機可以通訊了。通訊,就意味著相處,相處必須得有共同語言啊,得說普通話,要不然就容易會錯意,下了一個小時的小電影發現打不開,理解錯誤了!
但是每個計算機都有自己的主機位元組序啊,還都不依不饒,堅持做自己,怎麼辦?
TCP/IP協議隆重出場,RFC1700規定使用“大端”位元組序為網路位元組序,其他不使用大端的計算機要注意了,傳送資料的時候必須要將自己的主機位元組序轉換為網路位元組序(即“大端”位元組序),接收到的資料再轉換為自己的主機位元組序。這樣就與CPU、作業系統無關了,實現了網路通訊的標準化。突然覺得,TCP/IP協議好任性啊有木有!
為了程式的相容,你會看到,程式設計師們每次傳送和接受資料都要進行轉換,這樣做的目的是保證程式碼在任何計算機上執行時都能達到預期的效果。
這麼常用的操作,BSD Socket提供了封裝好的轉換介面,方便程式設計師使用。包括從主機位元組序到網路位元組序的轉換函式:htons、htonl;從網路位元組序到主機位元組序的轉換函式:ntohs、ntohl。當然,有了上面的理論基礎,也可以編寫自己的轉換函式。
下面的一段程式碼可以用來判斷計算機是大端的還是小端的,判斷的思路是確定一個多位元組的值(下面使用的是4位元組的整數),將其寫入記憶體(即賦值給一個變數),然後用指標取其首地址所對應的位元組(即低地址的一個位元組),判斷該位元組存放的是高位還是低位,高位說明是Big endian,低位說明是Little endian。
#include <stdio.h>
int main ()
{
unsigned int x = 0x12345678;
char *c = (char*)&x;
if (*c == 0x78) {
printf("Little endian");
} else {
printf("Big endian");
}
return ;
}
身邊的位元組序
字元編碼方式UTF-16、UTF-32同樣面臨位元組序的問題,因為他們分別使用2個位元組和4個位元組編碼Unicode字元,一旦某個值用多個位元組表示,就必須要考慮儲存的順序了。於是,採用了最簡單粗暴的方式,給檔案頭部寫幾個字元,用來表示是大端呢還是小端:
頭部的字元 編碼 位元組序 FF FE UTF-16/UCS-2 Little endian FE FF UTF-16/UCS-2 Big endian FF FE 00 00 UTF-32/UCS-4 Little endian 00 00 FE FF UTF-32/UCS-4 Big-endian
這裡不得不提一下UTF-8啊,明明人家是單個位元組的,不存在什麼位元組序的問題。微軟為了統一UTF-X,硬生生給他的頭部也加了幾個字元!是的,這幾個字元就是BOM(Byte Order Mark),這就是Windows下的UTF-8。
相信很多人都被UTF-8的BOM給坑過,多了這個BOM的UTF-8檔案,會導致很多問題啊。比如,寫的Shell指令碼,內容為#!/usr/bin/env bash,在UTF-8有BOM和UTF-8無BOM的編碼下,對應的16進製為:
所以,有BOM的話,Shell直譯器就報錯啦。原因在於,直譯器希望遇到#!/usr/bin/env bash,而使用UTF-8有BOM進行編碼的內容會多了3個位元組的EF BB BF。
對於UTF-8和UTF-8無BOM兩種編碼格式,我們更多的使用UTF-8無BOM。