В чем проблема с липким пакетом
Недавно я использовал 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: [这里才是一个完整的数据包]
...省略其它的...
Как видно из консольного вывода сервера, есть три типа вывода:
- Один - обычный пакетный вывод.
- Во-первых, несколько пакетов данных «слипаются» вместе, и мы определяем этот прочитанный пакет как «липкий».
- Во-первых, пакет данных «распаковывается» для формирования сломанного пакета, который мы определяем как полупакет.
Почему появляются полуупаковки и липкие упаковки?
- Клиент отправляет слишком много пакетов за определенный период времени, и сервер не обрабатывает их все. В результате данные будут задерживаться, что приведет к залипанию пакетов.
- Определенный буфер чтения недостаточно велик, а пакет данных слишком велик или генерируется из-за прилипших пакетов, поэтому сервер не может прочитать его весь сразу, что приводит к половине пакета.
Что следует учитывать при обращении с половиной упаковки и пакетом стика?
- TCP-соединение — это долгоживущее соединение, то есть одно соединение отправляет данные несколько раз.
- Данные, отправляемые каждый раз, структурированы, например, данные в формате JSON или протокол пакета данных определяется нами (заголовок пакета содержит фактическую длину данных, магический номер протокола и т. д.).
Решения
- Разделение фиксированной длины (максимальная длина каждого пакета данных, заполнение специальными символами при недостатке), но когда данных недостаточно, ресурсы передачи будут потрачены впустую
- использоватьконкретные персонажиразделить пакет данных, но если данные содержат разделенные символы, будет ошибка
- Добавление поля длины к пакету данных компенсирует недостатки двух вышеупомянутых идей.Рекомендуемое использование
Распаковка демо
Через приведенный выше анализ мы лучше всего проходимТретий способ мышленияРешить проблему с распаковкой и наклейкой.
Голанг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: [这里才是一个完整的数据包]
...省略其它的...