網站首頁 編程語言 正文
tcp粘包產生的原因這里就不說了,因為大家能搜索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協議、包長協議等等,而我們這里介紹的就是通過包長協議來解決問題的,當然包長協議也有很多種自定義的方法
通過演示的結果,我們可以看出來,后一個包粘到了前一個包,而且后一個包不一定是一個完整的包,也很有可能第一次收到的數據包也不是完整的數據包
tcp粘包問題處理方法
這樣我們就有必要校驗每次收到的數據包是否是我們期望收到的,比較直觀的,客戶端和服務端雙方協商某種協議,例如包長協議,在客戶端發送數據時,先計算一下數據的長度(假設用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字節,數據長度信息大于封包數據實際長度,表示數據包不完整,需要等到下一次讀取再拼接起來
原文鏈接:https://blog.csdn.net/wudics/article/details/125324371
相關推薦
- 2022-10-31 DatePicker日期滾動選擇使用詳解_Android
- 2022-08-30 C#中Linq的去重方式Distinct詳解_C#教程
- 2024-03-10 【Redis】Redis的持久化(備份)
- 2023-12-20 SpringCloud服務無法注冊到Nacos的踩坑記錄
- 2022-05-04 詳解python的異常捕獲_python
- 2023-02-02 C#實現網絡小程序的步驟詳解_C#教程
- 2022-06-01 Android利用MediaRecorder實現錄音功能_Android
- 2022-10-29 .Net?Core?配置文件讀取IOptions,IOptionsMonitor,IOptionsS
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支