Go: 拆解網(wǎng)絡(luò)數(shù)據(jù)包(2)

前面一篇文章介紹Go使用分隔符的方法來拆解網(wǎng)絡(luò)包時提到颁虐,還有另一種方式即根據(jù)協(xié)議來拆解包姓言。客戶端和服務(wù)端一般都會提前定義好收到的網(wǎng)絡(luò)數(shù)據(jù)的字節(jié)排列方式。類似我們在學(xué)習(xí)計算機(jī)網(wǎng)絡(luò)中的其他協(xié)議位仁,包含協(xié)議頭和實際發(fā)送數(shù)據(jù)兩大部分晒杈。協(xié)議頭里面定義根據(jù)需求會定義相應(yīng)的協(xié)議字段嫂伞。每個字段都會根據(jù)所占的字節(jié)數(shù)來讀取。

本文將介紹如何使用Go來自定義簡單的應(yīng)用層協(xié)議拯钻。我們稱該協(xié)議為TLV即(type-length-value)指的是收到的網(wǎng)絡(luò)數(shù)據(jù)包帖努,包含數(shù)據(jù)類型、數(shù)據(jù)長度和具體數(shù)據(jù)內(nèi)容粪般。TLV實現(xiàn)方式使用固定字節(jié)數(shù)來表示數(shù)據(jù)類型和數(shù)據(jù)長度然磷,而發(fā)送的具體數(shù)據(jù)內(nèi)容長度是不固定的。這里我們的實現(xiàn)使用5個字節(jié)的包頭:1個字節(jié)表示數(shù)據(jù)類型和4個字節(jié)表示發(fā)送的數(shù)據(jù)長度刊驴。TLV實現(xiàn)方式允許您將數(shù)據(jù)作為字節(jié)序列發(fā)送到遠(yuǎn)程節(jié)點姿搜,并從遠(yuǎn)程節(jié)點上根據(jù)字節(jié)序列組合出相同的數(shù)據(jù)類型寡润。如下代碼所示:

package networking

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

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

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

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

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

上面代碼創(chuàng)建常量定義兩種數(shù)據(jù)包類型:BinaryType和StringType。如果你理解了每種類型的實現(xiàn)舅柜,你就可以根據(jù)自己的需求實現(xiàn)自己的協(xié)議梭纹。為了安全起見,你需要創(chuàng)建一個最大發(fā)送數(shù)據(jù)字節(jié)數(shù)致份,我們在后面會討論的变抽。

上面代碼還定義了一個Payload接口,包含必須實現(xiàn)的方法氮块。每種類型都要實現(xiàn)四個方法:Bytes绍载、String、ReadFrom和WriteTo滔蝉。其中io.ReadFrom和io.WriteTo方法分別從網(wǎng)絡(luò)輸入接口讀取數(shù)據(jù)和寫入數(shù)據(jù)到網(wǎng)絡(luò)輸出接口中击儡。

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

//定義二進(jìn)制字節(jié)數(shù)據(jù)類型并實現(xiàn)Payload接口
type Binary []byte

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

//封裝二進(jìn)制字節(jié)數(shù)據(jù)并發(fā)送到遠(yuǎn)程節(jié)點
func (m Binary)WriteTo(w io.Writer) (int64, error) {
    err := binary.Write(w, binary.BigEndian, BinaryType) //按高位順序?qū)懭腩愋驼?byte
    if err != nil {
        return 0, err
    }
    var n int64 = 1
    err = binary.Write(w, binary.BigEndian, uint32(len(m))) //負(fù)載字節(jié)數(shù)
    if err != nil{
        return n, err
    }
    n += 4
    o, err := w.Write(m)
    return n + int64(o), err
}

Binary類型是一個字節(jié)切片蝠引,因此Bytes方法直接返回自己阳谍。String方法將字節(jié)切片轉(zhuǎn)換成字符串返回。WriteTo方法接收一個io.Writer參數(shù)以及返回寫入數(shù)據(jù)的字節(jié)數(shù)和一個error接口螃概。WriteTo方法首先寫入1字節(jié)數(shù)據(jù)作為發(fā)送數(shù)據(jù)的類型矫夯。然后寫入4字節(jié)表示發(fā)送的二進(jìn)制切片長度。最后寫入Binary數(shù)據(jù)吊洼,也就是要發(fā)送的數(shù)據(jù)內(nèi)容训貌。

