6 lines of code to quickly resolve the golang TCP sticky packet problem

  • 2020-06-12 09:31:30
  • OfStack

preface

What is TCP sticky packets and why TCP sticky packets are not discussed in this article. This article USES golang's bufio.Scanner To implement custom protocol unpacking.

Without further ado, let's take a look at the details.

Protocol packet definition

This article simulates a log server that receives and displays packets sent by the client


type Package struct {
 Version  [2]byte //  Agreement version, tentative V1
 Length   int16 //  Length of data section 
 Timestamp  int64 //  The time stamp 
 HostnameLength int16 //  Hostname length 
 Hostname  []byte //  The host name 
 TagLength  int16 //  The length of the label 
 Tag   []byte //  The label 
 Msg   []byte //  Log data 
}

There is nothing to be said for the protocol definition, which is defined in terms of the specific business logic.

The data package

Since TCP is a language-independent protocol, it is also impossible to send the protocol packet structure directly to the TCP connection. Only byte stream data can be sent, so you need to implement the data encoding yourself. Fortunately, golang provides binary to help us implement network byte coding.


func (p *Package) Pack(writer io.Writer) error {
 var err error
 err = binary.Write(writer, binary.BigEndian, &p.Version)
 err = binary.Write(writer, binary.BigEndian, &p.Length)
 err = binary.Write(writer, binary.BigEndian, &p.Timestamp)
 err = binary.Write(writer, binary.BigEndian, &p.HostnameLength)
 err = binary.Write(writer, binary.BigEndian, &p.Hostname)
 err = binary.Write(writer, binary.BigEndian, &p.TagLength)
 err = binary.Write(writer, binary.BigEndian, &p.Tag)
 err = binary.Write(writer, binary.BigEndian, &p.Msg)
 return err
}

The output target of the Pack method is ES30en.Writer, which is conducive to the extension of the interface. Once the interface is implemented, the data can be written. binary.BigEndian is a byte order. I will not discuss it in this article, but you can do your own research if you need to.

Data unpack

Unpacking requires parsing the TCP packet into the structure, and we'll see why we need to add several data-independent length fields.


func (p *Package) Unpack(reader io.Reader) error {
 var err error
 err = binary.Read(reader, binary.BigEndian, &p.Version)
 err = binary.Read(reader, binary.BigEndian, &p.Length)
 err = binary.Read(reader, binary.BigEndian, &p.Timestamp)
 err = binary.Read(reader, binary.BigEndian, &p.HostnameLength)
 p.Hostname = make([]byte, p.HostnameLength)
 err = binary.Read(reader, binary.BigEndian, &p.Hostname)
 err = binary.Read(reader, binary.BigEndian, &p.TagLength)
 p.Tag = make([]byte, p.TagLength)
 err = binary.Read(reader, binary.BigEndian, &p.Tag)
 p.Msg = make([]byte, p.Length-8-2-p.HostnameLength-2-p.TagLength)
 err = binary.Read(reader, binary.BigEndian, &p.Msg)
 return err
}

Since the data such as host name and label are not fixed in length, two bytes are needed to identify the data length, otherwise it is impossible to distinguish the host name, label name and log data when only knowing the total data length.

Packet sticky packet problem solved

The above only solves the encoding/decoding problem, provided that the received packet does not generate the sticky packet problem, which is solved by correctly splitting the data in the byte stream. 1 Generally, the following practices are adopted:

The disadvantage of fixed-length separation (the maximum length of each packet) is that transmission resources are wasted when data is insufficient The downside to specific character separations (such as rn) is that rn can cause problems if you have it in the body Add the length field to the packet (used in this article)

golang provides bufio.Scanner To solve the sticky wrap problem.


scanner := bufio.NewScanner(reader) // reader In order to achieve the io.Reader Object of an interface, such as net.Conn
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 if !atEOF && data[0] == 'V' { //  Since the data packet header we defined starts with a two-byte version number, we only have to start with V Only the first packet is processed 
  if len(data) > 4 { //  If received data >4 bytes (2 Byte version number +2 Byte packet length )
   length := int16(0)
   binary.Read(bytes.NewReader(data[2:4]), binary.BigEndian, &length) //  Read packet control 3-4 byte (int16)=> Length of data section 
   if int(length)+4 <= len(data) { //  If the length of the data body is read +2 Byte version number +2 The length of the byte data does not exceed that of the read data ( In fact, we've solved it successfully 1 A package )
    return int(length) + 4, data[:int(length)+4], nil
   }
  }
 }
 return
})
//  Prints the received packet 
for scanner.Scan() {
 scannedPack := new(Package)
 scannedPack.Unpack(bytes.NewReader(scanner.Bytes()))
 log.Println(scannedPack)
}

