Golang解決TCP粘包拆包問題

什麼是粘包問題

最近在使用Golang編寫Socket層,發現有時候接收端會一次讀到多個數據包的問題。因而經過查閱資料,發現這個就是傳說中的TCP粘包問題。下面經過編寫代碼來重現這個問題:golang

服務端代碼 server/main.go

func main() {
	l, err := net.Listen("tcp", ":4044")
	if err != nil {
		panic(err)
	}
	fmt.Println("listen to 4044")
	for {
        // 監聽到新的鏈接,建立新的 goroutine 交給 handleConn函數 處理
		conn, err := l.Accept()
		if err != nil {
			fmt.Println("conn err:", err)
		} else {
			go handleConn(conn)
		}
	}
}

func handleConn(conn net.Conn) {
	defer conn.Close()
	defer fmt.Println("關閉")
	fmt.Println("新鏈接:", conn.RemoteAddr())

	result := bytes.NewBuffer(nil)
	var buf [1024]byte
	for {
		n, err := conn.Read(buf[0:])
		result.Write(buf[0:n])
		if err != nil {
			if err == io.EOF {
				continue
			} else {
				fmt.Println("read err:", err)
				break
			}
		} else {
			fmt.Println("recv:", result.String())
		}
		result.Reset()
	}
}
複製代碼

客戶端代碼 client/main.go

func main() {
	data := []byte("[這裏纔是一個完整的數據包]")
	conn, err := net.DialTimeout("tcp", "localhost:4044", time.Second*30)
	if err != nil {
		fmt.Printf("connect failed, err : %v\n", err.Error())
        return
	}
	for i := 0; i <1000; i++ {
		_, err = conn.Write(data)
		if err != nil {
			fmt.Printf("write failed , err : %v\n", err)
			break
		}
	}
}
複製代碼

運行結果

listen to 4044
新鏈接: [::1]:53079
recv: [這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據�
recv: �][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包][這裏纔是一個完整的數據包][這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
...省略其它的...
複製代碼

從服務端的控制檯輸出能夠看出,存在三種類型的輸出:bash

  1. 一種是正常的一個數據包輸出。
  2. 一種是多個數據包「粘」在了一塊兒,咱們定義這種讀到的包爲粘包。
  3. 一種是一個數據包被「拆」開,造成一個破碎的包,咱們定義這種包爲半包。

爲何會出現半包和粘包?

  1. 客戶端一段時間內發送包的速度太多,服務端沒有所有處理完。因而數據就會積壓起來,產生粘包。
  2. 定義的讀的buffer不夠大,而數據包太大或者因爲粘包產生,服務端不能一次所有讀完,產生半包。

何時須要考慮處理半包和粘包?

  1. TCP鏈接是長鏈接,即一次鏈接屢次發送數據。
  2. 每次發送的數據是結構的,好比 JSON格式的數據 或者 數據包的協議是由咱們本身定義的(包頭部包含實際數據長度、協議魔數等)。

解決思路

  1. 定長分隔(每一個數據包最大爲該長度,不足時使用特殊字符填充) ,可是數據不足時會浪費傳輸資源
  2. 使用特定字符來分割數據包,可是若數據中含有分割字符則會出現Bug
  3. 在數據包中添加長度字段,彌補了以上兩種思路的不足,推薦使用

拆包演示

經過上述分析,咱們最好經過第三種思路來解決拆包粘包問題。app

Golang的bufio庫中有爲咱們提供了Scanner,來解決這類分割數據的問題。tcp

type Scanner

Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text. Successive calls to the Scan method will step through the 'tokens' of a file, skipping the bytes between the tokens. The specification of a token is defined by a split function of type SplitFunc; the default split function breaks the input into lines with line termination stripped. Split functions are defined in this package for scanning a file into lines, bytes, UTF-8-encoded runes, and space-delimited words. The client may instead provide a custom split function.ide

簡單來說便是:函數

Scanner爲 讀取數據 提供了方便的 接口。連續調用Scan方法會逐個獲得文件的「tokens」,跳過 tokens 之間的字節。token 的規範由 SplitFunc 類型的函數定義。咱們能夠改成提供自定義拆分功能。ui

接下來看看 SplitFunc 類型的函數是什麼樣子的:this

type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error) 複製代碼

Golang官網文檔上提供的使用例子🌰:spa

func main() {
	// An artificial input source.
	const input = "1234 5678 1234567901234567890"
	scanner := bufio.NewScanner(strings.NewReader(input))
	// Create a custom split function by wrapping the existing ScanWords function.
	split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
		advance, token, err = bufio.ScanWords(data, atEOF)
		if err == nil && token != nil {
			_, err = strconv.ParseInt(string(token), 10, 32)
		}
		return
	}
	// Set the split function for the scanning operation.
	scanner.Split(split)
	// Validate the input
	for scanner.Scan() {
		fmt.Printf("%s\n", scanner.Text())
	}

	if err := scanner.Err(); err != nil {
		fmt.Printf("Invalid input: %s", err)
	}
}
複製代碼

因而,咱們能夠這樣改寫咱們的程序:code

服務端代碼 server/main.go

func main() {
	l, err := net.Listen("tcp", ":4044")
	if err != nil {
		panic(err)
	}
	fmt.Println("listen to 4044")
	for {
		conn, err := l.Accept()
		if err != nil {
			fmt.Println("conn err:", err)
		} else {
			go handleConn2(conn)
		}
	}
}

func packetSlitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
        // 檢查 atEOF 參數 和 數據包頭部的四個字節是否 爲 0x123456(咱們定義的協議的魔數)
	if !atEOF && len(data) > 6 && binary.BigEndian.Uint32(data[:4]) == 0x123456 {
		var l int16
                // 讀出 數據包中 實際數據 的長度(大小爲 0 ~ 2^16)
		binary.Read(bytes.NewReader(data[4:6]), binary.BigEndian, &l)
		pl := int(l) + 6
		if pl <= len(data) {
			return pl, data[:pl], nil
		}
	}
	return
}

func handleConn2(conn net.Conn) {
	defer conn.Close()
	defer fmt.Println("關閉")
	fmt.Println("新鏈接:", conn.RemoteAddr())
	result := bytes.NewBuffer(nil)
        var buf [65542]byte // 因爲 標識數據包長度 的只有兩個字節 故數據包最大爲 2^16+4(魔數)+2(長度標識)
	for {
		n, err := conn.Read(buf[0:])
		result.Write(buf[0:n])
		if err != nil {
			if err == io.EOF {
				continue
			} else {
				fmt.Println("read err:", err)
				break
			}
		} else {
			scanner := bufio.NewScanner(result)
			scanner.Split(packetSlitFunc)
			for scanner.Scan() {
				fmt.Println("recv:", string(scanner.Bytes()[6:]))
			}
		}
		result.Reset()
	}
}
複製代碼

客戶端代碼 client/main.go

func main() {
	data := []byte("[這裏纔是一個完整的數據包]")
	l := len(data)
	fmt.Println(l)
	magicNum := make([]byte, 4)
	binary.BigEndian.PutUint32(magicNum, 0x123456)
	lenNum := make([]byte, 2)
	binary.BigEndian.PutUint16(lenNum, uint16(l))
	packetBuf := bytes.NewBuffer(magicNum)
	packetBuf.Write(lenNum)
	packetBuf.Write(data)
	conn, err := net.DialTimeout("tcp", "localhost:4044", time.Second*30)
	if err != nil {
		fmt.Printf("connect failed, err : %v\n", err.Error())
                return
	}
	for i := 0; i <1000; i++ {
		_, err = conn.Write(packetBuf.Bytes())
		if err != nil {
			fmt.Printf("write failed , err : %v\n", err)
			break
		}
	}
}
複製代碼

運行結果

listen to 4044
新鏈接: [::1]:55738
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
recv: [這裏纔是一個完整的數據包]
...省略其它的...
複製代碼
相關文章
相關標籤/搜索