前言
什么是TCP粘包問題以及為什么會產(chǎn)生TCP粘包,本文不加討論。本文使用golang的bufio.Scanner
來實現(xiàn)自定義協(xié)議解包。
下面話不多說了,來一起看看詳細的介紹吧。
協(xié)議數(shù)據(jù)包定義
本文模擬一個日志服務(wù)器,該服務(wù)器接收客戶端傳到的數(shù)據(jù)包并顯示出來
type Package struct { Version [2]byte // 協(xié)議版本,暫定V1 Length int16 // 數(shù)據(jù)部分長度 Timestamp int64 // 時間戳 HostnameLength int16 // 主機名長度 Hostname []byte // 主機名 TagLength int16 // 標(biāo)簽長度 Tag []byte // 標(biāo)簽 Msg []byte // 日志數(shù)據(jù) }
協(xié)議定義部分沒有什么好講的,根據(jù)具體的業(yè)務(wù)邏輯定義即可。
數(shù)據(jù)打包
由于TCP協(xié)議是語言無關(guān)的協(xié)議,所以直接把協(xié)議數(shù)據(jù)包結(jié)構(gòu)體發(fā)送到TCP連接中也是不可能的,只能發(fā)送字節(jié)流數(shù)據(jù),所以需要自己實現(xiàn)數(shù)據(jù)編碼。所幸golang提供了binary來幫助我們實現(xiàn)網(wǎng)絡(luò)字節(jié)編碼。
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方法的輸出目標(biāo)為io.Writer,有利于接口擴展,只要實現(xiàn)了該接口即可編碼數(shù)據(jù)寫入。binary.BigEndian是字節(jié)序,本文暫時不討論,有需要的讀者可以自行查找資料研究。
數(shù)據(jù)解包
解包需要將TCP數(shù)據(jù)包解析到結(jié)構(gòu)體中,接下來會講為什么需要添加幾個數(shù)據(jù)無關(guān)的長度字段。
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 }
由于主機名、標(biāo)簽這種數(shù)據(jù)是不固定長度的,所以需要兩個字節(jié)來標(biāo)識數(shù)據(jù)長度,否則讀取的時候只知道一個總的數(shù)據(jù)長度是無法區(qū)分主機名、標(biāo)簽名、日志數(shù)據(jù)的。
數(shù)據(jù)包的粘包問題解決
上文只是解決了編碼/解碼問題,前提是收到的數(shù)據(jù)包沒有產(chǎn)生粘包問題,解決粘包就是要正確分割字節(jié)流中的數(shù)據(jù)。一般有以下做法:
golang提供了bufio.Scanner
來解決粘包問題。
scanner := bufio.NewScanner(reader) // reader為實現(xiàn)了io.Reader接口的對象,如net.Conn scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { if !atEOF data[0] == 'V' { // 由于我們定義的數(shù)據(jù)包頭最開始為兩個字節(jié)的版本號,所以只有以V開頭的數(shù)據(jù)包才處理 if len(data) > 4 { // 如果收到的數(shù)據(jù)>4個字節(jié)(2字節(jié)版本號+2字節(jié)數(shù)據(jù)包長度) length := int16(0) binary.Read(bytes.NewReader(data[2:4]), binary.BigEndian, length) // 讀取數(shù)據(jù)包第3-4字節(jié)(int16)=>數(shù)據(jù)部分長度 if int(length)+4 = len(data) { // 如果讀取到的數(shù)據(jù)正文長度+2字節(jié)版本號+2字節(jié)數(shù)據(jù)長度不超過讀到的數(shù)據(jù)(實際上就是成功完整的解析出了一個包) return int(length) + 4, data[:int(length)+4], nil } } } return }) // 打印接收到的數(shù)據(jù)包 for scanner.Scan() { scannedPack := new(Package) scannedPack.Unpack(bytes.NewReader(scanner.Bytes())) log.Println(scannedPack) }
本文的核心就在于scanner.Split
方法,該方法用來解析TCP數(shù)據(jù)包
完整源碼
package main import ( "bufio" "bytes" "encoding/binary" "fmt" "io" "log" "os" "time" ) type Package struct { Version [2]byte // 協(xié)議版本 Length int16 // 數(shù)據(jù)部分長度 Timestamp int64 // 時間戳 HostnameLength int16 // 主機名長度 Hostname []byte // 主機名 TagLength int16 // Tag長度 Tag []byte // Tag Msg []byte // 數(shù)據(jù)部分長度 } 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(("現(xiàn)在時間是:" + 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("無效數(shù)據(jù)包") } }
寫在最后
golang作為一門強大的網(wǎng)絡(luò)編程語言,實現(xiàn)自定義協(xié)議是非常重要的,實際上實現(xiàn)自定義協(xié)議也不是很難,以下幾個步驟:
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
標(biāo)簽:瀘州 滄州 東營 昭通 泰安 阿壩 駐馬店 晉中
巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《6行代碼快速解決golang TCP粘包問題》,本文關(guān)鍵詞 6行,代碼,快速,解決,golang,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。