最近在從頭重寫 MobileIMSDK 的TCP版,自已組織TCP數據幀時就遇到了位元組序大小端問題。所以,借這個機會單獨整理了這篇文章,希望能加深大家對位元組序問題的理解,加強對IM這種基於網路通訊的程式在數據傳輸這一層的知識掌控情況。
程式設計師在寫應用層程式時,一般不需要考慮位元組序問題,因爲位元組序跟操作系統和硬體環境有關,而我們編寫的程式要麼不需要跨平臺(比如只執行在windows),要麼需要跨平臺時會由Java這種跨平臺語言在虛擬機器層遮蔽掉了。
但典型情況,當你編寫網路通訊程式,比如IM聊天應用時,就必須要考慮位元組序問題,因爲你的數據在這樣的場景下要跨機器、跨網路通訊,必須解決不同系統、不同平臺的位元組序問題。
* 閱讀物件:本文屬於計算機基礎知識,特別適合從事網路程式設計方面工作(比如IM這類通訊系統)的程式設計師閱讀。面視時,面視官一般都會聊到這個知識點。
本文已同步發佈於「即時通訊技術圈」公衆號,歡迎關注:
▲ 本文在公衆號上的鏈接是:點此進入 ,原文鏈接是:http://www.52im.net/thread-3101-1-1.html
位元組序,是指數據在記憶體中的存放順序,當位元組數大於1時需要考慮(只有一個位元組的情況下,比如char型別,也就不存在順序問題啦)。
從下圖中,可以直觀的感受到什麼是位元組序問題:
(上圖片改編自《C語言列印數據的二進制格式-原理解析與程式設計實現》)
位元組序常被分爲兩類:
舉個具體的例子,0x1234567 的大端位元組序和小端位元組序寫法如下:
如上圖所示:大端小端位元組序最小單位1位元組,即8bit;大端位元組序就是和我們平時寫法的順序一樣,從低地址到高地址寫入0x01234567;而小端位元組序就是和我們平時的寫法反過來,因爲位元組序最小單位爲1位元組,所以從低地址到高地址寫入0x67452301。
一個比較合理的解釋是說:計算機中電路優先處理低位位元組,效率比較高,因爲計算機都是從低位開始的,所以計算機內部處理都是小端位元組序。
而人類人類讀寫數值的方法,習慣用大端位元組序,所以除了計算機的內部處,其他的場理合都是大端位元組序,比如:網路傳輸和檔案儲存時都是用的大端位元組序(關於網路位元組序,會在後面繼續展開說明)。
大小端位元組序問題,最有可能是跟技術算硬體或軟體的創造者們,在技術創立之初的一些技術條件或個人習慣有關。
所以大小端問題,體現在實際的計算機工業應用來上,不同的操作系統和不同的晶片型別可能都會有不同。
具體來說:DEC和Intel的機器(X86平臺)一般採用小端,IBM、Motorola(Power PC)、Sun的機器一般採用大端。
當然,這不代表所有情況。有的CPU即能工作於小端, 又能工作於大端,比如:Arm、Alpha、摩托羅拉的PowerPC。
而且,具體這類CPU是大端還是小端,和具體設定也有關。如:Power PC支援小端位元組序,但在預設設定時是大端位元組序。
一般來說:大部分使用者的操作系統(如:Windows、FreeBsd、Linux)是小端位元組序。少部分,如:Mac OS 是大端位元組序。
怎麼判斷我的計算機裡使用的是大端還是小端位元組序呢?
下面 下麪的這段程式碼可以用來判斷計算機是大端的還是小端。判斷的思路是:確定一個多位元組的值(下面 下麪使用的是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 0;
}
根據網上的資料,據說名字的由來跟喬納森·斯威夫特的著名諷刺小說《格列佛遊記》有關。
書中的故事是這樣的:一般來說,大家都認爲吃雞蛋前,原始的方法是打破雞蛋較大的一端。可是當今皇帝的祖父小時候吃雞蛋,一次按古法打雞蛋時碰巧將一個手指弄破了,因此他的父親,當時的皇帝,就下了一道敕令,命令全體臣民吃雞蛋時打破雞蛋較小的一端,違令者重罰。
小人國內部分裂成Big-endian和Little-endian兩派,區別在於一派要求從雞蛋的大頭把雞蛋打破,另一派要求從雞蛋的小頭把雞蛋打破。
小人國國王改變了開啓雞蛋的方位與理由,並由此招致了修改法律、引發戰爭和宗教改革等一序列事件的發生。
《格列佛遊記》中的這則故事,原本是藉以諷刺英國的政黨之爭。而在計算機工業中,也借用了這個故事來代指大家在數據儲存位元組順序中的分歧,並把「大端」(Big-endian)、「小端」(Little-endian)的名字,沿用到了計算機中。
(上圖片改編自《「位元組序」是個什麼鬼?》)
或許,借用這個故事來命名大小端位元組序問題,無非就是想告訴大家,所謂的「大端」、「小端」實際上可能無關計算機效能,更多的只是創造者們在創立計算機之初,代入了個人的一些約定俗成的習慣而已。
對於搞網路通訊應用(比如IM、訊息推播、實時音視訊)開發的程式設計師來說,自已寫通訊底層的話是一定會遇到大小端問題的,對於網路位元組序這個知識點是一定要必知必會。(當然,你要是很沒追求的認爲,反正我公司就讓租租第3方,能用就行,具體通底層怎麼寫我纔不想掉頭髮去考慮那麼多。。。。 那哥也救不了你。。)
上面所說的大小端位元組序都是在說計算機自己,也被稱作主機位元組序。同型號計算機上寫的程式,在相同的系統上面執行總歸是沒有問題。
但計算機網路的出現讓大小端問題變的複雜化了,因爲每個計算機都有自己的主機位元組序。不同計算機之間通過網路通訊時:我「說」的你聽不懂,你「說」我也聽不懂,這可怎麼辦?
好訊息是,TCP/IP協定很好的解決了這個問題,TCP/IP協定規定使用「大端」位元組序作爲網路位元組序。
這樣,即使不使用大端的計算機也沒有關係,因爲發送數據的時候可以將自己的主機位元組序轉換爲網路位元組序(即「大端」位元組序),對接收到的數據轉換爲自己的主機位元組序。這樣一來,也就達到了與CPU、操作系統無關,實現了網路通訊的標準化。
具體的原理就是:
也就是說,該數值在記憶體中的起始地址處對應的那個位元組就是要發送的第一個高位位元組(即:高位位元組存放在低地址處)。由此可見,多位元組數值在發送之前,在記憶體中就是以大端法存放的。
所以說,網路位元組序就是大端位元組序。
那麼,爲了程式的相容,程式設計師們每次發送和接受數據都要進行轉換,這樣做的目的是保證程式碼在任何計算機上執行時都能達到預期的效果。
通訊時的這種常用的操作,Socket API這一層,一般都提供了封裝好的轉換函數,方便程式設計師使用。比如從主機位元組序到網路位元組序的轉換函數:htons、htonl(C語言中常用),從網路位元組序到主機位元組序的轉換函數:ntohs、ntohl(C語言中常用)。當然,也可以編寫自己的轉換函數。
在我編寫MobileIMSDK的TCP版時(MobileIMSDK是我開源的IM通訊層庫),同樣遇到了大小端位元組序問題。
以MobileIMSDK的iOS端拼裝網路數據收發的程式碼爲例:
如上圖程式碼所示,注意以下兩個大小端轉換函數的使用:
如果對網路大小端轉換這方面的實踐感興趣,可以自已去下載MobileIMSDK原始碼試一試:https://github.com/JackJiang2011/MobileIMSDK。
[1] 「位元組序」是個什麼鬼?
[2] 大小端及網路位元組序
本文是系列文章中的第9篇,本系 本係列大綱如下:
《腦殘式網路程式設計入門(一):跟着動畫來學TCP三次握手和四次揮手》
《腦殘式網路程式設計入門(二):我們在讀寫Socket時,究竟在讀寫什麼?》
《腦殘式網路程式設計入門(三):HTTP協定必知必會的一些知識》
《腦殘式網路程式設計入門(四):快速理解HTTP/2的伺服器推播(Server Push)》
《腦殘式網路程式設計入門(五):每天都在用的Ping命令,它到底是什麼?》
《腦殘式網路程式設計入門(六):什麼是公網IP和內網IP?NAT轉換又是什麼鬼?》
《腦殘式網路程式設計入門(七):面視必備,史上最通俗計算機網路分層詳解》
(本文同步發佈於:http://www.52im.net/thread-3101-1-1.html)