TCP 粘包问题

目录

什么是粘包?

TCP 为什么会出现粘包?

如何解决 TCP 粘包问题?

方法一:发送定长消息

方法二:使用特殊分隔符

方法三:消息头标识长度

例子


什么是粘包?

粘包指的是在基于流的协议(如 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))
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值