目录
什么是粘包?
粘包指的是在基于流的协议(如 TCP)中,由于数据的传输特性导致接收方无法明确区分多条消息的边界,从而将多条消息粘合在一起接收,或者将一条消息拆分成多次接收。
TCP 为什么会出现粘包?
TCP 是一个字节流协议,不是面向消息的协议。它只保证数据按照顺序可靠传输,但不关心发送方的数据包结构。TCP 可能会将多次发送的数据合并到一个 TCP 数据包中发送,造成粘包。例如,连续两次发送 10 字节数据,可能会被合并为一个 20 字节的 TCP 数据包。
比如:
发送方:
send("Hello")
send("World")
接收方:
recv() -> "HelloWorld"
UDP 不会有粘包问题:UDP 是面向数据报的协议,每次发包的时候都对应一个完整的 UDP 数据报,接收包的时候也只会接收到一个完整的数据报。因此,不会出现粘包或拆包问题。但 UDP 不保证数据的顺序和可靠性,因此需要上层协议保证消息的正确性。
如何解决 TCP 粘包问题?
方法一:发送定长消息
我们让发送的每一条消息的长度都是固定的,如果长度不够就填充空内容,接收方可以根据固定长度解析数据。但是缺点是浪费空间。
发送方:
send("Hello ")
send("World ")
接收方:
recv() -> "Hello "
recv() -> "World "
方法二:使用特殊分隔符
我们在每条消息之间插入一个特殊字符作为分隔符,如换行符或其他非业务字符。缺点是消息中不能包含分隔符,需对分隔符进行转义。
发送方:
send("Hello\n")
send("World\n")
接收方:
recv() -> "Hello\nWorld\n"
按 `\n` 分割 -> ["Hello", "World"]
方法三:消息头标识长度
我们在消息开头添加一个定长字段,表示消息的长度,接收方先读取消息头,再根据长度读取对应的消息内容。
发送方:
send("5Hello")
send("5World")
接收方:
recv() -> "5Hello5World"
解析 -> ["Hello", "World"]
例子
以Go语言为例。在这个例子中,和方法三的原理相同,发送的数字都有标识作用。不过这个例子中把消息长度和消息实体分开发送了。
我们在使用的时候,只需要调用 NewPacketIO 方法获取一个 PacketIO 对象,然后调用 WritePkg 或 ReadPkg 就可以了。
package main
import (
"encoding/binary"
"errors"
"net"
)
// PacketIO 处理数据包的输入输出
type PacketIO struct {
conn net.Conn // 服务端与客户端链接
buf [8096]byte // 缓冲
}
func NewPacketIO(conn net.Conn) *PacketIO {
return &PacketIO{
conn: conn,
buf: [8096]byte{},
}
}
// WritePkg 写包。先发送信息的长度,然后发送真正的信息。
func (p *PacketIO) WritePkg(data []byte) error {
pkgLen := uint32(len(data))
// 把 pkgLen 存储到 buf 的前四位中
binary.BigEndian.PutUint32(p.buf[:4], pkgLen)
// 先发送长度
n, err := p.conn.Write(p.buf[:4])
if n != 4 {
return errors.New("发送信息错误")
} else if err != nil {
return err
}
// 发送真正的信息
n, err = p.conn.Write(data)
// 如果理论上发送信息的长度和实际上发送的长度不相等
if n != int(pkgLen) {
return errors.New("发送信息错误")
} else if err != nil {
return err
}
return nil
}
// ReadPkg 读包。先读取信息的长度,然后读取真正的信息。
func (p *PacketIO) ReadPkg() ([]byte, error) {
// 读取信息的长度
_, err := p.conn.Read(p.buf[:4])
if err != nil {
return nil, err
}
// 转换为数字
pkgLen := binary.BigEndian.Uint32(p.buf[:4])
// 读取真正的信息
n, err := p.conn.Read(p.buf[:pkgLen])
if n != int(pkgLen) {
return nil, errors.New("读取信息错误")
} else if err != nil {
return nil, err
}
return p.buf[:pkgLen], nil
}
调用:
package main
import (
"fmt"
"net"
)
func main() {
listen, err := net.Listen("tcp", "0.0.0.0:8090")
if err != nil {
fmt.Println("listen err = ", err)
return
}
defer listen.Close()
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accept() err = ", err)
return
}
packetIO := NewPacketIO(conn)
// 发送消息
err = packetIO.WritePkg([]byte("hello world!"))
if err != nil {
return
}
// 读取客户端的消息...
data, err := packetIO.ReadPkg()
if err != nil {
return
}
fmt.Println(string(data))
}