我們一起聊聊如何理解字節(jié)序
計(jì)算機(jī)只能理解 0 和 1 組成的二進(jìn)制數(shù)據(jù), 一個(gè) bit 的值是 0 或 1,八個(gè)這樣的 bit 組成了一個(gè)字節(jié),通過字節(jié),計(jì)算機(jī)可以表示一些復(fù)雜的數(shù)據(jù),比如:音頻、視頻等,有些數(shù)據(jù)用一個(gè)字節(jié)就能表示,比如英文字符,而有些數(shù)據(jù)需要多個(gè)字節(jié)來表示,比如:漢字, 對于多字節(jié)的數(shù)據(jù),存儲(chǔ)的時(shí)候會(huì)有字節(jié)順序的問題,也就是字節(jié)序
字節(jié)序是什么
字節(jié)序是計(jì)算機(jī)存儲(chǔ)多字節(jié)數(shù)據(jù)的方式,目前的方式有:大端字節(jié)序和小端字節(jié)序,字節(jié)序主要是針對多字節(jié)的數(shù)據(jù)類型,比如 short、int 等
- 大端字節(jié)序
高位字節(jié)存儲(chǔ)在內(nèi)存的低地址上,低位字節(jié)存儲(chǔ)在內(nèi)存的高位地址上
- 小端字節(jié)序
高位字節(jié)存儲(chǔ)在內(nèi)存的高地址上,低位字節(jié)存儲(chǔ)在內(nèi)存的低地址上
如何理解字節(jié)序
我們平常書寫和閱讀數(shù)字的習(xí)慣是從左到右的,所以把最左邊的字節(jié)當(dāng)作最高位字節(jié),最右邊的字節(jié)當(dāng)作做最低位字節(jié),從左到右,表示從高位字節(jié)到低位字節(jié)
例如:對于 0x01020304,它的大端和小端字節(jié)序在內(nèi)存中的布局如下圖所示
0x 01 02 03 04 總共四個(gè)字節(jié)大小,以人們習(xí)慣的閱讀順序,0x01 處于左邊,屬于高位字節(jié),0x04 處于右邊,屬于低位字節(jié)
內(nèi)存地址從 0x 00 00 00 07 到 0x 00 00 00 0A 4個(gè)字節(jié)的空間,剛好能存儲(chǔ)得下
根據(jù)大端字節(jié)序的的規(guī)則:高位字節(jié)存儲(chǔ)在內(nèi)存低地址,所以處于高位字節(jié)的 0x01 存儲(chǔ)在 0x 00 00 00 07 地址處,緊接著 次高位字節(jié) 0x02 存儲(chǔ)在次低地址 0x 00 00 00 08 處,剩下的兩個(gè)字節(jié) 0x03 和 0x04 分別存儲(chǔ)于 0x 00 00 00 09 和 0x 00 00 00 0A 地址處,最后的結(jié)果是 0x 01 02 03 04
小端字節(jié)序和大端剛好相反,它指的是 高位字節(jié)存儲(chǔ)在內(nèi)存高地址處,所以處于高位字節(jié)的 0x01 存儲(chǔ)在 0x 00 00 00 0A 地址處,次高位字節(jié) 0x02 存儲(chǔ)在次高地址 0x 00 00 00 09 處,余下的 0x03 和 0x04 分別存儲(chǔ)于 0x 00 00 00 08 和 0x 00 00 00 07 地址處,最后的結(jié)果是 0x 04 03 02 01
從上圖可以看出,對于相同的數(shù)據(jù),大端和小端的內(nèi)存布局是不一樣的,大端字節(jié)序的存儲(chǔ)形式更符合人們平常書寫和閱讀的習(xí)慣
為什么會(huì)有字節(jié)序
可能有人會(huì)感到疑惑:既然大端字節(jié)序更符合人們閱讀的習(xí)慣,為什么不全部都采用大端的方式,這樣也就不會(huì)有字節(jié)序的問題了 ?
確實(shí),如果所有平臺(tái)都用同一種存儲(chǔ)順序,就沒有字節(jié)序這一說法了
在早期, CPU 只有幾千個(gè)邏輯門,小端的方式能更有效的使用邏輯電路,所以很多計(jì)算機(jī)內(nèi)部計(jì)算都采用小端的方式,這種方式也就保留到了現(xiàn)在
另外,字節(jié)序是跟 CPU 架構(gòu)相關(guān),不同的廠家設(shè)計(jì)的規(guī)范可能都不一樣,比如 Intel 的 x86 是小端方式,而 IBM 的 PowerPC 則采用大端方
大端的方式更符合人們的閱讀習(xí)慣,因此大部分網(wǎng)絡(luò)傳輸以及文件存儲(chǔ)都是大端的方式
總的來說,小端主要是在計(jì)算機(jī)內(nèi)部使用,大端則在外部使用
計(jì)算機(jī)如何處理字節(jié)序
計(jì)算機(jī)讀取數(shù)據(jù)的時(shí)候是不區(qū)分字節(jié)序的,它總是從內(nèi)存低地址到高地址的順序,按字節(jié)讀取
下面的示例圖展示了數(shù)據(jù) 0x0102 的 大端和小端的內(nèi)存布局以及CPU讀取內(nèi)存的順序
由上圖可知,對于大端字節(jié)序來說,內(nèi)存低地址處存儲(chǔ)的是高位字節(jié),也即計(jì)算機(jī)讀取內(nèi)存的第一個(gè)字節(jié)就是高位字節(jié),小端字節(jié)序就正好相反,內(nèi)存低地址處存儲(chǔ)的是低位字節(jié),讀取內(nèi)存的第一個(gè)字節(jié)是低位字節(jié)
計(jì)算機(jī)只有在讀取數(shù)據(jù)的時(shí)候才需要區(qū)分字節(jié)序
就拿上面展示大端方式的圖 ( 第一張 ) 來說,內(nèi)存 0x 00 00 00 07 地址處存儲(chǔ)的數(shù)據(jù)是 0x01 , 0x 00 00 00 08 地址處存儲(chǔ)的數(shù)據(jù)是 0x02
如果是以大端的方式讀取的話,地址 0x 00 00 00 07 處的數(shù)據(jù) 0x01 會(huì)放到高位字節(jié), 0x 00 00 00 08 處的數(shù)據(jù)是 0x02 放到低位字節(jié),最終這兩個(gè)字節(jié)的數(shù)據(jù)是 0x 01 02
如果是以小端的方式讀取的話,,地址 0x 00 00 00 07 處的數(shù)據(jù) 0x01 會(huì)放到低位字節(jié), 0x 00 00 00 08 處的數(shù)據(jù)是 0x02 放到高位字節(jié),最終這兩個(gè)字節(jié)的數(shù)據(jù)是 0x 02 01
網(wǎng)絡(luò)字節(jié)序
所有的協(xié)議都是人類編制定的,大端對人們閱讀更友好,所以 IEEE 標(biāo)準(zhǔn)協(xié)會(huì)規(guī)定除非有明確說明,否則網(wǎng)絡(luò)協(xié)議都使用大端字節(jié)序, 像 TCP/IP 就是如此
還記得我們在編寫網(wǎng)絡(luò)程序的時(shí)候,傳入 connect 函數(shù)實(shí)參中的 端口號(hào)嗎, 傳入之前需調(diào)用 htons 函數(shù)將其轉(zhuǎn)成網(wǎng)絡(luò)字節(jié)序,也就是要轉(zhuǎn)成大端字節(jié)序,下面是部分代碼示例
- struct sockaddr_in addr;
- addr.sin_family = AF_INET;
- addr.sin_addr.s_addr = inet_addr("192.168.1.10");
- addr.sin_port = htons( 5000 );
- connect( clientfd, (struct sockaddr *)&addr, sizeof(addr)) )
上面紅色的 htons 函數(shù)的作用是將 端口號(hào) 由主機(jī)字節(jié)序轉(zhuǎn)成網(wǎng)絡(luò)字節(jié)序,網(wǎng)絡(luò)字節(jié)序大多時(shí)候都是固定為大端序的,但不同的機(jī)器,主機(jī)序卻不一樣,如果本身就已經(jīng)是大端了,調(diào)用 htons 函數(shù),返回值和實(shí)參是一樣的,如果本身是小端,結(jié)果會(huì)轉(zhuǎn)成大端的形式,具體的數(shù)值也會(huì)不一樣
怎么判斷大小端
上面提到了主機(jī)字節(jié)序,那如何知道當(dāng)前機(jī)器是大端還是小端呢 ?
因?yàn)椴僮飨到y(tǒng)必須適配所有類型的 CPU ,所以對于操作系統(tǒng)來說,大端和小端它都是支持的
為了讓程序易于判斷當(dāng)前平臺(tái)是大端還是小端,Linux 下 glibc 庫提供了下面幾個(gè)宏定義
- BIG_ENDIAN # 大端序
- LITTLE_ENDIAN # 小端序
- BYTE_ORDER # 字節(jié)序
下面是測試代碼 test.c 文件
- #include <stdio.h>
- int main(int argc, char *argv[])
- {
- if(BYTE_ORDER == BIG_ENDIAN)
- {
- printf("big endian...\n");
- }
- else
- {
- printf("little endian...\n");
- }
- }
執(zhí)行 gcc -g -o test test.c 命令進(jìn)行編譯,運(yùn)行測試程序,結(jié)果如下:
- [root@localhost test]# ./test
- little endian...
由此,可以知道當(dāng)前平臺(tái)是小端字節(jié)序
除了用上面的方法之外,我們可以根據(jù)大端和小端的特點(diǎn),自己寫代碼獲取,修改 test.c 文件,內(nèi)容如下
- #include <stdio.h>
- int main(int argc, char *argv[])
- {
- union
- {
- unsigned short i;
- char ch[2];
- }un;
- un.i = 0x0102;
- if(0x01 == un.ch[0])
- {
- printf("big endian...\n");
- }
- else
- {
- printf("little endian...\n");
- }
- }
編譯并運(yùn)行,結(jié)果如下:
- [root@localhost test]# ./test
- little endian...
可以看出,不管是通過系統(tǒng)庫提供的宏來判斷還是自行封裝接口來判斷機(jī)器的字節(jié)序都是可行的
最后,如果想知道 LITTLE_ENDIAN、 BIG_ENDIAN 、BYTE_ORDER 宏定義的詳細(xì)情況,可以查看 glibc 源碼,它們在 glibc-2.17\string\endian.h 以及 glibc-2.17\sysdeps\x86\bits\endian.h 文件中
注意:不同版本的 glibc 源碼,具體的位置可能有差異,我使用的是 glibc-2.17 版本
大端小端的轉(zhuǎn)換
熟悉了大端和小端特點(diǎn),它們之間的轉(zhuǎn)換就簡單了,對于兩字節(jié)來說,每個(gè)字節(jié)值不變,互換字節(jié)位置,如果是更多字節(jié)的話,最低位字節(jié)和最高位字節(jié)交換,次低位字節(jié)與次高位字節(jié)交換,直到所有字節(jié)都完成了一遍交換為止
比如:下面是小端轉(zhuǎn)大端的偽代碼
- #小端轉(zhuǎn)大端 假設(shè):ch 和 i 是小端序
- char ch[2];
- int i = 0;
- # x 是大端字節(jié)序
- x = ch[1] << 8 | ch[0]
- # y 是大端字節(jié)序
- y = ( (i & 0xff000000) >> 24 ) | ( (i & 0x00ff0000) >> 8 ) | ( (i & 0x0000ff00) << 8 ) | ( (i & 0x000000ff) << 24 )
變量 i 字節(jié)序轉(zhuǎn)換說明:按照從左到右的順序,把 i 的第一個(gè)字節(jié)右移 3 個(gè)字節(jié)( 24 bit ),第二個(gè)字節(jié)右移 1 字節(jié) ( 8 bit ),第三個(gè)字節(jié)左移 1 字節(jié) ( 8 bit ),第四個(gè)字節(jié)左移 3 個(gè)字節(jié) ( 24 bit ),最后把移位后的字節(jié)組合起來就可以了
在實(shí)際的程序處理中,不應(yīng)該出現(xiàn)字節(jié)序的問題,只有 "網(wǎng)絡(luò)字節(jié)序" 和 "主機(jī)字節(jié)序" ,需要轉(zhuǎn)換字節(jié)序時(shí),使用 ntohl, ntohs, htonl, htons 等函數(shù)即可
- ntohl # uint32 類型 網(wǎng)絡(luò)序轉(zhuǎn)主機(jī)序
- htonl # uint32 類型 主機(jī)序轉(zhuǎn)網(wǎng)絡(luò)序
- ntohs # uint16 類型 網(wǎng)絡(luò)序轉(zhuǎn)主機(jī)序
- htons # uint16 類型 主機(jī)序轉(zhuǎn)網(wǎng)絡(luò)序
小結(jié)
本文詳述了字節(jié)序的一些知識(shí),開發(fā)網(wǎng)絡(luò)應(yīng)用的時(shí)候會(huì)涉及到字節(jié)序的相關(guān)問題,所以,花點(diǎn)兒時(shí)間弄明白還是很有必要的






































