字節序(byte order)關系到多字節整數(short/int16、int/int32,int64)和浮點數的各字節在內存中的存放順序。字節序分為兩種:小端字節序(little endian)和大端字節序(big endian)。小端字節序:低字節存放在內存低地址,例如對兩字節整數0x0100(十進制數256),低字節00放在低地址(假設地址為0x0041f880),高字節01放在高地址0x0041f881。大端字節序:高字節在低地址,同樣是0x0100,高字節01放在低地址(假設地址為0x0041f880),低字節00放高地址0x0041f881。可見對相同的兩字節整數,在不同字節序的機器上其內存布局是不同的,反過來內存布局相同的,在不同字節序的機器上被解釋為不同的整數值,除非這幾個字節值相同。
字節序是由cpu處理器架構決定的,和操作系統無關,例如Intel cpu采用小端字節序,PowerPC采用大端字節序,有些cpu例如Alpha支持兩種字節序,但在使用時要設置具體采用哪一種字節序,不可能同時用兩種。主機字節序(host byte order)就是指當前機器的字節序,根據cpu處理器的架構和設置,主機字節序可為小端字節序或大端字節序。關於字節序問題,較全面的描述見https://en.wikipedia.org/wiki/Endianness。
在socket網絡編程中通常會涉及到多字節整數、浮點數的傳輸,如果兩台機器字節序不同,直接傳多字節整數或浮點數會導致雙方將這些多字節解釋成不同的數字,所以要在網絡協議中規定編解碼方式,例如有的協議將整數編碼成字符串來避免字節序問題,但只要協議中有多字節整數,都要規定采用什么字節序來表示協議中的多字節整數(除非能保證兩台機器的字節序是相同的),也就出現了網絡字節序,網絡字節序其實就是大端字節序,協議當然也可采用小端字節序,只要雙方統一就行。
如上所述,在設計網絡二進制協議時,對多字節整數,要規定打包傳輸時的字節序:網絡字節序還是小端字節序。客戶端和服務器代碼在打包和解包時,對多字節整數,要進行主機字節序和協議規定的字節序的相互轉化。
Java應用通常使用java.nio.ByteBuffer進行協議數據的打包和解包,其order(ByteOrder bo)方法可設置打包或解包使用的字節序;如果使用netty框架,可使用ByteBuf類的order方法。
C/C++應用通常使用C庫中的如下函數來進行主機字節序和網絡字節序的相互轉換。
// hton* 主機字節轉網絡字節序
uint64_t htonll(uint64_t hostlonglong);
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
// ntoh* 網絡字節序轉主機字節序
uint64_t ntohll(uint64_t hostlonglong);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
linux系統在endian.h頭文件中提供了更多的函數進行主機字節和大小端字節序的相互轉換,如下:
uint16_t htobe16(uint16_t host_16bits);
uint16_t htole16(uint16_t host_16bits);
uint16_t be16toh(uint16_t big_endian_16bits);
uint16_t le16toh(uint16_t little_endian_16bits);
uint32_t htobe32(uint32_t host_32bits);
uint32_t htole32(uint32_t host_32bits);
uint32_t be32toh(uint32_t big_endian_32bits);
uint32_t le32toh(uint32_t little_endian_32bits);
uint64_t htobe64(uint64_t host_64bits);
uint64_t htole64(uint64_t host_64bits);
uint64_t be64toh(uint64_t big_endian_64bits);
uint64_t le64toh(uint64_t little_endian_64bits);
htobe*(例如htobe16)表示主機字節序到大端字節序(網絡字節序);htole*表示主機字節序到小端字節序;be*toh表示大端到主機;le*toh表示小端到主機。
上面的字節序轉換函數有個缺點,就是方法太多不方便使用,需要根據多字節整數的類型(uint16_t/int16_t/uint32_t/int32_t/uint64_t/int64_t)來調用不同的轉換函數,所以在c++應用中利用模板技術編寫了4個統一的字節序轉換函數,和整數的類型無關。如下:
/* * ByteOrderUtil.h * * Created on: Nov 15, 20xx * Author: wanshi */ #ifndef BYTEORDERUTIL_H_ #define BYTEORDERUTIL_H_ #include <stdint.h> namespace ByteOrder { const uint16_t us_flag = 1; // little_end_flag 表示主機字節序是否小端字節序 const bool little_end_flag = *((uint8_t*)&us_flag) == 1; //小端到主機 template<typename T> T le_to_host(T& from) { T to; uint8_t byteLen = sizeof(T); if(little_end_flag){ return from; } else{ char* to_char = (char*)&to; char* from_char = (char*)&from; for(int i=0;i<byteLen;i++){ to_char[i] = from_char[byteLen-i-1]; //此處也可用移位操作來實現 } return to; } } //主機到小端 template<typename T> T host_to_le(T& from) { return le_to_host(from); } //大端到主機 template<typename T> T be_to_host(T& from) { T to; uint8_t byteLen = sizeof(T); if(!little_end_flag){ return from; } else{ char* to_char = (char*)&to; char* from_char = (char*)&from; for(int i=0;i<byteLen;i++){ to_char[i] = from_char[byteLen-i-1]; //此處也可用移位操作來實現 } return to; } } //主機到大端 template<typename T> T host_to_be(T& from) { return be_to_host(from); } } #endif /* BYTEORDERUTIL_H_ */ 使用演示: #include "ByteOrderUtil.h" using namespace ByteOrder; int main(int argc,char** argv) { uint16_t u16t = 0x1514; //host到小端 uint16_t leu16t = host_to_le(u16t); uint16_t hu16t = le_to_host(leu16t); uint64_t u64t = 0x15141312; //host到大端 uint64_t beu64t = host_to_be(u64t); uint64_t hu64t = be_to_host(beu64t); return 0; }