<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
tcp粘包產生的原因這裡就不說了,因為大家能搜尋TCP粘包的處理方法,想必大概對TCP粘包有了一定了解,所以我們直接從處理思路開始講起
首先,我們來重現一下TCP粘包,然後再此基礎之上解決粘包的問題,這裡給出了client和server的範例程式碼如下
/* 檔名:client.go client使用者端的範例程式碼(未處理粘包問題) 通過無限迴圈無時間間隔傳送資料給server伺服器 server將會不間斷的出現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 } // 這裡通過限制傳送頻率和時間間隔來解決TCP粘包 // 雖然能夠實現,但是頻率被限制,效率也會被限制 // time.Sleep(time.Second * 1) } }
/* 檔名:server.go server伺服器端的範例程式碼(未處理粘包問題) 伺服器端接收到資料後立即列印 此時將會不間斷的出現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)) } }
按順序啟動server.go和client.go,正常情況下每行會輸出Hello, World!
字樣,出現TCP粘包後,將會出現類似Hello, World!Hello
之類的字樣,後一個包粘到前一個包了
解決TCP粘包有很多種方法,歸結起來就是通過自定義通訊協定來解決,例如分隔符協定、MQTT協定、包長協定等等,而我們這裡介紹的就是通過包長協定來解決問題的,當然包長協定也有很多種自定義的方法
通過演示的結果,我們可以看出來,後一個包粘到了前一個包,而且後一個包不一定是一個完整的包,也很有可能第一次收到的封包也不是完整的封包
這樣我們就有必要校驗每次收到的封包是否是我們期望收到的,比較直觀的,使用者端和伺服器端雙方協商某種協定,例如包長協定,在使用者端傳送資料時,先計算一下資料的長度(假設用2位元組的uint16表示),然後將計算得到的長度和實際的資料組裝成一個包,最後傳送給伺服器端;而伺服器端接收到資料時,先讀取2位元組的資料長度資訊(可能不足2位元組,程式需要針對這種情況設計),然後根據資料長度來讀取後邊的資料(可能會存在資料過剩、資料剛好、資料不足等情況,程式需要針對這些情況設計)
有了思路之後,我們就需要對傳送端和接收端的資料進行處理了,因為傳送端較為簡單,不需要考慮其他情況,只管封裝封包傳送,所以這裡我們先對傳送端client進行處理
/* 檔名:client.go 使用包長協定,封裝TCP包並回圈傳送給server伺服器端 */ 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()
函數計算出待傳送字串的長度,然後使用make()
函數建立一個[]byte切片作為待組裝傳送的封包快取sbyte,長度就是2位元組的包頭+字串的長度
,接著通過binary.BigEndian.PutUint16()
函數來對封包快取sbyte進行操作,將字串的長度資訊寫入2位元組的包頭中,緊接著又通過copy()
完成封包組裝,最後通過conn.Write()
將封包傳送出去,這樣子傳送出去的資料大概長成下面的樣子
[0][14][H][e][l][l][o][,][ ][S][e][r][v][e][r][!]
其中,封包整體長16bytes,Hello, Server!
則長14bytes
好了,至此資料將會迴圈不簡短的傳送給伺服器端,接下來我們就要對伺服器端server.go進行處理了,先上程式碼
/* 檔名:server.go 使用包長協定,處理接收到的封包資料 收到的封包資料,可能存在幾種情況: 1、封包總長度不足2位元組(這種情況不能完整獲取包頭),快取起來與下次獲取的資料拼接 2、封包總長度剛好2位元組,資料長度資訊讀出來是0,這種情況可以正常處理並清空快取 3、封包總長度大於2位元組,資料長度資訊大於封包資料實際長度,表示封包不完整,需要等到下一次讀取再拼接起來 4、封包總長度大於2位元組,資料長度資訊等於封包資料實際長度,這種情況(理想情況)可以正常處理並清空快取 5、封包總長度大於2位元組,資料長度資訊小於封包實際長度,表示封包發生TCP粘包了,讀取實際資料後,將剩餘部分快取起來等待下次拼接 PS:這裡只總結出了這幾種情況,其他未發現的情況還需另外處理 */ 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)) // 得到資料 if uint16(length) == 2+dataLength { tmp = []byte{} } else if uint16(length) > 2+dataLength { tmp = tmp[dataLength+2:] } } // fmt.Println(string(buf)) } }
ps:這裡的範例程式碼不能直接用於生產環境,只是提供tcp粘包處理的思路過程,程式碼還是存在一些問題的,例如server.go伺服器端還沒有對第3種情況進行處理,封包總長度大於2位元組,資料長度資訊大於封包資料實際長度,表示封包不完整,需要等到下一次讀取再拼接起來
到此這篇關於Golang通過包長協定處理TCP粘包的問題解決的文章就介紹到這了,更多相關Golang TCP粘包內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45