1. 程式人生 > 其它 >TCP粘包原理及解決方案

TCP粘包原理及解決方案

一、粘包是什麼

​ 兩個程式能夠互相通訊是採用了套接字(socket)技術,socket在傳送端和接收端都有個快取機制,傳送端在把需要傳送的資料先放在快取上,等資料超過快取大小時,就會打包發給接收端;接收端接到資料也會先放到快取,再根據應用程式(recv/read)去讀取這些資料,直到讀完快取上的資料。

​ TCP是一個流協議,TCP只保證把要傳送的資料按包序號完整的傳送給接收端,接收端讀取資料的時候會按應用程式上設定好的大小去讀取,假如接收端每次按照300位元組大小去讀,實際業務需要資料600個位元組,那麼接收端讀取一次是沒有把應用需要的資料讀取完整,而需讀取兩次;再假如實際業務需要資料100個位元組,接收端按照300位元組大小去讀取,會把業務上分為兩個含義資料一次就讀取出來了,這就產生了粘包的問題,所以粘包問題的本質就是資料讀取邊界錯誤所致。

二、位元組序

​ 位元組序是在記憶體中位元組儲存順序,分為大端序(網路位元組序)和小端序(主機位元組序);

​ 高位位元組:在ASCII上開始的位元組,如0x00

​ 低位位元組:在ASCII上最後的位元組,如0x7F

​ 大端序:高位位元組存入低地址;
​ 小端序:低位位元組存入低地址;

三、解決方案

​ 上面所說的應用資料,是在TCP資料包裡面的,應用資料的資料格式由使用者自己協定,比如http協議,有頭部和包體,頭部有設定資料長度,包體則是實際資料 。

​ 那麼解決粘包問題思路:

​ 1,設定頭部資訊,長度固定;

​ 2,使用特定字元作為分隔符,用來分割資料邊界問題;

​ 3,新增除頭部資訊的資料長度。

下面用golang程式碼結合上面3點思路解決粘包問題:

傳送端封裝資料

//定義頭部資訊固定長度為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 為實際資料

接收端拆分資料

// 讀取前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 {
    fmt.Println("2")
    return "", err
  }
}
// str, _ := DecodeData(pack[10:])
str := pack[10:]
fmt.Println(string(data)//輸出:測試內容