Go語言小貼士2 - 協議解析

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

今天這個小貼士主要介紹協議解析的一些知識,Go語言作為服務端程式設計語言,免不了要涉及到通訊協議解析,即便不是做網路通訊,也難免會涉及到檔案解析,其實它們的知識點都是一樣的。現實應用情境中,通訊協議按通常可以分為兩類:二進位協議和文本協議。Go語言內建的gob格式就是一種二進位協議,而JSON、XML等則是文本協議。

假設我們要發送123這個數值,用二進位協議只需要一個位元組,因為一個位元組(byte)有8個二進位位(bit),2的8次方是256,一個位元組可以表達0-255之間的任意值,共256種可能性。

如果我們用文本協議發送123這個數值,則需要至少三個位元組,因為123這個數字需要轉換成字元'1'、'2'、'3'這三個ASCII字元,存入三個位元組中。

所以同樣一個資料,用二進位協議表達的體積通常會小於用文本協議表達的體積。這個特性體現到網路應用中,可能就是網路頻寬需求的差異。

換個角度看,當我們用二進位協議把123這個數值寫入一個檔案以後,我們用文字編輯器開啟它,看到的會'{'這個字元,因為這個字元的ASCII值正好是123。而當我們用文本協議儲存資料時,我們可以用文字編輯器直接讀到123這個數值。

所以通常二進位協議比較不利於閱讀,而文本協議方便閱讀。這個特性體現到開發中的時候,可能就是調試難易度的差異。

位元據和文本資料還有個差異是執行效率差異。以123這個值為例,二進位序列化時候只需要直接對一個位元組進行賦值,而用用文字格式設定的時候,則需要計算出‘個’、‘十’、‘百’位上的值,並轉成ASCII碼,再賦值給三個位元組,還原序列化的時候也是如此。

以上分析了二進位協議和文本協議的一些特性,並沒有說哪個是最優方案,因為不同的應用情境會需要不同的技術方案。比如TCP/IP協議是二進位協議,在TCP/IP之上構建的HTTP協議則是文本協議,它們各有各的應用情境,所以會出現技術上的差異。

再說回協議解析,當我們要解析位元據的時候,通常會需要用到Go語言內建的encoding/binary這個包,這個包內建了大端序和小端序的位元據操作。

什麼是大端序和小端序呢?以數值256為例,上面我們有提到,一個位元組可以表達0-255之間的任意值,但是當我們要表達256這個值的時候怎麼表達呢?

255用二進位表達就是1111 1111,再加1就是1 0000 0000,多了一個1出來,顯然我們需要再用額外的一個位元組來存放這個1,但是這個1要存放在第一個位元組還是第二個位元組呢?這時候因為人們選擇的不同,就出現了大端序和小端序的差異。

當我們把這個1放在第一個位元組的時候,就稱之為大端序格式。當我們把1放在第二個位元組的時候,就稱之為小端序格式。

這兩種格式顯然沒辦法說誰更好,所以兩個格式一直都各自的支援者,如果是按標準實現一個通訊協議,那就得嚴格按照標準上說的位元組序來實現。如果是自訂的二進位協議,選擇哪個格式按自己喜好就可以了。

encoding/binary包中的全域變數BigEndian用於操作大端序資料,LittleEndian用於操作小端序資料,這兩個變數所對應的資料類型都實行了ByteOrder介面:

type ByteOrder interface {        Uint16([]byte) uint16        Uint32([]byte) uint32        Uint64([]byte) uint64        PutUint16([]byte, uint16)        PutUint32([]byte, uint32)        PutUint64([]byte, uint64)        String() string}

其中,前三個方法用於讀取資料,後三個方法用於寫入資料。

大家可能會注意到,上面的方法操作的都是無符號整型,如果我們要操作有符號整型的時候怎麼辦呢?很簡單,強制轉換就可以了,比如這樣:

func PutInt32(b []byte, v int32) {        binary.BigEndian.PutUint32(b, uint32(v))}

大家可能還注意到,上面提供的方法都是操作整型值。原因是浮點數的實現在不同程式設計語言中可能會不一樣,沒辦法在執行階段程式庫中給出一個普適的標準。如果我們要寫入和讀取浮點數怎麼辦呢?

項目實踐上有兩種做法,一種是在協議上約定好一個取整精度,比如小數點後多少位,然後把浮點數轉成對應精度的整數,這樣的做法跨語言相容性最好。

如果是Go語言開發的應用之間的位元據交換,或者是符合IEEE 754浮點數標準的程式設計語言,則可以用math包裡的這幾個函數:

