Golang решает проблему распаковки липких пакетов TCP

Go

В чем проблема с липким пакетом

Недавно я использовал Golang для написания слоя сокетов и обнаружил, что иногда принимающая сторона считывала несколько пакетов данных одновременно. Итак, просматривая информацию, я обнаружил, что это легендарная проблема с прилипшими пакетами TCP. Давайте воспроизведем проблему, написав код ниже:

Код сервера 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: [这里才是一个完整的数据包]
...省略其它的...

Как видно из консольного вывода сервера, есть три типа вывода:

  1. Один - обычный пакетный вывод.
  2. Во-первых, несколько пакетов данных «слипаются» вместе, и мы определяем этот прочитанный пакет как «липкий».
  3. Во-первых, пакет данных «распаковывается» для формирования сломанного пакета, который мы определяем как полупакет.

Почему появляются полуупаковки и липкие упаковки?

  1. Клиент отправляет слишком много пакетов за определенный период времени, и сервер не обрабатывает их все. В результате данные будут задерживаться, что приведет к залипанию пакетов.
  2. Определенный буфер чтения недостаточно велик, а пакет данных слишком велик или генерируется из-за прилипших пакетов, поэтому сервер не может прочитать его весь сразу, что приводит к половине пакета.

Что следует учитывать при обращении с половиной упаковки и пакетом стика?

  1. TCP-соединение — это долгоживущее соединение, то есть одно соединение отправляет данные несколько раз.
  2. Данные, отправляемые каждый раз, структурированы, например, данные в формате JSON или протокол пакета данных определяется нами (заголовок пакета содержит фактическую длину данных, магический номер протокола и т. д.).

Решения

  1. Разделение фиксированной длины (максимальная длина каждого пакета данных, заполнение специальными символами при недостатке), но когда данных недостаточно, ресурсы передачи будут потрачены впустую
  2. использоватьконкретные персонажиразделить пакет данных, но если данные содержат разделенные символы, будет ошибка
  3. Добавление поля длины к пакету данных компенсирует недостатки двух вышеупомянутых идей.Рекомендуемое использование

Распаковка демо

Через приведенный выше анализ мы лучше всего проходимТретий способ мышленияРешить проблему с распаковкой и наклейкой.

ГолангbufioПредлагаем перспективную библиотекуScanner, чтобы решить эту проблему разделения данных.

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.

Проще говоря:

Сканерчитать данныеобеспечивает удобныйинтерфейс.连续调用Scan方法会逐个得到文件的“tokens”,跳过 tokens 之间的字节。 token 的规范由SplitFuncТип определения функции. Вместо этого мы можем предоставить пользовательскую функцию разделения.

см. далееSplitFuncКак выглядит функция типа:

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

Примеры использования, представленные в документации официального сайта Golang 🌰:

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

Итак, мы можем переписать нашу программу следующим образом:

Код сервера 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: [这里才是一个完整的数据包]
...省略其它的...