go語言處理TCP粘包

 閱讀大約需要3分鐘

go語言處理TCP粘包

TCP粘包是指發送方發送的若干數據包到接收方接收時粘成一包,從接收緩衝區看,後一包數據的頭緊接著前一包數據的尾。

粘包可能由發送方造成,也可能由接收方造成。

粘包的原因:

發送端原因: 由於TCP協議本身的機制(面向連接的可靠的協議-三次握手機制)客戶端與服務器會維持一個連接,數據在連接不斷開的情況下,可以持續不斷地將多個數據包發往服務器,但是如果發送的網絡數據包太小,那麼他本身會啟用Nagle算法對較小的數據包進行合併,然後再發送(超時或者包大小足夠)。 那麼這樣的話,服務器在接收到消息(數據流)的時候就無法區分哪些數據包是客戶端自己分開發送的,這樣產生了粘包。

接收端原因: 服務器在接收到數據後,放到緩衝區中,如果消息沒有被及時從緩存區取走,下次在取數據的時候可能就會出現一次取出多個數據包的情況,造成粘包現象。

TCP粘包一般有3種解決方案:

(1)發送固定長度的消息

(2)把消息的尺寸與消息一塊發送

(3)使用特殊標記來區分消息間隔

這裡採用第二種解決方案,下面來看下go語言實現的TCP粘包現象和封包、拆包。

tcpserver.go

package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
)

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	var buf [2048]byte
	for {
		n, err := reader.Read(buf[:])
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("reader.Read error :", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Printf("received data:%s\n\n", recvStr)
	}
}

func main() {

	listen, err := net.Listen("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("net.Listen error : ", err)
		return
	}
	defer listen.Close()
	fmt.Println("server start ...  ")

	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen.Accept error :", err)
			continue
		}
		go process(conn)
	}
}

tcpclient.go

package main

import (
	"fmt"
	"net"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("net.Dial error : ", err)
		return
	}
	defer conn.Close()
	fmt.Println("client start ... ")

	for i := 0; i < 30; i++ {
		msg := `hello hacker`
		conn.Write([]byte(msg))
	}

	fmt.Println("send data over... ")

}

分別運行服務端和客戶端,結果如下:

server start ...  
received data:hello hackerhello hackerhello hackerhello hackerhello hacker

received data:hello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerello hackerhello hackerhello hackerhello hacker

received data:hello hackerhello hackerhello hackerhello hacker

received data:hello hacker

received data:hello hackerhello hackerhello hackerhello hackerhello hacker

可以看到發生了粘包現象,客戶端發送了30次,服務器端接收到5次。

我們採用第二種方案把消息的長度和消息體一起發送。

tcpclient2.go

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
)

func Encode(message string) ([]byte, error) {
	var length = uint16(len(message))
	var nb = new(bytes.Buffer)

	// 寫入消息頭
	err := binary.Write(nb, binary.BigEndian, length)
	if err != nil {
		return nil, err
	}

	// 寫入消息體
	nb.Write([]byte(message))
	return nb.Bytes(), nil
}

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("net.Dial error : ", err)
		return
	}
	defer conn.Close()
	for i := 0; i < 30; i++ {
		msg := `hello hacker`
		data, err := Encode(msg)
		if err != nil {
			fmt.Println("Encode msg error : ", err)
			return
		}
		conn.Write(data)
	}
}

客戶端封包先計算消息體的長度,作為消息的頭部,之後寫入消息體,一起發送。

tcpserver2.go

package main

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"net"
)

func Decode(reader *bufio.Reader) (string, error) {
	// 讀取消息的長度
	lengthByte, _ := reader.Peek(2) // 讀取前2個字節,看看包頭
	fmt.Println("get byte",lengthByte)
	lengthBuff := bytes.NewBuffer(lengthByte)
	var length int16
	err := binary.Read(lengthBuff, binary.BigEndian, &length)
	if err != nil {
		return "", err
	}
	fmt.Println("get length ",length)
	if int16(reader.Buffered()) < length+2 {
		return "", err
	}

	realData := make([]byte, int(2+length))
	_, err = reader.Read(realData)
	if err != nil {
		return "", err
	}
	fmt.Println("get data",realData)
	return string(realData[2:]), nil
}

func process2(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)

	for {
		msg, err := Decode(reader)
		if err == io.EOF {
			return
		}
		if err != nil {
			fmt.Println("Decode error : ", err)
			return
		}
		fmt.Println("received data :", msg)
	}
}

func main() {

	listen, err := net.Listen("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("net.Listen error :", err)
		return
	}
	defer listen.Close()
	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen.Accept error :", err)
			continue
		}
		go process2(conn)
	}
}

服務器端先讀取消息的長度,之後在讀取消息體中的內容

分別運行服務端和客戶端,可以看到服務器端接收數據正常。