TCP粘包原理及解決方案


一、粘包是什么

​ 兩個程序能夠互相通信是采用了套接字(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
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM