解开粘包拆包谜团

一、什么是粘包和拆包?

1.先理解 MTU 和 MSS

  • MTU:全称 Maximum transmission unit 最大传输单元,由硬件规定;一个网络包的最大长度,以太网中一般为 1500 字节;

  • MSS:全称 Maximum Segment Size 最大分段大小,除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;一般为 1460 字节。TCP 在建立连接的时候通常要协商双方的 MSS 值

MSS 计算公式:MTU - IP Header(20byte) - TCP Header(20byte) -> 1500-20-20=1460

TCP 底层并不了解上层业务数据的具体含义,它会根据TCP 缓冲区()的实际情况进行包的划分。

所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。

2.粘包

场景
  • 要发送的数据小于TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包;

  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;

举例

粘包:TCP 发了两个数据:A 数据长度 18ByteB 数据长度 14Byte。接受端一下子读了 32Byte。

解决:每段数据前加一个 2Byte 的消息头长度,用来表示这个数据
段的长度。A 数据:2Byte(表示 A 数据长度,简称消息头)+16Byte(A
的实际数据,简称消息体)。接受端先读 2Bye 得出剩下数据长度 18
再读 18Byte 那么就是 A 的数据了。

这个问题并不是 TCP 协议的问题,其实就是“如何设计应用层协议的问题”

3.拆包

场景
  • 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包;

  • 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包

举例

拆包:TCP 发了一个数据:A 数据长度 2048 Byte,接受端最大为 1460 Byte,随意就会奖该数据包拆分发送

解决:和粘包相对应,先读取消息头,然后再读消息体

二、粘包和拆包解决策略

由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:

  1. 消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  2. 设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如 FTP 协议。
  3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。
  4. 更复杂的应用层协议。如 Json,Protobuf

三、Go实现获取完整数据报文

代码见Github,记得选study分支

实现获取完整数据报文所在目录:

├── code
│   ├── base
│   |   ├── unpack
│   |   ├── ├──tcp_client    tcp客户端实现
│   |   ├── ├──tcp_server    tcp服务端实现
│   |   ├── ├──unpack        数据编解码

整个过程步骤如图:
在这里插入图片描述
为了避免粘包/拆包情况,做了如下处理:

  1. Encode(),编码数据,消息格式为:消息头长度+消息体长度+消息体内容
  2. Decode(). 解码数据,和Encode()对应的解码方法
  3. 消息定长,每次只能发10个字节

贴部分代码,主要是数据编码和解码:

package unpack

import (
	"encoding/binary"
	"errors"
	"io"
)

// MsgHeader 消息头
const MsgHeader = "hello"

// Encode 编码数据
// 粘包处理,消息格式:消息头长度+消息体长度+消息体内容
func Encode(bytesBuffer io.Writer, content string) error {
	// binary.Write将数据以二进制的形式写入writer,其中使用BigEndian进行编码
	// 1.先写入消息头部长度
	if err := binary.Write(bytesBuffer, binary.BigEndian, []byte(MsgHeader)); err != nil {
		return err
	}
	// 2.再写如消息体长度
	contetLen := int32(len([]byte(content)))
	if err := binary.Write(bytesBuffer, binary.BigEndian, contetLen); err != nil {
		return err
	}
	// 3.写入消息体内容
	if err := binary.Write(bytesBuffer, binary.BigEndian, []byte(content)); err != nil {
		return err
	}
	return nil
}

// Decode 解码数据
func Decode(bytesBuffer io.Reader) (bodyBuf []byte, err error) {
	// 创建一个长度为消息头长度大小的切片
	magicBuf := make([]byte, len(MsgHeader))
	// 1.读取消息头  TODO 为啥不用 BigEnian读取?
	if _, err := io.ReadFull(bytesBuffer, magicBuf); err != nil {
		return nil, err
	}
	if string(magicBuf) != MsgHeader {
		return nil, errors.New("MsgHeader error")
	}
	// 2.读取消息体长度
	contenLen := make([]byte, 10) // 自定义为固定值10
	if _, err = io.ReadFull(bytesBuffer, contenLen); err != nil {
		return nil, err
	}
	// 3.读取消息体,在读取前
	realContentLen := binary.BigEndian.Uint32(contenLen) // 将二进制contenLen解码为大端字节序
	bodyBuf = make([]byte, realContentLen)
	if _, err = io.ReadFull(bytesBuffer, bodyBuf); err != nil {
		return nil, err
	}
	return bodyBuf, err
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值