前言
什么是TCP粘包問(wèn)題以及為什么會(huì)產(chǎn)生TCP粘包,本文不加討論。本文使用golang的bufio.Scanner
來(lái)實(shí)現(xiàn)自定義協(xié)議解包。
下面話不多說(shuō)了,來(lái)一起看看詳細(xì)的介紹吧。
協(xié)議數(shù)據(jù)包定義
本文模擬一個(gè)日志服務(wù)器,該服務(wù)器接收客戶端傳到的數(shù)據(jù)包并顯示出來(lái)
type Package struct {
Version [2]byte // 協(xié)議版本,暫定V1
Length int16 // 數(shù)據(jù)部分長(zhǎng)度
Timestamp int64 // 時(shí)間戳
HostnameLength int16 // 主機(jī)名長(zhǎng)度
Hostname []byte // 主機(jī)名
TagLength int16 // 標(biāo)簽長(zhǎng)度
Tag []byte // 標(biāo)簽
Msg []byte // 日志數(shù)據(jù)
}
協(xié)議定義部分沒(méi)有什么好講的,根據(jù)具體的業(yè)務(wù)邏輯定義即可。
數(shù)據(jù)打包
由于TCP協(xié)議是語(yǔ)言無(wú)關(guān)的協(xié)議,所以直接把協(xié)議數(shù)據(jù)包結(jié)構(gòu)體發(fā)送到TCP連接中也是不可能的,只能發(fā)送字節(jié)流數(shù)據(jù),所以需要自己實(shí)現(xiàn)數(shù)據(jù)編碼。所幸golang提供了binary來(lái)幫助我們實(shí)現(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,有利于接口擴(kuò)展,只要實(shí)現(xiàn)了該接口即可編碼數(shù)據(jù)寫(xiě)入。binary.BigEndian是字節(jié)序,本文暫時(shí)不討論,有需要的讀者可以自行查找資料研究。
數(shù)據(jù)解包
解包需要將TCP數(shù)據(jù)包解析到結(jié)構(gòu)體中,接下來(lái)會(huì)講為什么需要添加幾個(gè)數(shù)據(jù)無(wú)關(guān)的長(zhǎng)度字段。
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
}
由于主機(jī)名、標(biāo)簽這種數(shù)據(jù)是不固定長(zhǎng)度的,所以需要兩個(gè)字節(jié)來(lái)標(biāo)識(shí)數(shù)據(jù)長(zhǎng)度,否則讀取的時(shí)候只知道一個(gè)總的數(shù)據(jù)長(zhǎng)度是無(wú)法區(qū)分主機(jī)名、標(biāo)簽名、日志數(shù)據(jù)的。
數(shù)據(jù)包的粘包問(wèn)題解決
上文只是解決了編碼/解碼問(wèn)題,前提是收到的數(shù)據(jù)包沒(méi)有產(chǎn)生粘包問(wèn)題,解決粘包就是要正確分割字節(jié)流中的數(shù)據(jù)。一般有以下做法:
- 定長(zhǎng)分隔(每個(gè)數(shù)據(jù)包最大為該長(zhǎng)度) 缺點(diǎn)是數(shù)據(jù)不足時(shí)會(huì)浪費(fèi)傳輸資源
- 特定字符分隔(如rn) 缺點(diǎn)是如果正文中有rn就會(huì)導(dǎo)致問(wèn)題
- 在數(shù)據(jù)包中添加長(zhǎng)度字段(本文采用的)
golang提供了bufio.Scanner
來(lái)解決粘包問(wèn)題。
scanner := bufio.NewScanner(reader) // reader為實(shí)現(xiàn)了io.Reader接口的對(duì)象,如net.Conn
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if !atEOF data[0] == 'V' { // 由于我們定義的數(shù)據(jù)包頭最開(kāi)始為兩個(gè)字節(jié)的版本號(hào),所以只有以V開(kāi)頭的數(shù)據(jù)包才處理
if len(data) > 4 { // 如果收到的數(shù)據(jù)>4個(gè)字節(jié)(2字節(jié)版本號(hào)+2字節(jié)數(shù)據(jù)包長(zhǎng)度)
length := int16(0)
binary.Read(bytes.NewReader(data[2:4]), binary.BigEndian, length) // 讀取數(shù)據(jù)包第3-4字節(jié)(int16)=>數(shù)據(jù)部分長(zhǎng)度
if int(length)+4 = len(data) { // 如果讀取到的數(shù)據(jù)正文長(zhǎng)度+2字節(jié)版本號(hào)+2字節(jié)數(shù)據(jù)長(zhǎng)度不超過(guò)讀到的數(shù)據(jù)(實(shí)際上就是成功完整的解析出了一個(gè)包)
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
方法,該方法用來(lái)解析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ù)部分長(zhǎng)度
Timestamp int64 // 時(shí)間戳
HostnameLength int16 // 主機(jī)名長(zhǎng)度
Hostname []byte // 主機(jī)名
TagLength int16 // Tag長(zhǎng)度
Tag []byte // Tag
Msg []byte // 數(shù)據(jù)部分長(zhǎng)度
}
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)在時(shí)間是:" + 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)
// 寫(xiě)入四次,模擬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("無(wú)效數(shù)據(jù)包")
}
}
寫(xiě)在最后
golang作為一門強(qiáng)大的網(wǎng)絡(luò)編程語(yǔ)言,實(shí)現(xiàn)自定義協(xié)議是非常重要的,實(shí)際上實(shí)現(xiàn)自定義協(xié)議也不是很難,以下幾個(gè)步驟:
- 數(shù)據(jù)包編碼
- 數(shù)據(jù)包解碼
- 處理TCP粘包問(wèn)題
- 斷線重連(可以使用心跳實(shí)現(xiàn))(非必須)
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
您可能感興趣的文章:- python TCP Socket的粘包和分包的處理詳解
- C#中TCP粘包問(wèn)題的解決方法
- 使用Netty搭建服務(wù)端和客戶端過(guò)程詳解
- spring+netty服務(wù)器搭建的方法
- Spring Boot實(shí)戰(zhàn)之netty-socketio實(shí)現(xiàn)簡(jiǎn)單聊天室(給指定用戶推送消息)
- 使用Netty解決TCP粘包和拆包問(wèn)題過(guò)程詳解