什么是粘包,拆包?
- TCP的粘包和拆包問題往往出現(xiàn)在基于TCP協(xié)議的通訊中,比如RPC框架
- 在使用TCP進(jìn)行數(shù)據(jù)傳輸時,由于TCP是基于字節(jié)流的協(xié)議,而不是基于消息的協(xié)議,可能會出現(xiàn)粘包(多個消息粘在一起)和拆包(一個消息被拆分成多個部分)的問題。這些問題可能會導(dǎo)致數(shù)據(jù)解析錯誤或數(shù)據(jù)不完整。
為什么UDP沒有粘包?
- 由于UDP沒有像TCP那樣的流控制和擁塞控制機(jī)制,它不會對數(shù)據(jù)進(jìn)行緩沖或重組。因此,在UDP中,每個數(shù)據(jù)報都是獨(dú)立傳輸?shù)模ń邮斩艘淮沃荒芙邮芤粭l獨(dú)立的消息),不存在多個消息粘在一起的問題,也就沒有粘包的概念。
- 由于UDP是不可靠的傳輸協(xié)議,它無法保證數(shù)據(jù)的可靠傳輸和順序傳輸。數(shù)據(jù)包可能會丟失、重復(fù)或亂序到達(dá)。在使用UDP時,應(yīng)該自行處理這些問題,比如使用應(yīng)答機(jī)制、超時重傳等手段來保證數(shù)據(jù)的可靠性和正確性。
粘包拆包發(fā)生場景
因?yàn)門CP是面向流,沒有邊界,而操作系統(tǒng)在發(fā)送TCP數(shù)據(jù)時,會通過緩沖區(qū)來進(jìn)行優(yōu)化,例如緩沖區(qū)為1024個字節(jié)大小。
- 如果一次請求發(fā)送的數(shù)據(jù)量比較小,沒達(dá)到緩沖區(qū)大小,TCP則會將多個請求合并為同一個請求進(jìn)行發(fā)送,這就形成了粘包問題。
- 如果一次請求發(fā)送的數(shù)據(jù)量比較大,超過了緩沖區(qū)大小,TCP就會將其拆分為多次發(fā)送,這就是拆包。
關(guān)于粘包和拆包可以參考下圖的幾種情況:
- 理想狀況:兩個數(shù)據(jù)包逐一分開發(fā)送
- 粘包:兩個包一同發(fā)送,
- 拆包:Server接收到不完整的或多出一部分的數(shù)據(jù)包
常見的解決方案
- 固定長度:發(fā)送端將每個消息固定為相同的長度,接收端按照固定長度進(jìn)行拆包。這樣可以確保每個消息的長度是一致的,但是對于不同長度的消息可能會浪費(fèi)一些空間。
- 分隔符:發(fā)送端在每個消息的末尾添加一個特殊的分隔符(比如換行符或特殊字符),接收端根據(jù)分隔符進(jìn)行拆包。這種方法適用于消息中不會出現(xiàn)分隔符的情況。
- 消息長度前綴:發(fā)送端在每個消息前面添加一個固定長度的消息長度字段,接收端先讀取消息長度字段,然后根據(jù)長度讀取相應(yīng)長度的數(shù)據(jù)。這種方法可以準(zhǔn)確地拆分消息,但需要保證消息長度字段的一致性。
代碼實(shí)現(xiàn)
固定長度
發(fā)送端將每個包都封裝成固定的長度,比如20字節(jié)大小。如果不足20字節(jié)可通過補(bǔ)0或空等進(jìn)行填充到指定長度;
服務(wù)端
package main
import (
"fmt"
"log"
"net"
)
func main() {
// 監(jiān)聽指定的TCP端口
listener, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
fmt.Println("Server started. Listening on localhost:8080...")
// 接收客戶端連接
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
// 啟動一個并發(fā)的goroutine來處理連接
go handleConnection(conn)
}
}
// 處理連接
func handleConnection(conn net.Conn) {
defer conn.Close()
// 讀取固定長度的數(shù)據(jù)
fixedLength := 20 // 假設(shè)要讀取的數(shù)據(jù)固定長度為20字節(jié)
buffer := make([]byte, fixedLength)
_, err := conn.Read(buffer)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Received data: %s\n", string(buffer))
// 可以在這里對接收到的數(shù)據(jù)進(jìn)行處理和響應(yīng)
// ...
// 發(fā)送響應(yīng)給客戶端
response := "Hello, Client!"
_, err = conn.Write([]byte(response))
if err != nil {
log.Fatal(err)
}
fmt.Println("Response sent successfully!")
}
客戶端
package main
import (
"fmt"
"log"
"net"
)
func main() {
// 建立TCP連接
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 發(fā)送固定長度的數(shù)據(jù)
message := "Hello, Server!"
fixedLength := 20 // 假設(shè)要發(fā)送的數(shù)據(jù)固定長度為20字節(jié)
// 如果消息長度小于固定長度,則使用空字符填充
if len(message) < fixedLength {
padding := make([]byte, fixedLength-len(message))
message += string(padding)
}
_, err = conn.Write([]byte(message))
if err != nil {
log.Fatal(err)
}
fmt.Println("Data sent successfully!")
}
分隔符
發(fā)送端在每個包的末尾使用固定的分隔符,例如\n文章來源:http://www.zghlxwxcb.cn/news/detail-616076.html
服務(wù)端
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
func main() {
// 監(jiān)聽指定的TCP端口
listener, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
fmt.Println("Server started. Listening on localhost:8080...")
// 接收客戶端連接
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
// 啟動一個并發(fā)的goroutine來處理連接
go handleConnection(conn)
}
}
// 處理連接
func handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
// 讀取一行數(shù)據(jù),以分隔符"\n"作為結(jié)束標(biāo)志
message, err := reader.ReadString('\n')
if err != nil {
log.Println(err)
break
}
// 去除消息中的換行符
message = strings.TrimRight(message, "\n")
fmt.Printf("Received message: %s\n", message)
// 可以在這里對接收到的消息進(jìn)行處理和響應(yīng)
// ...
// 發(fā)送響應(yīng)給客戶端
response := "Hello, Client!\n"
_, err = conn.Write([]byte(response))
if err != nil {
log.Println(err)
break
}
}
fmt.Println("Connection closed.")
}
客戶端
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
)
func main() {
// 建立TCP連接
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
reader := bufio.NewReader(os.Stdin)
for {
// 讀取用戶輸入的消息
fmt.Print("Enter message: ")
message, err := reader.ReadString('\n')
if err != nil {
log.Println(err)
break
}
// 發(fā)送消息給服務(wù)器
_, err = conn.Write([]byte(message))
if err != nil {
log.Println(err)
break
}
// 讀取服務(wù)器的響應(yīng)
response, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
log.Println(err)
break
}
fmt.Printf("Server response: %s", response)
}
fmt.Println("Connection closed.")
}
消息長度前綴
將消息分為頭部和消息體,頭部中保存整個消息的長度,只有讀取到足夠長度的消息之后才算是讀到了一個完整的消息;文章來源地址http://www.zghlxwxcb.cn/news/detail-616076.html
代碼實(shí)現(xiàn)
package main
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
)
const headerSize = 4 // 頭部長度的字節(jié)數(shù)
func main() {
// 啟動服務(wù)器
go startServer()
// 連接到服務(wù)器
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Println("連接服務(wù)器失敗:", err)
return
}
defer conn.Close()
// 發(fā)送消息
message := "Hello, Server!"
sendMessage(conn, message)
// 讀取服務(wù)器響應(yīng)
response, err := readMessage(conn)
if err != nil {
fmt.Println("讀取消息失敗:", err)
return
}
fmt.Println("服務(wù)器響應(yīng):", response)
}
func startServer() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("啟動服務(wù)器失敗:", err)
return
}
defer listener.Close()
fmt.Println("服務(wù)器已啟動,等待連接...")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("接受連接失敗:", err)
return
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
fmt.Printf("客戶端 %s 已連接\n", conn.RemoteAddr().String())
defer conn.Close()
// 讀取消息
message, err := readMessage(conn)
if err != nil {
fmt.Println("讀取消息失敗:", err)
return
}
fmt.Println("收到消息:", message)
// 發(fā)送響應(yīng)
response := "Hello, Client!"
sendMessage(conn, response)
}
func sendMessage(conn net.Conn, message string) error {
// 計算消息長度
messageLength := len(message)
// 將消息長度寫入頭部
header := make([]byte, headerSize)
binary.BigEndian.PutUint32(header, uint32(messageLength))
if _, err := conn.Write(header); err != nil {
return fmt.Errorf("寫入消息頭部失敗: %v", err)
}
// 寫入消息體
if _, err := conn.Write([]byte(message)); err != nil {
return fmt.Errorf("寫入消息體失敗: %v", err)
}
return nil
}
func readMessage(conn net.Conn) (string, error) {
// 讀取消息頭部
header := make([]byte, headerSize)
if _, err := io.ReadFull(conn, header); err != nil {
return "", fmt.Errorf("讀取消息頭部失敗: %v", err)
}
// 解析消息長度
messageLength := binary.BigEndian.Uint32(header)
// 讀取消息體
message := make([]byte, messageLength)
if _, err := io.ReadFull(conn, message); err != nil {
return "", fmt.Errorf("讀取消息體失敗: %v", err)
}
return string(message), nil
}
- 這段代碼中,我們啟動了一個TCP服務(wù)器,等待客戶端連接。客戶端在連接成功后,發(fā)送消息給服務(wù)器,服務(wù)器接收到消息后,返回一個響應(yīng)。
- 在發(fā)送消息時,我們首先計算消息的長度,并將長度以4字節(jié)的大端字節(jié)序?qū)懭氲筋^部。然后,將消息體寫入
總結(jié)
- TCP 不管發(fā)送端要發(fā)什么,都基于字節(jié)流把數(shù)據(jù)發(fā)到接收端。這個字節(jié)流里可能包含上一次想要發(fā)的數(shù)據(jù)的部分信息。接收端根據(jù)需要在消息里加上識別消息邊界的信息。不加就可能出現(xiàn)粘包問題
- UDP 是基于數(shù)據(jù)報的傳輸協(xié)議,每個數(shù)據(jù)報都是獨(dú)立傳輸?shù)模ń邮斩艘淮沃荒芙邮芤粭l獨(dú)立的消息),不會有粘包問題。
參考
- 硬核圖解|tcp為什么會粘包?背后的原因讓人暖心
到了這里,關(guān)于TCP的粘包、拆包、解決方案以及Go語言實(shí)現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!