Go: 拆解網絡數據包(2)

本文將介紹如何使用 Go 來自定義簡單的應用層協議。我們稱該協議爲 TLV 即(type-length-value)指的是收到的網絡數據包,包含數據類型、數據長度和具體數據內容。TLV 實現方式使用固定字節數來表示數據類型和數據長度,而發送的具體數據內容長度是不固定的。這裏我們的實現使用 5 個字節的包頭:1 個字節表示數據類型和 4 個字節表示發送的數據長度。TLV 實現方式允許您將數據作爲字節序列發送到遠程節點,並從遠程節點上根據字節序列組合出相同的數據類型。如下代碼所示:

package networking

import (
    "bytes"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "net"
    "reflect"
    "testing"
)

const (
    //使用一個字節無符號整數定義兩種要發送的數據類型
    BinaryType uint8 = iota + 1  //1代表發送的數據是二進制類型數據
    StringType                   //2代表發送的是字符串

    MaxPayloadSize uint32 = 10 << 20 //10MB
)

var ErrMaxPayloadSize = errors.New("maximum payload size exceeded")

//定義一個解析數據包的接口
type Payload interface {
    fmt.Stringer
    io.ReaderFrom   //拆解網絡數據包方法
    io.WriterTo     //封裝網絡數據包方法
    Bytes() []byte
}

上面代碼創建常量定義兩種數據包類型:BinaryType 和 StringType。如果你理解了每種類型的實現,你就可以根據自己的需求實現自己的協議。爲了安全起見,你需要創建一個最大發送數據字節數,我們在後面會討論的。

上面代碼還定義了一個 Payload 接口,包含必須實現的方法。每種類型都要實現四個方法:Bytes、String、ReadFrom 和 WriteTo。其中 io.ReadFrom 和 io.WriteTo 方法分別從網絡輸入接口讀取數據和寫入數據到網絡輸出接口中。

接下來就可以定義 TLV 的數據類型了,如下所示:

//定義二進制字節數據類型並實現Payload接口
type Binary []byte

func (m Binary)Bytes() []byte { return m}
func (m Binary)String() string { return string(m)}

//封裝二進制字節數據併發送到遠程節點
func (m Binary)WriteTo(w io.Writer) (int64, error) {
    err := binary.Write(w, binary.BigEndian, BinaryType) //按高位順序寫入類型佔1byte
    if err != nil {
        return 0, err
    }
    var n int64 = 1
    err = binary.Write(w, binary.BigEndian, uint32(len(m))) //負載字節數
    if err != nil{
        return n, err
    }
    n += 4
    o, err := w.Write(m)
    return n + int64(o), err
}

Binary 類型是一個字節切片,因此 Bytes 方法直接返回自己。String 方法將字節切片轉換成字符串返回。WriteTo 方法接收一個 io.Writer 參數以及返回寫入數據的字節數和一個 error 接口。WriteTo 方法首先寫入 1 字節數據作爲發送數據的類型。然後寫入 4 字節表示發送的二進制切片長度。最後寫入 Binary 數據,也就是要發送的數據內容。

//拆解二進制字節切片類型數據包
func (m *Binary)ReadFrom(r io.Reader) (int64, error) {
    var typ uint8
    err := binary.Read(r, binary.BigEndian, &typ) //讀取高位1字節
    if err != nil {
        return 0, err
    }
    var n int64 = 1
    if typ != BinaryType {
        return n, errors.New("invalid Binary")
    }
    var size uint32
    err = binary.Read(r, binary.BigEndian, &size) //讀取負載字節數
    if err != nil {
        return n, err
    }
    n += 4
    if size > MaxPayloadSize {
        return n, ErrMaxPayloadSize
    }
    *m = make([]byte, size)
    o, err := r.Read(*m) //負載
    return n + int64(o), err
}

ReadFrom 方法從 reader 網絡輸入接口中讀取 1 字節到 typ 變量中。接着驗證是否爲 BinaryType 類型才繼續。然後讀取後面 4 個字節數據到 size 變量,代碼要接收到 Binary 字節切片的長度。最後,填充 Binary 字節切片。

注意檢查最大負載大小。因爲用 4 字節整數來表示負載大小最大值爲 4,294,967,295,表示發送的最大數據不能超過 4GB。對於如此大的有效負載,惡意參與者很容易執行 Dos 攻擊,從而耗盡計算機上所有可用的隨機訪問內存 (RAM)。保持合理的最大有效負載可以提升內存耗盡攻擊的難度。

下面的代碼介紹了 String 類型,和 Binary 類型一樣實現 Payload 接口。

//定義字符串類型並實現Payload接口
type String string

func (s String) String() string {return string(s)}

func (s String) Bytes() []byte {return []byte(s)}

//封裝字符串類型的數據包併發送到遠程節點
func (s String) WriteTo(w io.Writer) (n int64, err error) {
    err = binary.Write(w, binary.BigEndian, StringType) //高位寫入1字節類型
    if err != nil {
        return 0, err
    }
    n = 1
    err = binary.Write(w, binary.BigEndian, uint32(len(s))) //負載字節數
    if err != nil {
        return n, err
    }
    n += 4
    o, err := w.Write([]byte(s))
    return n + int64(o), err
}

String 實現 Bytes 方法直接將字符串轉爲字節切片即可。String 方法將 String 類型轉爲它的基礎類型。WriteTo 方法和 Binary 的 writeTo 方法類似,除了寫入第一個字節是 StringType 和將字符串轉爲字節切片再寫入網絡輸入接口 writer 中。