func Float32bits(f float32) uint32func Float32frombits(b uint32) float32func Float64bits(f float64) uint64func Float64frombits(b uint64) float64

上面說到的都是數實值型別的操作,但是實際應用情境中文本,列表,字典等各種複雜資料要怎麼在二進位協議中實現呢?

複雜的位元據表達,最主要的一個問題就是資料分割問題,比如我們要將下面這個Go結構體進行二進位序列化:

type MyStruct struct {        Field1 int32        Field2 string        Field3 []int16}

首先我們會遇到的問題就是怎麼區別各個欄位?

首先我們對結構體進行分析,其中第一個欄位是int32類型的,這種資料類型固定都會被表達成4個位元組,所以我們稱之為定長類型。第二個欄位是string類型的,字串裡面內容多上是不一定的,所以我們稱之為變長資料類型。第三個欄位是[]int16類型的,列表中元素個數是不一定的,但是每個元素的位元組長度是固定的。

對於字串,我們可以在字串開始前用兩個位元組來存放它的長度,這樣我們的字串可以存放65536個字元(2的16次方)。對於數組,一樣可以用兩個位元組來存放它的元素個數。

這樣我們就可以得到以下的序列化和還原序列化代碼:

package mainimport (    "fmt"    "encoding/binary")func main() {        var s1 = MyStruct {123, "456", []int16{1,2,3}}        var s2 MyStruct        s2.Unmarshal(s1.Marshal())    fmt.Println(s1, s2)}type MyStruct struct {        Field1 int32        Field2 string        Field3 []int16}func (s *MyStruct) binarySize() int {        return 4 +                    // Field1               2 + len(s.Field2) +    // Len + Field2               2 + 2 * len(s.Field3)  // Len + Field3}func (s *MyStruct) Marshal() []byte {        b := make([]byte, s.binarySize())        n := 0        binary.BigEndian.PutUint32(b[n:], uint32(s.Field1))        n += 4        binary.BigEndian.PutUint16(b[n:], uint16(len(s.Field2)))        n += 2        copy(b[n:], s.Field2)        n += len(s.Field2)        binary.BigEndian.PutUint16(b[n:], uint16(len(s.Field3)))        n += 2        for i := 0; i < len(s.Field3); i ++ {                binary.BigEndian.PutUint16(b[n:], uint16(s.Field3[i]))                n += 2        }        return b}func (s *MyStruct) Unmarshal(b []byte) {        n := 0        s.Field1 = int32(binary.BigEndian.Uint32(b[n:]))        n += 4    x := int(binary.BigEndian.Uint16(b[n:]))    n += 2        s.Field2 = string(b[n : n + x])        n += x        s.Field3 = make([]int16, binary.BigEndian.Uint16(b[n:]))        n += 2        for i := 0; i < len(s.Field3); i ++ {                s.Field3[i] = int16(binary.BigEndian.Uint16(b[n:]))                n += 2        }}

上面用到的協議設計技巧,同樣適用於不定長的訊息包的發送。在很多應用情境中,訊息包的長度是不固定的,就像上面的字串欄位一樣。我們一樣可以用開頭固定的幾個位元組來存放訊息長度,在解析通訊協議的時候就可以從位元組流中截出一個個的訊息包了,這樣的操作通常叫做協議分包或者粘包處理。

貼個從Socket讀取訊息包的虛擬碼(沒編譯):

func ReadPacket(conn net.Conn) ([]byte, error) {        var head [2]byte        if _, err := io.ReadFull(conn, head[:]); err != nil {                return err        }        size := binary.BigEndian.Uint16(head)        packet := make([]byte, size)        if _, err := io.ReadFull(conn, packet); err != nil {                return err        }        return packet}

上面的代碼就用到了前一個小貼士中說到的io.ReadFull來確保一次讀取完整資料。

要注意,這段代碼不是安全執行緒的,如果有兩個線程同時對一個net.Conn進行ReadPacket操作,很可能會發生嚴重錯誤,具體邏輯請自行分析。

從上面結構體序列化和還原序列化的代碼中,大家不難看出,實現一個二進位協議是挺繁瑣和容易出BUG的,只要稍微有一個數值計算錯就解析出錯了。

所以在工程實踐中,不推薦大家手寫二進位協議的解析代碼,項目中通常會用自動化的工具來輔助產生代碼。

因為篇幅限制,這篇文章沒辦法進一步介紹文本協議相關的知識,因為之後有準備講bufiojson,這兩者都會涉及到文本協議相關的知識,所以就放到以後的文章中再介紹。

Go語言小貼士全面徵集大家的建議和反饋,歡迎留言。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.