一、粘包是什么
兩個程序能夠互相通信是采用了套接字(socket)技術,socket在發送端和接收端都有個緩存機制,發送端在把需要發送的數據先放在緩存上,等數據超過緩存大小時,就會打包發給接收端;接收端接到數據也會先放到緩存,再根據應用程序(recv/read)去讀取這些數據,直到讀完緩存上的數據。
TCP是一個流協議,TCP只保證把要發送的數據按包序號完整的發送給接收端,接收端讀取數據的時候會按應用程序上設置好的大小去讀取,假如接收端每次按照300字節大小去讀,實際業務需要數據600個字節,那么接收端讀取一次是沒有把應用需要的數據讀取完整,而需讀取兩次;再假如實際業務需要數據100個字節,接收端按照300字節大小去讀取,會把業務上分為兩個含義數據一次就讀取出來了,這就產生了粘包的問題,所以粘包問題的本質就是數據讀取邊界錯誤所致。
二、字節序
字節序是在內存中字節存儲順序,分為大端序(網絡字節序)和小端序(主機字節序);
高位字節:在ASCII上開始的字節,如0x00
低位字節:在ASCII上最后的字節,如0x7F
大端序:高位字節存入低地址;
小端序:低位字節存入低地址;
三、解決方案
上面所說的應用數據,是在TCP數據包里面的,應用數據的數據格式由用戶自己協定,比如http協議,有頭部和包體,頭部有設定數據長度,包體則是實際數據 。
那么解決粘包問題思路:
1,設置頭部信息,長度固定;
2,使用特定字符作為分隔符,用來分割數據邊界問題;
3,添加除頭部信息的數據長度。
下面用golang代碼結合上面3點思路解決粘包問題:
發送端封裝數據 client.go
package main
import (
"encoding/binary"
"fmt"
"net"
)
func main() {
//定義頭部信息固定長度為10,分隔符兩個字符:0x43, 0x53
headData := []byte{0x43, 0x53, 0, 0, 0, 0, 0, 0, 0, 0}
//實際數據
context := "測試內容"
//實際數據長度用4個字節,小端序方式來儲存
binary.LittleEndian.PutUint32(headData[2:6], uint32(len(context)))
//數據包(頭部信息+實際數據)
data := make([]byte, 0)
data = append(data, headData...)
data = append(data, context...)
fmt.Println(data) //輸出 [67 83 12 0 0 0 0 0 0 0 230 181 139 232 175 149 229 134 133 229 174 185]
//67 83 12 0 0 0 0 0 0 0 為頭部信息;230 181 139 232 175 149 229 134 133 229 174 185 為實際數據
conn, err := net.Dial("tcp", "192.168.3.116:9010")
if err != nil {
fmt.Println("connect err=", err)
return
}
_, err = conn.Write(data)
if err != nil {
fmt.Println("send data failed, err= ", err)
return
}
}
接收端拆分數據 server.go
package main
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
)
func main() {
fmt.Println("服務器開始監聽......")
listen, err := net.Listen("tcp", "192.168.3.116:9010")
if err != nil {
fmt.Println("listen err=", err)
return
}
// 延遲關閉
defer listen.Close()
// 循環等待客戶端連接
for {
fmt.Println("循環等待客戶端連接...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err=", err)
}
// 創建goroutine處理客戶端連接
go process(conn)
}
}
//處理客戶端連接
func process(conn net.Conn) {
defer func() {
fmt.Println("conn.Close")
conn.Close()
}()
reader := bufio.NewReader(conn)
for {
msg, err := Decode(reader)
if err != nil {
fmt.Println("decode failed, err = ", err)
return
}
fmt.Println("收到數據", msg)
}
}
func Decode(reader *bufio.Reader) (string, error) {
var err error
// 讀取前10個字節的數據(表示數據包頭部的信息)
// peek操作只讀數據,但不會移動數據位置!(沒有數據則阻塞)
headData, err := reader.Peek(10)
fmt.Println(headData)//[67 83 12 0 0 0 0 0 0 0]
if err != nil {
return "", err
}
//驗證頭部分割符
if headData[0] != 0x43 || headData[1] != 0x53 {
err = errors.New(fmt.Sprintf("headData[0]: %d,headData[1]: %d", headData[0], headData[1]))
return "", err
}
headLen := len(headData)
//驗證頭部長度
if headLen < 10 {
err = errors.New(fmt.Sprintf("headData len :%d", headLen))
return "", err
}
//獲取實際數據長度byte
lengthByte := headData[2:6]//[12 0 0 0]
lengthBuf := bytes.NewBuffer(lengthByte)
var dataLen int32
//小端序方式轉換實際數據長度數字
err = binary.Read(lengthBuf, binary.LittleEndian, &dataLen)
if err != nil {
return "", err
}
//數據包長度(頭部信息長度+實際數據長度)
packLen := 10 + dataLen
pack := make([]byte, packLen)
// 判斷緩存數據是否滿足一個完整數據包
buffLen := int32(reader.Buffered())
if buffLen < packLen {
cacheBuff := make([]byte, 0)
dataBuff := make([]byte, buffLen, buffLen)
for {
n, err := reader.Read(dataBuff)
cacheBuff = append(cacheBuff, dataBuff[0:n]...)
if err == nil {
if int32(len(cacheBuff)) < packLen {
num := packLen - int32(len(cacheBuff))
dataBuff = make([]byte, num, num)
continue
}
} else {
return "", err
}
pack = cacheBuff
break
}
} else {
// 讀取整個數據包
_, err = reader.Read(pack)
if err != nil {
return "", err
}
}
// str, _ := DecodeData(pack[10:])
str := pack[10:]
fmt.Println(string(str)//輸出:測試內容
return string(str), nil
}