下面的代碼完成了 String 類型的 Payload 的實現。

func (s *String) ReadFrom(r io.Reader) (n int64, err error) {
    var typ uint8
    err = binary.Read(r, binary.BigEndian, &typ) //高位順序讀取1字節類型
    if err != nil {
        return 0, err
    }
    n = 1
    if typ != StringType {
        return n, errors.New("invalid String")
    }
    var size uint32
    err = binary.Read(r, binary.BigEndian, &size)
    if err != nil {
        return n, err
    }
    n += 4
    buf := make([]byte, size)
    o, err := r.Read(buf)
    *s = String(buf[:o])
    return n + int64(o), err
}

這裏 ReadFrom 和 Binary 的一樣,除了兩個地方。第一先對比 typ 變量類型是 StringType 再繼續。第二,將數據轉爲 String 類型返回。

剩下要實現的就是從網絡連接讀取任意數據並使用我們實現的兩種類型來解析數據包。

//拆解任意類型的數據包
func decode(r io.Reader) (Payload, error) {
    var typ uint8
    err := binary.Read(r, binary.BigEndian, &typ)
    if err != nil {
        return nil, err
    }
    var payload Payload
    switch typ {
    case BinaryType:
        payload = new(Binary)
    case StringType:
        payload = new(String)
    default:
        return nil, errors.New("unknown type")
    }
    _, err = payload.ReadFrom(
        io.MultiReader(bytes.NewReader([]byte{typ}), r))
    if err != nil {
        return nil, err
    }
    return payload, nil
}

decode 函數接收一個 io.Reader 參數並返回一個 Payload 接口實例和一個 error。如果 decode 不能對讀取到的數據解碼爲 Bianry 或 StringType 類型,將返回 error 和 nil。

你必須從 reader 中讀取 1 個字節才能判斷是哪種數據類型,並創建 payload 變量來存儲解碼數據。如果從 reader 中讀取的類型是已經定義的其中一種,然後定義對應的類型並賦值給 payload 變量。

知道數據的類型以後,就可以根據特定的類型來對網絡中讀取的數據進行解碼。但是你不能簡單的將 reader 傳給 ReadFrom 方法。前面已經從 reader 中將第一個字節的類型數據讀取出來了,而 ReadFrom 方法也需要讀取第一個字節數據來判斷數據類型。幸虧 io 包有一個函數可以使用:MultiReader。可以使用它來將已經讀取的數據重寫到 Reader 裏面去。這樣 ReadFrom 就可以繼續按順序讀取數據並解析。

儘管 io.MultiReader 可以實現字節切片注入到 reader 中去,但並不是最好的方法。正確的解決方法是在 ReadFrom 中不用讀取第一個字節。decode 函數已經知道接收的數據類型了,可以直接調用對應的 ReadFrom 方法,解析剩下的數據即可。讀者可以自行實現。

下面我們來測試下 decode 函數:

func TestPayloads(t *testing.T)  {
    //服務端
    b1 := Binary("Clear is better than clever.")
    b2 := Binary("Don't panic")
    s1 := String("Errors are values.")
    payloads := []Payload{&b1, &s1, &b2}
    listener, err := net.Listen("tcp", "127.0.0.1:")
    if err != nil {
        t.Fatal(err)
    }
    go func() {
        conn, err := listener.Accept()
        if err != nil {
            t.Error(err)
            return
        }
        defer conn.Close()
        for _, p := range payloads {
            _, err = p.WriteTo(conn)
            if err != nil {
                t.Error(err)
                break
            }
        }
    }()

測試代碼先創建要發送的數據類型。這裏我們創建了兩個 Binary 類型和一個 String 類型的數據。然後創建一個 Payload 接口切片,並將創建的類型的地址添加到切片中。然後創建一個 listener 將接收網絡連接將切片中的每種類型數據寫進網絡輸入接口。

    //客戶端
    conn, err := net.Dial("tcp", listener.Addr().String())
    if err != nil {
        t.Fatal(err)
    }
    defer conn.Close()
    for i := 0; i < len(payloads); i++{
        actual, err := decode(conn)
        if err != nil{
            t.Fatal(err)
        }
        if expected := payloads[i]; !reflect.DeepEqual(expected, actual) {
            t.Errorf("value mismatch: %v != %v", expected, actual)
            continue
        }
        t.Logf("[%T] %[1]q", actual)
    }
}

測試中你知道總共發送了多少種類型的數據,因此初始化一個連接到 listener,然後對接收到的數據進行解碼。最後比較你解碼的類型和服務器發送的類型。如果發送的數據不一致測試就失敗。

下面測試下發送最大數據負載情況:

func TestMaxPayloadSize(t *testing.T)  {
    buf := new(bytes.Buffer)
    err := buf.WriteByte(BinaryType)
    if err != nil {
        t.Fatal(err)
    }
    err = binary.Write(buf, binary.BigEndian, uint32(1 << 30)) //1GB
    if err != nil {
        t.Fatal(err)
    }
    var b Binary
    _, err = b.ReadFrom(buf)
    if err != ErrMaxPayloadSize {
        t.Fatalf("expected ErrMaxPayloadSize; actual: %v", err)
    }
}

該測試創建了一個 bytes.Buffer,包含 BinaryType 類型和 4 字節無符號整數表示 1GB 的數據。如果發送的數據是 1GB,已經超過我們定義的最大 10MB 限制了,雖然 4 字節可以最大表示 4GB 的數據,但是出於安全等原因一般不會發送這麼大的數據包的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QjKRWpKFtffMFtoFnfw9Cw