6行代碼解決golang TCP粘包
什么是TCP粘包問題以及為什么會產生TCP粘包,本文不加討論。本文使用golang的 bufio.Scanner 來實現自定義協議解包。
協議數據包定義
本文模擬一個日志服務器,該服務器接收客戶端傳到的數據包并顯示出來
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
}
由于主機名、標簽這種數據是不固定長度的,所以需要兩個字節來標識數據長度,否則讀取的時候只知道一個總的數據長度是無法區分主機名、標簽名、日志數據的。
數據包的粘包問題解決
上文只是解決了 編碼/解碼 問題,前提是收到的數據包沒有產生粘包問題,解決粘包就是要正確分割字節流中的數據。一般有以下做法:
- 定長分隔(每個數據包最大為該長度) 缺點是數據不足時會浪費傳輸資源
- 特定字符分隔(如rn) 缺點是如果正文中有rn就會導致問題
- 在數據包中添加長度字段(本文采用的)
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數據包
完整源碼
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作為一門強大的網絡編程語言,實現自定義協議是非常重要的,實際上實現自定義協議也不是很難,以下幾個步驟:
- 數據包編碼
- 數據包解碼
- 處理TCP粘包問題
- 斷線重連(可以使用心跳實現)(非必須)
本文引用自我自己的博客 golang解決TCP粘包問題
來自:https://segmentfault.com/a/1190000013493942
本文由用戶 klyq0140 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!