Go language handles TCP sticky packets

 5 minutes to read

go language handles TCP sticky packets

TCP sticky packet means that several data packets sent by the sender are glued into one packet when received by the receiver. From the perspective of the receiving buffer, the head of the next packet of data is immediately followed by the end of the previous packet of data.

Sticky packets can be caused by the sender or by the receiver.

Reasons for sticky packages:

Sending side reason: Due to the mechanism of the TCP protocol itself (connection-oriented reliable protocol - three-way handshake mechanism) the client and the server will maintain a connection, and the data can continue to send multiple data packets when the connection is not disconnected. To the server, but if the network packet sent is too small, then he will enable the Nagle algorithm to merge the smaller packets before sending (timeout or the packet size is sufficient). In this case, when the server receives a message (data stream), it cannot distinguish which data packets are sent separately by the client itself, which results in sticky packets.

Receiver’s reason: After the server receives the data, it puts it in the buffer. If the message is not taken from the buffer in time, the next time when the data is taken, multiple data packets may be taken out at one time, causing stickiness. package phenomenon.

There are generally 3 solutions for TCP sticky packets:

(1) Send a fixed-length message

(2) Send the size of the message together with the message

(3) Use special markers to distinguish message intervals

The second solution is adopted here. Let’s take a look at the TCP sticky phenomenon, packetization and unpacking implemented by the go language.

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... ")

}

Running the server and client separately, the results are as follows:

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

received data: hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker hello hacker

received data: hello hackerhello hackerhello hackerhello hacker

received data: hello hacker

received data: hello hackerhello hackerhello hackerhello hackerhello hacker

It can be seen that the sticky packet phenomenon occurs, the client sends 30 times, and the server receives 5 times.

We use the second scheme to send the length of the message together with the message body.

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)
	}
}

The client package first calculates the length of the message body as the header of the message, and then writes the message body and sends it together.

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)
	}
}

The server first reads the length of the message, and then reads the content of the message body

Run the server and client respectively, and you can see that the server receives data normally.