什么是 TCP 粘包
粘包问题是指当发送两条消息时,比如发送了 ABC 和 DEF,但另一端接收到的却是 ABCD,像这种一次性读取了两条数据的情况就叫做粘包(正常情况应该是一条一条读取的), 正确读取 ABC 和 DEF 两条信息。

当发送的消息是 ABC 时,另一端却接收到的是 AB 和 C 两条信息,像这种情况就叫做半包。

为什么会有粘包和半包?
这是因为 TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息。

造成粘包的主要原因

发送方每次写入数据 < 套接字(Socket)缓冲区大小

接收方读取套接字(Socket)缓冲区数据不够及时。

造成半包的主要原因

发送方每次写入数据 > 套接字(Socket)缓冲区大小

发送的数据大于协议的 MTU (Maximum Transmission Unit,最大传输单元),因此必须拆包。

怎么处理粘包?
fix length 处理粘包
 

package frame_decoder

import (
	"fmt"
	"math/rand"
	"net"
	"week9/protocol"
)

// ClientTcpFrameDecoder length field based frame decoder
func ClientTcpFrameDecoder(conn net.Conn) {
	Log("client, length field based frame decoder")
	for i := 0; i < 10; i++ {
		userName := randStringRunes(6)
		words := "{\"Name\":\"" + userName + "20211217\",\"Meta\":\"golang\",\"Content\":\"message\"}"
		_, err := conn.Write(protocol.Packet([]byte(words)))
		if err != nil {
			fmt.Println(err, ",写入字符串错误 index=", i)
			return
		}
		fmt.Println(words) // 打印发送出去的信息
	}
	Log("send over")
}
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randStringRunes(n int) string {
	b := make([]rune, n)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

 


执行程序在 client server 目录

每次发送固定缓冲区大小数据。客户端和服务器约定每次发送请求的大小。例如客户端发送 1024 个字节,服务器接受 1024 个字节。

这样虽然可以解决粘包的问题,但是如果发送的数据小于 1024 个字节,就会导致数据内存冗余和浪费;且如果发送请求大于 1024 字节,会出现半包的问题,也就是数据接收的不完整。

delimiter based 处理粘包
 

package fix_length

import (
	"fmt"
	"net"
)

func ServerTcpFixLength(server net.Conn) {
	fmt.Println("server fixed length")
	const BYTES = 1024
	for {
		var buf = make([]byte, BYTES)
		_, err := server.Read(buf)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Println("client data: ", string(buf))
	}
}


​ 执行程序在 client server 目录

基于定界符来判断是不是一个请求(例如结尾’\n’). 客户端发送过来的数据,每次以 \n 结束,服务器每接受到一个 \n 就以此作为一个请求。然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包。这种方式的缺点在于如果数据量过大,查找定界符会消耗一些性能
length field based frame decoder 处理粘包
 
​ 执行程序在 client server 目录

在 TCP 协议头里面写入每次发送请求的长度。 客户端在协议头里面带入数据长度,服务器在接收到请求后,根据协议头里面的数据长度来决定接受多少数据,只有在读取到足够长度的消息之后才算是读到了一个完整的消息。之后会按照参数指定的包长度偏移量数据对接收到的数据进行解码,从而得到目标消息体数据。