//拆解二進(jìn)制字節(jié)切片類型數(shù)據(jù)包
func (m *Binary)ReadFrom(r io.Reader) (int64, error) {
    var typ uint8
    err := binary.Read(r, binary.BigEndian, &typ) //讀取高位1字節(jié)
    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) //讀取負(fù)載字節(jié)數(shù)
    if err != nil {
        return n, err
    }
    n += 4
    if size > MaxPayloadSize {
        return n, ErrMaxPayloadSize
    }
    *m = make([]byte, size)
    o, err := r.Read(*m) //負(fù)載
    return n + int64(o), err
}

ReadFrom方法從reader網(wǎng)絡(luò)輸入接口中讀取1字節(jié)到typ變量中。接著驗證是否為BinaryType類型才繼續(xù)冒窍。然后讀取后面4個字節(jié)數(shù)據(jù)到size變量递沪,代碼要接收到Binary字節(jié)切片的長度。最后超燃,填充Binary字節(jié)切片区拳。

注意檢查最大負(fù)載大小。因為用4字節(jié)整數(shù)來表示負(fù)載大小最大值為4,294,967,295意乓,表示發(fā)送的最大數(shù)據(jù)不能超過4GB樱调。對于如此大的有效負(fù)載,惡意參與者很容易執(zhí)行Dos攻擊届良,從而耗盡計算機(jī)上所有可用的隨機(jī)訪問內(nèi)存(RAM)笆凌。保持合理的最大有效負(fù)載可以提升內(nèi)存耗盡攻擊的難度。

下面的代碼介紹了String類型士葫,和Binary類型一樣實現(xiàn)Payload接口乞而。

//定義字符串類型并實現(xiàn)Payload接口
type String string

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

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

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

String實現(xiàn)Bytes方法直接將字符串轉(zhuǎn)為字節(jié)切片即可。String方法將String類型轉(zhuǎn)為它的基礎(chǔ)類型慢显。WriteTo方法和Binary的writeTo方法類似爪模,除了寫入第一個字節(jié)是StringType和將字符串轉(zhuǎn)為字節(jié)切片再寫入網(wǎng)絡(luò)輸入接口writer中欠啤。

下面的代碼完成了String類型的Payload的實現(xiàn)。

func (s *String) ReadFrom(r io.Reader) (n int64, err error) {
    var typ uint8
    err = binary.Read(r, binary.BigEndian, &typ) //高位順序讀取1字節(jié)類型
    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再繼續(xù)。第二共郭,將數(shù)據(jù)轉(zhuǎn)為String類型返回祠丝。

剩下要實現(xiàn)的就是從網(wǎng)絡(luò)連接讀取任意數(shù)據(jù)并使用我們實現(xiàn)的兩種類型來解析數(shù)據(jù)包。

//拆解任意類型的數(shù)據(jù)包
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函數(shù)接收一個io.Reader參數(shù)并返回一個Payload接口實例和一個error除嘹。如果decode不能對讀取到的數(shù)據(jù)解碼為Bianry或StringType類型写半,將返回error和nil。

你必須從reader中讀取1個字節(jié)才能判斷是哪種數(shù)據(jù)類型尉咕,并創(chuàng)建payload變量來存儲解碼數(shù)據(jù)叠蝇。如果從reader中讀取的類型是已經(jīng)定義的其中一種,然后定義對應(yīng)的類型并賦值給payload變量龙考。

知道數(shù)據(jù)的類型以后蟆肆,就可以根據(jù)特定的類型來對網(wǎng)絡(luò)中讀取的數(shù)據(jù)進(jìn)行解碼矾睦。但是你不能簡單的將reader傳給ReadFrom方法晦款。前面已經(jīng)從reader中將第一個字節(jié)的類型數(shù)據(jù)讀取出來了,而ReadFrom方法也需要讀取第一個字節(jié)數(shù)據(jù)來判斷數(shù)據(jù)類型枚冗。幸虧io包有一個函數(shù)可以使用:MultiReader缓溅。可以使用它來將已經(jīng)讀取的數(shù)據(jù)重寫到Reader里面去赁温。這樣ReadFrom就可以繼續(xù)按順序讀取數(shù)據(jù)并解析坛怪。

盡管io.MultiReader可以實現(xiàn)字節(jié)切片注入到reader中去,但并不是最好的方法股囊。正確的解決方法是在ReadFrom中不用讀取第一個字節(jié)袜匿。decode函數(shù)已經(jīng)知道接收的數(shù)據(jù)類型了,可以直接調(diào)用對應(yīng)的ReadFrom方法稚疹,解析剩下的數(shù)據(jù)即可居灯。讀者可以自行實現(xiàn)。

下面我們來測試下decode函數(shù):

func TestPayloads(t *testing.T)  {
    //服務(wù)端
    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
            }
        }
    }()

