日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

6行代码解决golang TCP粘包

發(fā)布時間:2025/3/20 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 6行代码解决golang TCP粘包 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

什么是TCP粘包問題以及為什么會產(chǎn)生TCP粘包,本文不加討論。本文使用golang的bufio.Scanner來實(shí)現(xiàn)自定義協(xié)議解包。

協(xié)議數(shù)據(jù)包定義

本文模擬一個日志服務(wù)器,該服務(wù)器接收客戶端傳到的數(shù)據(jù)包并顯示出來

type Package struct {Version [2]byte // 協(xié)議版本,暫定V1Length int16 // 數(shù)據(jù)部分長度Timestamp int64 // 時間戳HostnameLength int16 // 主機(jī)名長度Hostname []byte // 主機(jī)名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ù),所以需要自己實(shí)現(xiàn)數(shù)據(jù)編碼。所幸golang提供了binary來幫助我們實(shí)現(xiàn)網(wǎng)絡(luò)字節(jié)編碼。

func (p *Package) Pack(writer io.Writer) error {var err errorerr = 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,有利于接口擴(kuò)展,只要實(shí)現(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 errorerr = 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 }

由于主機(jī)名、標(biāo)簽這種數(shù)據(jù)是不固定長度的,所以需要兩個字節(jié)來標(biāo)識數(shù)據(jù)長度,否則讀取的時候只知道一個總的數(shù)據(jù)長度是無法區(qū)分主機(jī)名、標(biāo)簽名、日志數(shù)據(jù)的。

數(shù)據(jù)包的粘包問題解決

上文只是解決了編碼/解碼問題,前提是收到的數(shù)據(jù)包沒有產(chǎn)生粘包問題,解決粘包就是要正確分割字節(jié)流中的數(shù)據(jù)。一般有以下做法:

  • 定長分隔(每個數(shù)據(jù)包最大為該長度) 缺點(diǎn)是數(shù)據(jù)不足時會浪費(fèi)傳輸資源
  • 特定字符分隔(如rn) 缺點(diǎn)是如果正文中有rn就會導(dǎo)致問題
  • 在數(shù)據(jù)包中添加長度字段(本文采用的)
  • golang提供了bufio.Scanner來解決粘包問題。

    scanner := bufio.NewScanner(reader) // reader為實(shí)現(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ù)(實(shí)際上就是成功完整的解析出了一個包)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 mainimport ("bufio""bytes""encoding/binary""fmt""io""log""os""time" )type Package struct {Version [2]byte // 協(xié)議版本Length int16 // 數(shù)據(jù)部分長度Timestamp int64 // 時間戳HostnameLength int16 // 主機(jī)名長度Hostname []byte // 主機(jī)名TagLength int16 // Tag長度Tag []byte // TagMsg []byte // 數(shù)據(jù)部分長度 }func (p *Package) Pack(writer io.Writer) error {var err errorerr = 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 errorerr = 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)// scannerscanner := 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作為一門強(qiáng)大的網(wǎng)絡(luò)編程語言,實(shí)現(xiàn)自定義協(xié)議是非常重要的,實(shí)際上實(shí)現(xiàn)自定義協(xié)議也不是很難,以下幾個步驟:

  • 數(shù)據(jù)包編碼
  • 數(shù)據(jù)包解碼
  • 處理TCP粘包問題
  • 斷線重連(可以使用心跳實(shí)現(xiàn))(非必須)
  • 本文引用自我自己的博客golang解決TCP粘包問題

    總結(jié)

    以上是生活随笔為你收集整理的6行代码解决golang TCP粘包的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。