在寫網絡程序的時候,我們經常需要將結構體或者整數等數據類型序列化成二進制的buffer串。或者從一個buffer中解析出來一個結構體出來,最典型的就是在協議的header部分表徵head length 或者body length在拼包和拆包的過程中,需要按照規定的整數類型進行解析,且涉及到大小端序的問題。
1.C中是怎麼操作的
在C中我們最簡單的方法是用memcpy來一個整形數或者結構體等其他類型複製到一塊內存中,然後在強轉回需要的類型。如:
// produce int a = 32; char *buf = (char *)malloc(sizeof(int)); memcpy(buf,&a,sizeof(int)); // consume int b ; memcpy(&b,buf,sizeof(int))
必要的時候採用ntoh/hton系列函數進行大小端序的轉換。
2.golang中操作
通過"encoding/binary"可以提供常用的二進制序列化的功能。該模塊主要提供瞭如下幾個接口:
func Read(r io.Reader, order ByteOrder, data interface{}) error func Write(w io.Writer, order ByteOrder, data interface{}) error func Size(v interface{}) int var BigEndian bigEndian var LittleEndian littleEndian /* type ByteOrder interface { Uint16([]byte) uint16 Uint32([]byte) uint32 Uint64([]byte) uint64 PutUint16([]byte, uint16) PutUint32([]byte, uint32) PutUint64([]byte, uint64) String() string } /*
通過Read接口可以將buf中得內容填充到data參數表示的數據結構中,通過Write接口可以將data參數裏面包含的數據寫入到buffer中。 變量BigEndian和LittleEndian是實現了ByteOrder接口的對象,通過接口中提供的方法可以直接將uintx類型序列化(uintx())或者反序列化(putuintx())到buf中。
2.1將結構體序列化到一個buf中
在序列化結構對象時,需要注意的是,被序列化的結構的大小必須是已知的,可以通過Size接口來獲得該結構的大小,從而決定buffer的大小。
i := uint16(1) size := binary.Size(i)
固定大小的結構體,就要求結構體中不能出現[]byte這樣的切片成員,否則Size返回-1,且不能進行正常的序列化操作。
type A struct { // should be exported member when read back from buffer One int32 Two int32 } var a A a.One = int32(1) a.Two = int32(2) buf := new(bytes.Buffer) fmt.Println("a's size is ",binary.Size(a)) binary.Write(buf,binary.LittleEndian,a) fmt.Println("after write ,buf is:",buf.Bytes())
對應的輸出爲:
a's size is 8 after write ,buf is : [1 0 0 0 2 0 0 0]
通過Size可以得到所需buffer的大小。通過Write可以將對象a的內容序列化到buffer中。這裏採用了小端序的方式進行序列化(x86架構都是小端序,網絡字節序是大端序)。
對於結構體中得“_”成員不進行序列化。
2.2從buf中反序列化回一個結構
從buffer中讀取時,一樣要求結構體的大小要固定,且需要反序列化的結構體成員必須是可導出的也就是必須是大寫開頭的成員,同樣對於“_”不進行反序列化:
type A struct { // should be exported member when read back from buffer One int32 Two int32 } var aa A buf := new(bytes.Buffer) binary.Write(buf,binary.LittleEndian,a) binary.Read(buf,binary.LittleEndian,&aa) fmt.Println("after aa is ",aa)
輸出爲:
after write ,bufis : [1 0 0 0 2 0 0 0] before aa is : {0 0} after aa is {1 2}
這裏使用Read從buffer中將數據導入到結構體對象aa中。如果結構體中對應的成員不是可導出的,那麼在轉換的時候會panic出錯。
2.3將整數序列化到buf中,並從buf中反序列化出來
我們可以通過Read/Write直接去讀或者寫一個uintx類型的變量來實現對整形數的序列化和反序列化。由於在網絡中,對於整形數的序列化非常常用,因此係統庫提供了type ByteOrder接口可以方便的對uint16/uint32/uint64進行序列化和反序列化:
int16buf := new(bytes.Buffer) i := uint16(1) binary.Write(int16buf,binary.LittleEndian,i) fmt.Println(“write buf is:”int16buf.Bytes()) var int16buf2 [2]byte binary.LittleEndian.PutUint16(int16buf2[:],uint16(1)) fmt.Println("put buffer is :",int16buf2[:]) ii := binary.LittleEndian.Uint16(int16buf2[:]) fmt.Println("Get buf is :",ii)
輸出爲:
write buffer is : [1 0] put buf is: [1 0] Get buf is : 1
通過調用binary.LittleEndian.PutUint16,可以按照小端序的格式將uint16類型的數據序列化到buffer中。通過binary.LittleEndian.Uint16將buffer中內容反序列化出來。
3. 一個實在的例子
我們來看一個網絡包包頭的定義和初始化:
type Head struct { Cmd byte Version byte Magic uint16 Reserve byte HeadLen byte BodyLen uint16 } func NewHead(buf []byte)*Head{ head := new(Head) head.Cmd = buf[0] head.Version = buf[1] head.Magic = binary.BigEndian.Uint16(buf[2:4]) head.Reserve = buf[4] head.HeadLen = buf[5] head.BodyLen = binary.BigEndian.Uint16(buf[6:8]) return head }
這個是一個常見的在tcp 拼包得例子。在例子中通過binary.BigEndian.Uint16將數據按照網絡序的格式讀出來,放入到head中對應的結構裏面。