什麼是TCP粘包問題以及爲何會產生TCP粘包,本文不加討論。本文使用golang的bufio.Scanner
來實現自定義協議解包。html
本文模擬一個日誌服務器,該服務器接收客戶端傳到的數據包並顯示出來golang
type Package struct { Version [2]byte // 協議版本,暫定V1 Length int16 // 數據部分長度 Timestamp int64 // 時間戳 HostnameLength int16 // 主機名長度 Hostname []byte // 主機名 TagLength int16 // 標籤長度 Tag []byte // 標籤 Msg []byte // 日誌數據 }
協議定義部分沒有什麼好講的,根據具體的業務邏輯定義便可。編程
因爲TCP協議是語言無關的協議,因此直接把協議數據包結構體發送到TCP鏈接中也是不可能的,只能發送字節流數據,因此須要本身實現數據編碼。所幸golang提供了binary
來幫助咱們實現網絡字節編碼。服務器
func (p *Package) Pack(writer io.Writer) error { var err error err = binary.Write(writer, binary.BigEndian, &p.Version) err = binary.Write(writer, binary.BigEndian, &p.Length) err = binary.Write(writer, binary.BigEndian, &p.Timestamp) err = binary.Write(writer, binary.BigEndian, &p.HostnameLength) err = binary.Write(writer, binary.BigEndian, &p.Hostname) err = binary.Write(writer, binary.BigEndian, &p.TagLength) err = binary.Write(writer, binary.BigEndian, &p.Tag) err = binary.Write(writer, binary.BigEndian, &p.Msg) return err }
Pack方法的輸出目標爲io.Writer
,有利於接口擴展,只要實現了該接口便可編碼數據寫入。binary.BigEndian
是字節序,本文暫時不討論,有須要的讀者能夠自行查找資料研究。網絡
解包須要將TCP數據包解析到結構體中,接下來會講爲何須要添加幾個數據無關
的長度字段。tcp
func (p *Package) Unpack(reader io.Reader) error { var err error err = binary.Read(reader, binary.BigEndian, &p.Version) err = binary.Read(reader, binary.BigEndian, &p.Length) err = binary.Read(reader, binary.BigEndian, &p.Timestamp) err = binary.Read(reader, binary.BigEndian, &p.HostnameLength) p.Hostname = make([]byte, p.HostnameLength) err = binary.Read(reader, binary.BigEndian, &p.Hostname) err = binary.Read(reader, binary.BigEndian, &p.TagLength) p.Tag = make([]byte, p.TagLength) err = binary.Read(reader, binary.BigEndian, &p.Tag) p.Msg = make([]byte, p.Length-8-2-p.HostnameLength-2-p.TagLength) err = binary.Read(reader, binary.BigEndian, &p.Msg) return err }
因爲主機名、標籤這種數據是不固定長度的,因此須要兩個字節來標識數據長度,不然讀取的時候只知道一個總的數據長度是沒法區分主機名、標籤名、日誌數據的。編程語言
上文只是解決了編碼/解碼
問題,前提是收到的數據包沒有產生粘包問題,解決粘包就是要正確分割字節流中的數據。通常有如下作法:編碼
golang提供了bufio.Scanner
來解決粘包問題。日誌
scanner := bufio.NewScanner(reader) // reader爲實現了io.Reader接口的對象,如net.Conn scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { if !atEOF && data[0] == 'V' { // 因爲咱們定義的數據包頭最開始爲兩個字節的版本號,因此只有以V開頭的數據包才處理 if len(data) > 4 { // 若是收到的數據>4個字節(2字節版本號+2字節數據包長度) length := int16(0) binary.Read(bytes.NewReader(data[2:4]), binary.BigEndian, &length) // 讀取數據包第3-4字節(int16)=>數據部分長度 if int(length)+4 <= len(data) { // 若是讀取到的數據正文長度+2字節版本號+2字節數據長度不超過讀到的數據(實際上就是成功完整的解析出了一個包) return int(length) + 4, data[:int(length)+4], nil } } } return }) // 打印接收到的數據包 for scanner.Scan() { scannedPack := new(Package) scannedPack.Unpack(bytes.NewReader(scanner.Bytes())) log.Println(scannedPack) }
本文的核心就在於scanner.Split
方法,該方法用來解析TCP數據包code
package main import ( "bufio" "bytes" "encoding/binary" "fmt" "io" "log" "os" "time" ) type Package struct { Version [2]byte // 協議版本 Length int16 // 數據部分長度 Timestamp int64 // 時間戳 HostnameLength int16 // 主機名長度 Hostname []byte // 主機名 TagLength int16 // Tag長度 Tag []byte // Tag Msg []byte // 數據部分長度 } func (p *Package) Pack(writer io.Writer) error { var err error err = binary.Write(writer, binary.BigEndian, &p.Version) err = binary.Write(writer, binary.BigEndian, &p.Length) err = binary.Write(writer, binary.BigEndian, &p.Timestamp) err = binary.Write(writer, binary.BigEndian, &p.HostnameLength) err = binary.Write(writer, binary.BigEndian, &p.Hostname) err = binary.Write(writer, binary.BigEndian, &p.TagLength) err = binary.Write(writer, binary.BigEndian, &p.Tag) err = binary.Write(writer, binary.BigEndian, &p.Msg) return err } func (p *Package) Unpack(reader io.Reader) error { var err error err = binary.Read(reader, binary.BigEndian, &p.Version) err = binary.Read(reader, binary.BigEndian, &p.Length) err = binary.Read(reader, binary.BigEndian, &p.Timestamp) err = binary.Read(reader, binary.BigEndian, &p.HostnameLength) p.Hostname = make([]byte, p.HostnameLength) err = binary.Read(reader, binary.BigEndian, &p.Hostname) err = binary.Read(reader, binary.BigEndian, &p.TagLength) p.Tag = make([]byte, p.TagLength) err = binary.Read(reader, binary.BigEndian, &p.Tag) p.Msg = make([]byte, p.Length-8-2-p.HostnameLength-2-p.TagLength) err = binary.Read(reader, binary.BigEndian, &p.Msg) return err } func (p *Package) String() string { return fmt.Sprintf("version:%s length:%d timestamp:%d hostname:%s tag:%s msg:%s", p.Version, p.Length, p.Timestamp, p.Hostname, p.Tag, p.Msg, ) } func main() { hostname, err := os.Hostname() if err != nil { log.Fatal(err) } pack := &Package{ Version: [2]byte{'V', '1'}, Timestamp: time.Now().Unix(), HostnameLength: int16(len(hostname)), Hostname: []byte(hostname), TagLength: 4, Tag: []byte("demo"), Msg: []byte(("如今時間是:" + time.Now().Format("2006-01-02 15:04:05"))), } pack.Length = 8 + 2 + pack.HostnameLength + 2 + pack.TagLength + int16(len(pack.Msg)) buf := new(bytes.Buffer) // 寫入四次,模擬TCP粘包效果 pack.Pack(buf) pack.Pack(buf) pack.Pack(buf) pack.Pack(buf) // scanner scanner := bufio.NewScanner(buf) scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { if !atEOF && data[0] == 'V' { if len(data) > 4 { length := int16(0) binary.Read(bytes.NewReader(data[2:4]), binary.BigEndian, &length) if int(length)+4 <= len(data) { return int(length) + 4, data[:int(length)+4], nil } } } return }) for scanner.Scan() { scannedPack := new(Package) scannedPack.Unpack(bytes.NewReader(scanner.Bytes())) log.Println(scannedPack) } if err := scanner.Err(); err != nil { log.Fatal("無效數據包") } }
golang做爲一門強大的網絡編程語言,實現自定義協議是很是重要的,實際上實現自定義協議也不是很難,如下幾個步驟:
本文引用自我本身的博客golang解決TCP粘包問題