The core of this article lies in scanner.Split Method used to parse the TCP packet

Complete source code


package main
import (
 "bufio"
 "bytes"
 "encoding/binary"
 "fmt"
 "io"
 "log"
 "os"
 "time"
)

type Package struct {
 Version  [2]byte //  Protocol version 
 Length   int16 //  Length of data section 
 Timestamp  int64 //  The time stamp 
 HostnameLength int16 //  Hostname length 
 Hostname  []byte //  The host name 
 TagLength  int16 // Tag The length of the 
 Tag   []byte // Tag
 Msg   []byte //  Length of data section 
}

func (p *Package) Pack(writer io.Writer) error {
 var err error
 err = binary.Write(writer, binary.BigEndian, &p.Version)
 err = binary.Write(writer, binary.BigEndian, &p.Length)
 err = binary.Write(writer, binary.BigEndian, &p.Timestamp)
 err = binary.Write(writer, binary.BigEndian, &p.HostnameLength)
 err = binary.Write(writer, binary.BigEndian, &p.Hostname)
 err = binary.Write(writer, binary.BigEndian, &p.TagLength)
 err = binary.Write(writer, binary.BigEndian, &p.Tag)
 err = binary.Write(writer, binary.BigEndian, &p.Msg)
 return err
}
func (p *Package) Unpack(reader io.Reader) error {
 var err error
 err = binary.Read(reader, binary.BigEndian, &p.Version)
 err = binary.Read(reader, binary.BigEndian, &p.Length)
 err = binary.Read(reader, binary.BigEndian, &p.Timestamp)
 err = binary.Read(reader, binary.BigEndian, &p.HostnameLength)
 p.Hostname = make([]byte, p.HostnameLength)
 err = binary.Read(reader, binary.BigEndian, &p.Hostname)
 err = binary.Read(reader, binary.BigEndian, &p.TagLength)
 p.Tag = make([]byte, p.TagLength)
 err = binary.Read(reader, binary.BigEndian, &p.Tag)
 p.Msg = make([]byte, p.Length-8-2-p.HostnameLength-2-p.TagLength)
 err = binary.Read(reader, binary.BigEndian, &p.Msg)
 return err
}

func (p *Package) String() string {
 return fmt.Sprintf("version:%s length:%d timestamp:%d hostname:%s tag:%s msg:%s",
  p.Version,
  p.Length,
  p.Timestamp,
  p.Hostname,
  p.Tag,
  p.Msg,
 )
}

func main() {
 hostname, err := os.Hostname()
 if err != nil {
  log.Fatal(err)
 }

 pack := &Package{
  Version:  [2]byte{'V', '1'},
  Timestamp:  time.Now().Unix(),
  HostnameLength: int16(len(hostname)),
  Hostname:  []byte(hostname),
  TagLength:  4,
  Tag:   []byte("demo"),
  Msg:   []byte((" Now the time is :" + time.Now().Format("2006-01-02 15:04:05"))),
 }
 pack.Length = 8 + 2 + pack.HostnameLength + 2 + pack.TagLength + int16(len(pack.Msg))

 buf := new(bytes.Buffer)
 //  write 4 Time, the simulation TCP Package the performance 
 pack.Pack(buf)
 pack.Pack(buf)
 pack.Pack(buf)
 pack.Pack(buf)
 // scanner
 scanner := bufio.NewScanner(buf)
 scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
  if !atEOF && data[0] == 'V' {
   if len(data) > 4 {
    length := int16(0)
    binary.Read(bytes.NewReader(data[2:4]), binary.BigEndian, &length)
    if int(length)+4 <= len(data) {
     return int(length) + 4, data[:int(length)+4], nil
    }
   }
  }
  return
 })
 for scanner.Scan() {
  scannedPack := new(Package)
  scannedPack.Unpack(bytes.NewReader(scanner.Bytes()))
  log.Println(scannedPack)
 }
 if err := scanner.Err(); err != nil {
  log.Fatal(" Invalid packet ")
 }
}

Write in the last

As a powerful network programming language, golang is very important to implement custom protocols. In fact, it is not difficult to implement custom protocols. The following steps are:

Packet encoding Packet decoding Deal with TCP sticking problem Disconnect and reconnect (can be done using heartbeat)(not required)

conclusion


Related articles: