golang模擬TCP粘包和拆包
1. 什么是 TCP 粘包與拆包
1.粘包(Sticky Packet)
粘包是指在發(fā)送多個(gè)小的數(shù)據(jù)包時(shí),接收端會(huì)將這些數(shù)據(jù)包合并成一個(gè)數(shù)據(jù)包接收。由于 TCP 是面向流的協(xié)議,它并不會(huì)在每次數(shù)據(jù)發(fā)送時(shí)附加邊界信息。所以當(dāng)多個(gè)數(shù)據(jù)包按順序發(fā)送時(shí),接收端可能會(huì)一次性接收多個(gè)數(shù)據(jù)包的數(shù)據(jù),造成數(shù)據(jù)被粘在一起。
粘包一般發(fā)生在發(fā)送端每次寫(xiě)入的數(shù)據(jù) < 接收端套接字(Socket)緩沖區(qū)的大小。
假設(shè)發(fā)送端發(fā)送了兩個(gè)消息:消息1:“Hello”,消息2:“World”;由于 TCP 是流協(xié)議,接收端可能會(huì)接收到如下數(shù)據(jù):“HelloWorld”。這種情況就是粘包,接收端就無(wú)法準(zhǔn)確區(qū)分這兩個(gè)消息。
2.拆包(Packet Fragmentation)
拆包是指發(fā)送的數(shù)據(jù)包在傳輸過(guò)程中被分割成多個(gè)小包。盡管發(fā)送端可能發(fā)送了一個(gè)完整的消息,但由于 TCP 協(xié)議在網(wǎng)絡(luò)傳輸時(shí)可能會(huì)對(duì)數(shù)據(jù)進(jìn)行分段,接收端可能接收到的是多個(gè)小數(shù)據(jù)包。
拆包一般發(fā)生在發(fā)送端每次寫(xiě)入的數(shù)據(jù) > 接收端套接字(Socket)緩沖區(qū)的大小。
假設(shè)發(fā)送端發(fā)送了一個(gè)大的消息:“Hello, this is a long message.”;但是在傳輸過(guò)程中,網(wǎng)絡(luò)層可能會(huì)將該消息拆分成多個(gè)小包,接收端可能先收到一部分?jǐn)?shù)據(jù):“Hello, this”,然后再收到另外一部分:“is a long message.”;這樣接收端就會(huì)得到多個(gè)數(shù)據(jù)包,且它們并不代表單一的邏輯消息。
2. go 模擬TCP粘包
server.go(接收端)
package main import ( "bufio" "fmt" "io" "net" ) func handleConnection(conn net.Conn) { defer conn.Close() // 創(chuàng)建緩沖讀取器,讀取客戶端數(shù)據(jù) reader := bufio.NewReader(conn) var buffer [1024]byte for { // 持續(xù)讀取數(shù)據(jù) n, err := reader.Read(buffer[:]) if err == io.EOF { break } if err != nil { fmt.Println("Error reading data:", err) break } recvStr := string(buffer[:n]) // 打印接收到的數(shù)據(jù) fmt.Println("Received:", recvStr) } } func main() { // 啟動(dòng)服務(wù)器,監(jiān)聽(tīng) 8080 端口 ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println("Error starting server:", err) return } defer ln.Close() fmt.Println("Server started on port 8080...") for { // 等待客戶端連接 conn, err := ln.Accept() if err != nil { fmt.Println("Error accepting connection:", err) continue } // 處理連接 go handleConnection(conn) } }
client.go(發(fā)送端)
package main import ( "fmt" "net" "time" ) func main() { // 連接到服務(wù)器 conn, err := net.Dial("tcp", "localhost:8080") if err != nil { fmt.Println("Error connecting to server:", err) return } defer conn.Close() // 模擬粘包和拆包 for i := 0; i < 100; i++ { // 發(fā)送粘包情況:多個(gè)小消息一次發(fā)送 message := fmt.Sprintf("Message %d\n", i+1) conn.Write([]byte(message)) } // 等待服務(wù)器輸出接收到的消息 time.Sleep(2 * time.Second) }
執(zhí)行結(jié)果分析
可以看到接收端收到的消息并非都是一條,說(shuō)明發(fā)生了粘包
3. go模擬TCP拆包
server.go(接收端)
package main import ( "bufio" "fmt" "io" "net" ) func handleConnection(conn net.Conn) { defer conn.Close() // 創(chuàng)建緩沖讀取器,讀取客戶端數(shù)據(jù) reader := bufio.NewReader(conn) var buffer [18]byte for { // 持續(xù)讀取數(shù)據(jù) n, err := reader.Read(buffer[:]) if err == io.EOF { break } if err != nil { fmt.Println("Error reading data:", err) break } recvStr := string(buffer[:n]) // 打印接收到的數(shù)據(jù) fmt.Println("Received message :", recvStr) } } func main() { // 啟動(dòng)服務(wù)器,監(jiān)聽(tīng) 8080 端口 ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println("Error starting server:", err) return } defer ln.Close() fmt.Println("Server started on port 8080...") for { // 等待客戶端連接 conn, err := ln.Accept() if err != nil { fmt.Println("Error accepting connection:", err) continue } // 處理連接 go handleConnection(conn) } }
client.go(發(fā)送端)
package main import ( "fmt" "net" "strings" "time" ) func main() { // 連接到服務(wù)器 conn, err := net.Dial("tcp", "localhost:8080") if err != nil { fmt.Println("Error connecting to server:", err) return } defer conn.Close() // 構(gòu)造一個(gè)超過(guò)默認(rèn) MTU 的大數(shù)據(jù)包(32 字節(jié)) message := strings.Repeat("A", 32) // 模擬發(fā)送大量數(shù)據(jù) for i := 0; i < 100; i++ { fmt.Printf("Sending message : %s\n", message) conn.Write([]byte(message)) } // 等待服務(wù)器輸出 time.Sleep(2 * time.Second) }
執(zhí)行結(jié)果分析
可以看到接收端對(duì)接收到的數(shù)據(jù)進(jìn)行了拆分,說(shuō)明發(fā)生了拆包
4. 如何解決 TCP 粘包與拆包問(wèn)題
4.1 自定義協(xié)議
發(fā)送端將請(qǐng)求的數(shù)據(jù)封裝為兩部分:消息頭(發(fā)送數(shù)據(jù)大?。?消息體(發(fā)送具體數(shù)據(jù));接收端根據(jù)消息頭的值讀取相應(yīng)長(zhǎng)度的消息體數(shù)據(jù)
server.go(接收端)
服務(wù)端接收到數(shù)據(jù)時(shí),首先讀取前4個(gè)字節(jié)來(lái)獲取消息的長(zhǎng)度,然后再根據(jù)該長(zhǎng)度讀取完整的消息體
package main import ( "encoding/binary" "fmt" "io" "log" "net" ) // readMessage 函數(shù)根據(jù)長(zhǎng)度字段讀取消息 func readMessage(conn net.Conn) (string, error) { // 讀取4個(gè)字節(jié)的長(zhǎng)度字段 lenBytes := make([]byte, 4) _, err := io.ReadFull(conn, lenBytes) if err != nil { return "", fmt.Errorf("failed to read length field: %v", err) } // 解析消息長(zhǎng)度 msgLength := binary.BigEndian.Uint32(lenBytes) // 讀取消息體 msgBytes := make([]byte, msgLength) _, err = io.ReadFull(conn, msgBytes) if err != nil { return "", fmt.Errorf("failed to read message body: %v", err) } return string(msgBytes), nil } func handleConnection(conn net.Conn) { defer conn.Close() // 一直循環(huán)接收客戶端發(fā)來(lái)的消息 for { msg, err := readMessage(conn) if err != nil { log.Printf("Error reading message: %v", err) break } fmt.Println("Received message:", msg) } } func main() { // 啟動(dòng)監(jiān)聽(tīng)服務(wù) listener, err := net.Listen("tcp", ":8080") if err != nil { log.Fatalf("Error starting server: %v", err) } defer listener.Close() fmt.Println("Server is listening on port 8080...") // 接受客戶端連接并處理 for { conn, err := listener.Accept() if err != nil { log.Printf("Error accepting connection: %v", err) continue } // 啟動(dòng)新的 Goroutine 處理客戶端請(qǐng)求 go handleConnection(conn) } }
client.go(發(fā)送端)
客戶端將連接到服務(wù)端,并發(fā)送多個(gè)消息。每個(gè)消息的前4字節(jié)表示消息的長(zhǎng)度,隨后是消息體
package main import ( "bytes" "encoding/binary" "log" "net" ) // sendMessage 函數(shù)將消息和長(zhǎng)度一起發(fā)送給服務(wù)端 func sendMessage(conn net.Conn, msg string) { // 計(jì)算消息的長(zhǎng)度 msgLen := uint32(len(msg)) buf := new(bytes.Buffer) // 將消息長(zhǎng)度轉(zhuǎn)換為4字節(jié)的二進(jìn)制數(shù)據(jù) binary.Write(buf, binary.BigEndian, msgLen) // 將消息體內(nèi)容添加到緩沖區(qū) buf.Write([]byte(msg)) // 發(fā)送緩沖區(qū)數(shù)據(jù)到服務(wù)端 conn.Write(buf.Bytes()) } func main() { // 連接到服務(wù)端 conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { log.Fatalf("Error connecting to server: %v", err) } defer conn.Close() // 發(fā)送多個(gè)消息 sendMessage(conn, "Hello, Server!") sendMessage(conn, "This is a second message.") sendMessage(conn, "Goodbye!") }
4.2 固定長(zhǎng)度數(shù)據(jù)包
每個(gè)消息的長(zhǎng)度是固定的(例如 1024 字節(jié))。如果客戶端發(fā)送的數(shù)據(jù)長(zhǎng)度不足指定長(zhǎng)度,則會(huì)使用空格填充,確保每個(gè)數(shù)據(jù)包的大小一致
server.go(接收端)
服務(wù)端接收到的數(shù)據(jù)是固定長(zhǎng)度的。每次接收 1024 字節(jié)的數(shù)據(jù),并將其打印出來(lái)。如果數(shù)據(jù)不足 1024 字節(jié),服務(wù)端會(huì)讀取并處理這些數(shù)據(jù)。
package main import ( "fmt" "io" "log" "net" "strings" ) // handleConnection 函數(shù)處理每個(gè)客戶端的連接 func handleConnection(conn net.Conn) { defer conn.Close() // 設(shè)定每個(gè)消息的固定長(zhǎng)度 const messageLength = 1024 buf := make([]byte, messageLength) for { // 每次讀取固定長(zhǎng)度的消息 _, err := io.ReadFull(conn, buf) if err != nil { if err.Error() == "EOF" { // 客戶端關(guān)閉連接 break } log.Printf("Error reading message: %v", err) break } // 將讀取的字節(jié)轉(zhuǎn)換為字符串并打印 msg := string(buf) // 去除空格填充 fmt.Println("Received message:", strings.TrimSpace(msg)) } } func main() { // 啟動(dòng) TCP 監(jiān)聽(tīng) listener, err := net.Listen("tcp", ":8080") if err != nil { log.Fatalf("Error starting server: %v", err) } defer listener.Close() fmt.Println("Server is listening on port 8080...") // 等待客戶端連接 for { conn, err := listener.Accept() if err != nil { log.Printf("Error accepting connection: %v", err) continue } // 啟動(dòng)新的 Goroutine 處理每個(gè)客戶端的連接 go handleConnection(conn) } }
client.go(發(fā)送端)
客戶端會(huì)向服務(wù)器發(fā)送固定長(zhǎng)度的消息,如果消息長(zhǎng)度不足 1024 字節(jié),則會(huì)填充空格
package main import ( "log" "net" "strings" ) // sendFixedLengthMessage 函數(shù)向服務(wù)端發(fā)送固定長(zhǎng)度的消息 func sendFixedLengthMessage(conn net.Conn, msg string) { // 確保消息長(zhǎng)度為 1024 字節(jié),不足部分用空格填充 if len(msg) < 1024 { msg = msg + strings.Repeat(" ", 1024-len(msg)) } // 發(fā)送消息到服務(wù)端 _, err := conn.Write([]byte(msg)) if err != nil { log.Fatalf("Error sending message: %v", err) } } func main() { // 連接到服務(wù)端 conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { log.Fatalf("Error connecting to server: %v", err) } defer conn.Close() // 發(fā)送固定長(zhǎng)度的消息 sendFixedLengthMessage(conn, "Hello, Server!") sendFixedLengthMessage(conn, "This is a second message.") sendFixedLengthMessage(conn, "Goodbye!") }
4.3 特殊字符來(lái)標(biāo)識(shí)消息邊界
通過(guò)在發(fā)送端每條消息的末尾加上 \n,然后接收端使用 ReadLine() 方法按行讀取數(shù)據(jù)來(lái)區(qū)分每個(gè)數(shù)據(jù)包的邊界
server.go(接收端)
服務(wù)端會(huì)監(jiān)聽(tīng)端口,并按行讀取客戶端發(fā)送的消息。每個(gè)消息的末尾會(huì)有一個(gè) \n 來(lái)標(biāo)識(shí)消息的結(jié)束
package main import ( "bufio" "fmt" "log" "net" "strings" ) func handleConnection(conn net.Conn) { defer conn.Close() // 創(chuàng)建一個(gè)帶緩沖的讀取器 reader := bufio.NewReader(conn) for { // 讀取客戶端發(fā)送的一行數(shù)據(jù),直到遇到 '\n' 為止 line, err := reader.ReadString('\n') if err != nil { log.Printf("Error reading from client: %v", err) break } // 去掉結(jié)尾的換行符 line = strings.TrimSpace(line) fmt.Printf("Received message: %s\n", line) } } func main() { // 啟動(dòng) TCP 監(jiān)聽(tīng) listener, err := net.Listen("tcp", ":8080") if err != nil { log.Fatalf("Error starting server: %v", err) } defer listener.Close() fmt.Println("Server is listening on port 8080...") // 等待客戶端連接 for { conn, err := listener.Accept() if err != nil { log.Printf("Error accepting connection: %v", err) continue } // 啟動(dòng)新的 Goroutine 處理每個(gè)客戶端的連接 go handleConnection(conn) } }
client.go(發(fā)送端)
客戶端向服務(wù)端發(fā)送消息,每條消息末尾都會(huì)加上一個(gè) \n,然后發(fā)送到服務(wù)器
package main import ( "log" "net" ) func sendMessage(conn net.Conn, message string) { // 將消息添加換行符并發(fā)送 message = message + "\n" _, err := conn.Write([]byte(message)) if err != nil { log.Fatalf("Error sending message: %v", err) } } func main() { // 連接到服務(wù)端 conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { log.Fatalf("Error connecting to server: %v", err) } defer conn.Close() // 發(fā)送幾條消息 sendMessage(conn, "Hello, Server!") sendMessage(conn, "How are you?") sendMessage(conn, "Goodbye!") }
5. 三種方式的優(yōu)缺點(diǎn)對(duì)比
特性 | 固定長(zhǎng)度方式 | 特殊字符分隔方式 | 自定義協(xié)議方式 |
---|---|---|---|
實(shí)現(xiàn)簡(jiǎn)單 | 高 | 中 | 低 |
帶寬效率 | 低(需要填充) | 高(僅傳輸有效數(shù)據(jù)) | 高(僅傳輸有效數(shù)據(jù),且靈活處理) |
靈活性 | 低 | 中 | 高 |
易于調(diào)試 | 高(每包大小固定) | 中(需解析換行符等) | 低(需要解析協(xié)議頭和體) |
性能開(kāi)銷(xiāo) | 低 | 低 | 中等(需要額外解析消息頭) |
適用場(chǎng)景 | 長(zhǎng)度固定的消息 | 消息大小可變但有清晰的分隔符 | 復(fù)雜協(xié)議、支持多類(lèi)型消息的場(chǎng)景 |
以上就是golang模擬TCP粘包和拆包的詳細(xì)內(nèi)容,更多關(guān)于go TCP粘包和拆包的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用go進(jìn)行云存儲(chǔ)上傳實(shí)現(xiàn)實(shí)例
這篇文章主要為大家介紹了使用go進(jìn)行云存儲(chǔ)上傳實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2024-01-01Golang實(shí)現(xiàn)內(nèi)網(wǎng)穿透詳解
這篇文章主要為大家詳細(xì)介紹了Golang實(shí)現(xiàn)內(nèi)網(wǎng)穿透的相關(guān)知識(shí),包括原理和代碼實(shí)現(xiàn),文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下2024-11-11淺談Go語(yǔ)言多態(tài)的實(shí)現(xiàn)與interface使用
如果大家系統(tǒng)的學(xué)過(guò)C++、Java等語(yǔ)言以及面向?qū)ο蟮脑?,相信?yīng)該對(duì)多態(tài)不會(huì)陌生。多態(tài)是面向?qū)ο蠓懂牣?dāng)中經(jīng)常使用并且非常好用的一個(gè)功能,它主要是用在強(qiáng)類(lèi)型語(yǔ)言當(dāng)中,像是Python這樣的弱類(lèi)型語(yǔ)言,變量的類(lèi)型可以隨意變化,也沒(méi)有任何限制,其實(shí)區(qū)別不是很大2021-06-06Go語(yǔ)言通過(guò)chan進(jìn)行數(shù)據(jù)傳遞的方法詳解
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言如何通過(guò)chan進(jìn)行數(shù)據(jù)傳遞的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-06-06Go語(yǔ)言基礎(chǔ)之網(wǎng)絡(luò)編程全面教程示例
這篇文章主要為大家介紹了Go語(yǔ)言基礎(chǔ)之網(wǎng)絡(luò)編程全面教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12Go函數(shù)使用(函數(shù)定義、函數(shù)聲明、函數(shù)調(diào)用等)
本文主要介紹了Go函數(shù)使用,包括函數(shù)定義、函數(shù)聲明、函數(shù)調(diào)用、可變參數(shù)函數(shù)、匿名函數(shù)、遞歸函數(shù)、高階函數(shù)等,感興趣的可以了解一下2023-11-11