中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

6行代碼解決golang TCP粘包

2018-07-20    來源:編程學習網(wǎng)

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

什么是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   // 標簽長度
    Tag            []byte  // 標簽
    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方法的輸出目標為 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
}

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

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

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

  1. 定長分隔(每個數(shù)據(jù)包最大為該長度) 缺點是數(shù)據(jù)不足時會浪費傳輸資源
  2. 特定字符分隔(如rn) 缺點是如果正文中有rn就會導致問題
  3. 在數(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é)議也不是很難,以下幾個步驟:

  1. 數(shù)據(jù)包編碼
  2. 數(shù)據(jù)包解碼
  3. 處理TCP粘包問題
  4. 斷線重連(可以使用心跳實現(xiàn))(非必須)

本文引用自我自己的博客 golang解決TCP粘包問題

 

來自:https://segmentfault.com/a/1190000013493942

 

標簽: 服務(wù)器 網(wǎng)絡(luò)

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:日調(diào)度5萬億次,騰訊云微服務(wù)架構(gòu)體系TSF深度解讀

下一篇:每日一博 | 記一次 JVM 堆外內(nèi)存泄露 Bug 的查找