淺談字節(jié)序(Byte Order)及其相關(guān)操作 - Jeffrey Zhao - 博客園
淺談字節(jié)序(Byte Order)及其相關(guān)操作
2010-02-10 23:05 by Jeffrey Zhao, 12152 閱讀, 41 評(píng)論, 收藏 , 編輯最近在為 Tokyo Tyrant 寫(xiě)一個(gè).NET客戶端類庫(kù)。Tokyo Tyrant公開(kāi)了一個(gè)基于TCP協(xié)議的二進(jìn)制協(xié)議,于是我們的工作其實(shí)也只是按照協(xié)議發(fā)送和讀取一些二進(jìn)制數(shù)據(jù)流而已,并不麻煩。不過(guò)在其中涉及到了“字節(jié)序”的概念,這本是計(jì)算機(jī)體系結(jié)構(gòu)/操作系統(tǒng)等課程的基礎(chǔ),不過(guò)我還是打算在這里進(jìn)行簡(jiǎn)單說(shuō)明,并且對(duì).NET中部分類庫(kù)在此類數(shù)據(jù)流處理時(shí)的注意事項(xiàng)進(jìn)行些許記錄與總結(jié)。
字節(jié)序(Byte Order)
說(shuō)到程序間的通信,說(shuō)到底便是發(fā)送數(shù)據(jù)流。我們一般把字節(jié)(byte)看作是數(shù)據(jù)的最小單位。當(dāng)然,其實(shí)一個(gè)字節(jié)中還包含8個(gè)比特(bit)──有時(shí)候我奇怪為什么很多朋友會(huì)不知道bit或是它和byte的關(guān)系。當(dāng)我們拿到一系列byte的時(shí)候,它本身其實(shí)是沒(méi)有意義的,有意義的只是“識(shí)別字節(jié)的方式”。例如,同樣4個(gè)字節(jié)的數(shù)據(jù),我們可以把它看作是1個(gè)32位整數(shù)、 2個(gè)Unicode、 或者字符4個(gè)ASCII字符。
同樣我們知道,在一個(gè)32位的CPU中“字長(zhǎng)”為32個(gè)bit,也就是4個(gè)byte。在這樣的CPU中,總是以4字節(jié)對(duì)齊的方式來(lái)讀取或?qū)懭雰?nèi)存,那么同樣這4個(gè)字節(jié)的數(shù)據(jù)是以什么順序保存在內(nèi)存中的呢?例如,現(xiàn)在我們要向內(nèi)存地址為a的地方寫(xiě)入數(shù)據(jù)0x0A0B0C0D,那么這4個(gè)字節(jié)分別落在哪個(gè)地址的內(nèi)存上呢?這就涉及到字節(jié)序的問(wèn)題了。
每個(gè)數(shù)據(jù)都有所謂的“有效位(significant byte)”,它的意思是“表示這個(gè)數(shù)據(jù)所用的字節(jié)”。例如一個(gè)32位整數(shù),它的有效位就是4個(gè)字節(jié)。而對(duì)于0x0A0B0C0D來(lái)說(shuō),它的有效位從高到低便是0A、0B、0C及0D——這里您可以把它作為一個(gè)256進(jìn)制的數(shù)來(lái)看(相對(duì)于我們平時(shí)所用的10進(jìn)制數(shù))。
而所謂 大字節(jié)序(big endian) ,便是指其“ 最高有效位(most significant byte) ”落在低地址上的存儲(chǔ)方式。例如像地址a寫(xiě)入0x0A0B0C0D之后,在內(nèi)存中的數(shù)據(jù)便是:
![]()
而對(duì)于 小字節(jié)序(little endian) 來(lái)說(shuō)就正好相反了,它把“ 最低有效位(least significant byte) ”放在低地址上。例如:
![]()
對(duì)于我們常用的CPU架構(gòu),如Intel,AMD的CPU使用的都是小字節(jié)序,而例如Mac OS以前所使用的Power PC使用的便是大字節(jié)序(不過(guò)現(xiàn)在Mac OS也使用Intel的CPU了)。此外,除了大字節(jié)序和小字節(jié)序之外,還有一種很少見(jiàn)的中字節(jié)序(middle endian),它會(huì)以2143的方式來(lái)保存數(shù)據(jù)(相對(duì)于大字節(jié)序的1234及小字節(jié)序的4321)。
關(guān)于字節(jié)序的詳細(xì)說(shuō)明,您可以參考Wikipedia里的 Endianness條目 。
相關(guān).NET類庫(kù)
BinaryWriter和BinaryReader
在.NET框架操作數(shù)據(jù)流的時(shí)候,我們往往會(huì)使用BinaryWriter和BinaryReader進(jìn)行讀寫(xiě)。這兩個(gè)類中都有對(duì)應(yīng)的WriteInt32或是ReadInt32方法,那么它們是如何處理字節(jié)序的呢?從MSDN上我們了解到 BinaryReader使用小字節(jié)序讀取數(shù)據(jù) 。這意味著:
var stream = new MemoryStream ( new byte [] { 4, 1, 0, 0 }); var reader = new BinaryReader (stream); int i = reader.ReadInt32(); // i == 260與之類似,自然BinaryWriter也是使用小字節(jié)序來(lái)寫(xiě)入數(shù)據(jù)。
BitConverter
有時(shí)候我們還會(huì)使用BitConverter來(lái)轉(zhuǎn)化byte數(shù)組及一個(gè)32位整數(shù)(自然也包括其他類型),這也是涉及到字節(jié)序的操作,那么它們又是如何處理的呢?與BinaryWriter和BinaryReader的“固定策略”不同,BitConverter的行為是平臺(tái)相關(guān)的。
首先,BitConverter有一個(gè)只讀靜態(tài)字段IsLittleEndian,它表示當(dāng)前平臺(tái)的字節(jié)序。由于我們?yōu)椴煌腃PU會(huì)安裝不同的.NET類庫(kù),因此您現(xiàn)在如果通過(guò).NET Reflector來(lái)查看這個(gè)字段會(huì)發(fā)現(xiàn)它被設(shè)置為一個(gè)常量true。那么接下來(lái),BitConverter上的各個(gè)方便便會(huì)根據(jù)IsLittleEndian的值產(chǎn)生不同行為了,例如它的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]; } }顯然,這里會(huì)根據(jù)IsLittleEndian返回不同的值。
判斷當(dāng)前平臺(tái)的字節(jié)序
在.NET Framework中BitConverter.IsLittleEndian字段是一個(gè)常量,也就是說(shuō)它在編譯期便寫(xiě)入了一個(gè)靜態(tài)的值。那么我們?nèi)绻胍ㄟ^(guò)代碼來(lái)判斷當(dāng)前平臺(tái)的字節(jié)序,又該怎么做呢?其實(shí)這很簡(jiǎn)單:
static unsafe bool IsLittleEndian() { int i = 1; byte * b = ( byte *)&i; return b[0] == 1; }這里我們通過(guò)檢查32位整數(shù)1的第一個(gè)字節(jié)來(lái)確定當(dāng)前平臺(tái)的字節(jié)序。當(dāng)然,我們也可以使用其他類型,例如:
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); }這段代碼來(lái)自mono的BitConverter類庫(kù),至于它為什么使用double而不是int,我也不是很清楚。
Buffer.BlockCopy方法
.NET類庫(kù)中自帶一個(gè) Buffer.BlockCopy 方法,它的作用是將一個(gè)數(shù)組的字節(jié)——不是元素——復(fù)制到另一個(gè)數(shù)組中去。換句話說(shuō),一個(gè)長(zhǎng)度為100的int數(shù)組經(jīng)過(guò)完整的復(fù)制后,就變成了長(zhǎng)度為50的long數(shù)組,因?yàn)橐粋€(gè)int為4字節(jié),而long為8字節(jié)。從文檔上看,Buffer.BlockCopy是與字節(jié)序相關(guān)的,也就是說(shuō),同樣的.NET代碼在字節(jié)序不同的平臺(tái)上得到的結(jié)果可能不同。因此,我建議在使用這個(gè)方法的時(shí)候多加小心。
面向特定字節(jié)序編程
我們知道,BitConverter的工作結(jié)果是和當(dāng)前平臺(tái)的字節(jié)序相關(guān)的,但是在很多時(shí)候,尤其是根據(jù)某個(gè)公開(kāi)的協(xié)議進(jìn)行通信編程的時(shí)候,是需要固定一個(gè)字節(jié)序的。例如Tokyo Tyrant便要求每個(gè)整數(shù)都以大字節(jié)序的方式來(lái)通信——無(wú)論是發(fā)送還是讀取。為了保證.NET代碼的平臺(tái)無(wú)關(guān)性,我們不能直接使用BitConverter.GetBytes或ToInt32方法進(jìn)行轉(zhuǎn)化。那么我們?cè)撛趺崔k呢?最直觀的方法自然是手動(dòng)進(jìn)行轉(zhuǎn)換:
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); }由于我們可以通過(guò)BitConverter.IsLittleEndian來(lái)得到當(dāng)前平臺(tái)的字節(jié)序,我們也可以用它進(jìn)行判斷:
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都是依據(jù)小字節(jié)序進(jìn)行讀寫(xiě)的,因此我們也可以利用這點(diǎn)來(lái)讀寫(xiě)數(shù)據(jù)流。要不,接下來(lái)就由您試試看如何?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