測試代碼先創(chuàng)建要發(fā)送的數(shù)據(jù)類型内狗。這里我們創(chuàng)建了兩個Binary類型和一個String類型的數(shù)據(jù)怪嫌。然后創(chuàng)建一個Payload接口切片,并將創(chuàng)建的類型的地址添加到切片中柳沙。然后創(chuàng)建一個listener將接收網(wǎng)絡(luò)連接將切片中的每種類型數(shù)據(jù)寫進(jìn)網(wǎng)絡(luò)輸入接口岩灭。

    //客戶端
    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)
    }
}

測試中你知道總共發(fā)送了多少中類型的數(shù)據(jù),因此初始化一個連接到listener赂鲤,然后對接收到的數(shù)據(jù)進(jìn)行解碼柱恤。最后比較你解碼的類型和服務(wù)器發(fā)送的類型。如果發(fā)送的數(shù)據(jù)不一致測試就失敗。

下面測試下發(fā)送最大數(shù)據(jù)負(fù)載情況:

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)
    }
}

該測試創(chuàng)建了一個bytes.Buffer薄料,包含BinaryType類型和4字節(jié)無符號整數(shù)表示1GB的數(shù)據(jù)誊役。如果發(fā)送的數(shù)據(jù)是1GB,已經(jīng)超過我們定義的最大10MB限制了,雖然4字節(jié)可以最大表示4GB的數(shù)據(jù),但是出于安全等原因一般不會發(fā)送這么大的數(shù)據(jù)包的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末感混,一起剝皮案震驚了整個濱河市婆跑,隨后出現(xiàn)的幾起案子犀忱,更是在濱河造成了極大的恐慌节槐,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚂子,死亡現(xiàn)場離奇詭異沃测,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)食茎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門蒂破,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人董瞻,你說我怎么就攤上這事寞蚌√锇停” “怎么了钠糊?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長壹哺。 經(jīng)常有香客問我抄伍,道長,這世上最難降的妖魔是什么管宵? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任截珍,我火速辦了婚禮,結(jié)果婚禮上箩朴,老公的妹妹穿的比我還像新娘岗喉。我一直安慰自己,他們只是感情好炸庞,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布钱床。 她就那樣靜靜地躺著,像睡著了一般埠居。 火紅的嫁衣襯著肌膚如雪查牌。 梳的紋絲不亂的頭發(fā)上事期,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機(jī)與錄音纸颜,去河邊找鬼兽泣。 笑死,一個胖子當(dāng)著我的面吹牛胁孙,可吹牛的內(nèi)容都是我干的唠倦。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼涮较,長吁一口氣:“原來是場噩夢啊……” “哼牵敷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起法希,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤枷餐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后苫亦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體毛肋,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年屋剑,在試婚紗的時候發(fā)現(xiàn)自己被綠了润匙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡唉匾,死狀恐怖孕讳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情巍膘,我是刑警寧澤厂财,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站峡懈,受9級特大地震影響璃饱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肪康,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一荚恶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧磷支,春花似錦谒撼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春茉帅,著一層夾襖步出監(jiān)牢的瞬間叨叙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工堪澎, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留擂错,地道東北人。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓樱蛤,卻偏偏與公主長得像钮呀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子昨凡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容