Golang通過包長(zhǎng)協(xié)議處理TCP粘包的問題解決
tcp粘包產(chǎn)生的原因這里就不說了,因?yàn)榇蠹夷芩阉鱐CP粘包的處理方法,想必大概對(duì)TCP粘包有了一定了解,所以我們直接從處理思路開始講起
tcp粘包現(xiàn)象代碼重現(xiàn)
首先,我們來重現(xiàn)一下TCP粘包,然后再此基礎(chǔ)之上解決粘包的問題,這里給出了client和server的示例代碼如下
/* 文件名:client.go client客戶端的示例代碼(未處理粘包問題) 通過無限循環(huán)無時(shí)間間隔發(fā)送數(shù)據(jù)給server服務(wù)器 server將會(huì)不間斷的出現(xiàn)TCP粘包問題 */ package main import ( "fmt" "net" ) func main() { conn, err := net.Dial("tcp", ":9000") if err != nil { return } defer conn.Close() for { s := "Hello, Server!" n, err := conn.Write([]byte(s)) if err != nil { fmt.Println("Error:", err) fmt.Println("Error N:", n) return } // 這里通過限制發(fā)送頻率和時(shí)間間隔來解決TCP粘包 // 雖然能夠?qū)崿F(xiàn),但是頻率被限制,效率也會(huì)被限制 // time.Sleep(time.Second * 1) } }
/* 文件名:server.go server服務(wù)端的示例代碼(未處理粘包問題) 服務(wù)端接收到數(shù)據(jù)后立即打印 此時(shí)將會(huì)不間斷的出現(xiàn)TCP粘包問題 */ package main import ( "fmt" "net" ) func main() { ln, err := net.Listen("tcp", ":9000") if err != nil { return } for { conn, err := ln.Accept() if err != nil { continue } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() tmp := []byte{} for { buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { fmt.Println("Read Error:", err) fmt.Println("Read N:", n) return } fmt.Println(string(buf)) } }
按順序啟動(dòng)server.go和client.go,正常情況下每行會(huì)輸出Hello, World!
字樣,出現(xiàn)TCP粘包后,將會(huì)出現(xiàn)類似Hello, World!Hello
之類的字樣,后一個(gè)包粘到前一個(gè)包了
解決TCP粘包有很多種方法,歸結(jié)起來就是通過自定義通訊協(xié)議來解決,例如分隔符協(xié)議、MQTT協(xié)議、包長(zhǎng)協(xié)議等等,而我們這里介紹的就是通過包長(zhǎng)協(xié)議來解決問題的,當(dāng)然包長(zhǎng)協(xié)議也有很多種自定義的方法
通過演示的結(jié)果,我們可以看出來,后一個(gè)包粘到了前一個(gè)包,而且后一個(gè)包不一定是一個(gè)完整的包,也很有可能第一次收到的數(shù)據(jù)包也不是完整的數(shù)據(jù)包
tcp粘包問題處理方法
這樣我們就有必要校驗(yàn)每次收到的數(shù)據(jù)包是否是我們期望收到的,比較直觀的,客戶端和服務(wù)端雙方協(xié)商某種協(xié)議,例如包長(zhǎng)協(xié)議,在客戶端發(fā)送數(shù)據(jù)時(shí),先計(jì)算一下數(shù)據(jù)的長(zhǎng)度(假設(shè)用2字節(jié)的uint16表示),然后將計(jì)算得到的長(zhǎng)度和實(shí)際的數(shù)據(jù)組裝成一個(gè)包,最后發(fā)送給服務(wù)端;而服務(wù)端接收到數(shù)據(jù)時(shí),先讀取2字節(jié)的數(shù)據(jù)長(zhǎng)度信息(可能不足2字節(jié),程序需要針對(duì)這種情況設(shè)計(jì)),然后根據(jù)數(shù)據(jù)長(zhǎng)度來讀取后邊的數(shù)據(jù)(可能會(huì)存在數(shù)據(jù)過剩、數(shù)據(jù)剛好、數(shù)據(jù)不足等情況,程序需要針對(duì)這些情況設(shè)計(jì))
有了思路之后,我們就需要對(duì)發(fā)送端和接收端的數(shù)據(jù)進(jìn)行處理了,因?yàn)榘l(fā)送端較為簡(jiǎn)單,不需要考慮其他情況,只管封裝數(shù)據(jù)包發(fā)送,所以這里我們先對(duì)發(fā)送端client進(jìn)行處理
/* 文件名:client.go 使用包長(zhǎng)協(xié)議,封裝TCP包并循環(huán)發(fā)送給server服務(wù)端 */ package main import ( "encoding/binary" "fmt" "net" ) func main() { conn, err := net.Dial("tcp", ":9000") if err != nil { return } defer conn.Close() for { s := "Hello, Server!" sbytes := make([]byte, 2+len(s)) binary.BigEndian.PutUint16(sbytes, uint16(len(s))) copy(sbytes[2:], []byte(s)) n, err := conn.Write(sbytes) if err != nil { fmt.Println("Error:", err) fmt.Println("Error N:", n) return } // time.Sleep(time.Second * 1) } }
按照我們的思路,首先使用len()
函數(shù)計(jì)算出待發(fā)送字符串的長(zhǎng)度,然后使用make()
函數(shù)創(chuàng)建一個(gè)[]byte切片作為待組裝發(fā)送的數(shù)據(jù)包緩存sbyte,長(zhǎng)度就是2字節(jié)的包頭+字符串的長(zhǎng)度
,接著通過binary.BigEndian.PutUint16()
函數(shù)來對(duì)數(shù)據(jù)包緩存sbyte進(jìn)行操作,將字符串的長(zhǎng)度信息寫入2字節(jié)的包頭中,緊接著又通過copy()
完成封包組裝,最后通過conn.Write()
將封包發(fā)送出去,這樣子發(fā)送出去的數(shù)據(jù)大概長(zhǎng)成下面的樣子
[0][14][H][e][l][l][o][,][ ][S][e][r][v][e][r][!]
其中,封包整體長(zhǎng)16bytes,Hello, Server!
則長(zhǎng)14bytes
好了,至此數(shù)據(jù)將會(huì)循環(huán)不簡(jiǎn)短的發(fā)送給服務(wù)端,接下來我們就要對(duì)服務(wù)端server.go進(jìn)行處理了,先上代碼
/* 文件名:server.go 使用包長(zhǎng)協(xié)議,處理接收到的封包數(shù)據(jù) 收到的封包數(shù)據(jù),可能存在幾種情況: 1、封包總長(zhǎng)度不足2字節(jié)(這種情況不能完整獲取包頭),緩存起來與下次獲取的數(shù)據(jù)拼接 2、封包總長(zhǎng)度剛好2字節(jié),數(shù)據(jù)長(zhǎng)度信息讀出來是0,這種情況可以正常處理并清空緩存 3、封包總長(zhǎng)度大于2字節(jié),數(shù)據(jù)長(zhǎng)度信息大于封包數(shù)據(jù)實(shí)際長(zhǎng)度,表示數(shù)據(jù)包不完整,需要等到下一次讀取再拼接起來 4、封包總長(zhǎng)度大于2字節(jié),數(shù)據(jù)長(zhǎng)度信息等于封包數(shù)據(jù)實(shí)際長(zhǎng)度,這種情況(理想情況)可以正常處理并清空緩存 5、封包總長(zhǎng)度大于2字節(jié),數(shù)據(jù)長(zhǎng)度信息小于封包實(shí)際長(zhǎng)度,表示數(shù)據(jù)包發(fā)生TCP粘包了,讀取實(shí)際數(shù)據(jù)后,將剩余部分緩存起來等待下次拼接 PS:這里只總結(jié)出了這幾種情況,其他未發(fā)現(xiàn)的情況還需另外處理 */ package main import ( "encoding/binary" "fmt" "net" ) func main() { ln, err := net.Listen("tcp", ":9000") if err != nil { return } for { conn, err := ln.Accept() if err != nil { continue } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() tmp := []byte{} for { buf := make([]byte, 1024) // fmt.Println("len:", len(buf), " cap:", cap(buf)) n, err := conn.Read(buf) if err != nil { if e, ok := err.(*net.OpError); ok { fmt.Println(e.Source, e.Addr, e.Net, e.Op, e.Err) if e.Timeout() { fmt.Println("Timeout Error") } } fmt.Println("Read Error:", err) fmt.Println("Read N:", n) return } if n == 0 { fmt.Println("Read N:", n) return } tmp = append(tmp, buf[:n]...) length := len(tmp) if length < 2 { continue } if length >= 2 { head := make([]byte, 2) copy(head, tmp[:2]) dataLength := binary.BigEndian.Uint16(head) data := make([]byte, dataLength) copy(data, tmp[2:dataLength+2]) fmt.Println(string(data)) // 得到數(shù)據(jù) if uint16(length) == 2+dataLength { tmp = []byte{} } else if uint16(length) > 2+dataLength { tmp = tmp[dataLength+2:] } } // fmt.Println(string(buf)) } }
ps:這里的示例代碼不能直接用于生產(chǎn)環(huán)境,只是提供tcp粘包處理的思路過程,代碼還是存在一些問題的,例如server.go服務(wù)端還沒有對(duì)第3種情況進(jìn)行處理,封包總長(zhǎng)度大于2字節(jié),數(shù)據(jù)長(zhǎng)度信息大于封包數(shù)據(jù)實(shí)際長(zhǎng)度,表示數(shù)據(jù)包不完整,需要等到下一次讀取再拼接起來
到此這篇關(guān)于Golang通過包長(zhǎng)協(xié)議處理TCP粘包的問題解決的文章就介紹到這了,更多相關(guān)Golang TCP粘包內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- golang之tcp自動(dòng)重連實(shí)現(xiàn)方法
- 利用Golang實(shí)現(xiàn)TCP連接的雙向拷貝詳解
- golang 實(shí)現(xiàn)tcp轉(zhuǎn)發(fā)代理的方法
- Golang 實(shí)現(xiàn)Socket服務(wù)端和客戶端使用TCP協(xié)議通訊
- golang中net的tcp服務(wù)使用
- Golang?編寫Tcp服務(wù)器的解決方案
- golang實(shí)現(xiàn)簡(jiǎn)單的tcp數(shù)據(jù)傳輸
- Golang實(shí)現(xiàn)自己的Redis(TCP篇)實(shí)例探究
- Golang TCP網(wǎng)絡(luò)編程的具體實(shí)現(xiàn)
相關(guān)文章
GO中高效的將int轉(zhuǎn)換為string的方法與源碼
本文將從逐步介紹幾種在?Go?中將?int?轉(zhuǎn)換為?string?的常見方法,并重點(diǎn)剖析這幾種方法在性能上的特點(diǎn),另外,還會(huì)重點(diǎn)介紹?FormatInt?高效的算法實(shí)現(xiàn),需要的朋友可以參考下2024-01-01go 代碼的調(diào)試---打印調(diào)用堆棧的實(shí)例
下面小編就為大家?guī)硪黄猤o 代碼的調(diào)試---打印調(diào)用堆棧的實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10Go語言基礎(chǔ)結(jié)構(gòu)體用法及示例詳解
這篇文章主要為大家介紹了Go語言基礎(chǔ)結(jié)構(gòu)體的用法及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11