前面一篇文章介紹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ù)包的。