6行代碼解決golang TCP粘包

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

什麼是TCP粘包問題以及為什麼會產生TCP粘包,本文不加討論。本文使用golang的bufio.Scanner來實現自訂協議解包。

協議資料包定義

本文類比一個Log Service器,該伺服器接收用戶端傳到的資料包並顯示出來

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資料包解析到結構體中,接下來會講為什麼需要添加幾個資料無關的長度欄位。

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}

由於主機名稱、標籤這種資料是不固定長度的,所以需要兩個位元組來標識資料長度,否則讀取的時候只知道一個總的資料長度是無法區分主機名稱、標籤名、日誌資料的。

資料包的粘包問題解決

上文只是解決了編碼/解碼問題,前提是收到的資料包沒有產生粘包問題,解決粘包就是要正確分割位元組流中的資料。一般有以下做法:

  1. 定長分隔(每個資料包最大為該長度) 缺點是資料不足時會浪費傳輸資源
  2. 特定字元分隔(如rn) 缺點是如果本文中有rn就會導致問題
  3. 在資料包中添加長度欄位(本文採用的)

golang提供了bufio.Scanner來解決粘包問題。

scanner := bufio.NewScanner(reader) // reader為實現了io.Reader介面的對象,如net.Connscanner.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資料包

完整源碼

package mainimport (    "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作為一門強大的網路程式設計語言,實現自訂協議是非常重要的,實際上實現自訂協議也不是很難,以下幾個步驟:

  1. 資料包編碼
  2. 資料包解碼
  3. 處理TCP粘包問題
  4. 斷線重連(可以使用心跳實現)(非必須)

本文引用自我自己的部落格golang解決TCP粘包問題

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.