TCP粘”包”问题浅析及解决方案Golang代码实现

一、粘”包”问题简介

在socket网络编程中,都是端到端通信, 客户端端口+客户端IP+服务端端口+服务端IP+传输协议就组成一个可以唯一可以明确的标识一条连接。在TCP的socket编程中,发送端和接收端也同样遵循这样的规则。

如果发送端多次发送字符串,接收端从socket读取数据放到接收数据的recv数组,由于recv数组初始化为\0,仅收到部分字符串就开始打印。该部分字符串放在recv数组中,末尾仍是以\0结尾,打印函数见到\0则默认结束打印输出,后部分数据还未读取到就出现读取字符不完整的情况。如果出现乱码,则可能是因为,定义的recv数组容量不够,接收端的数据占满recv数组之后,打印函数仍会寻找以\0为边界的字符作为结束标志,这样从内存中就会读取数据的时候越界。内存中存在的数据不一定可读,打印函数在按照字符的格式输出就会显示乱码。所以有时候在socket编程的时候,会出现读取字符串不完整或者乱码的现象。

接收双方收发数据的时候直接在这样一条连接中进行,TCP是面向字节流的协议,数据像是在管道中流动一样。在TCP看来,数据之间并没有明确的边界。

TCP并没有包这一概念,而所谓的包可能是报文段或者,发送端一次发送的数据被误称为包。而粘包的现象主要表现在两方面:
1、发送端在发送数据的时候,为了避免频繁发送负载量极小的报文段导致的传输性价比低的问题,默认使用优化算法,在收集多个小的报文之后,在适当的条件一次发送。由于TCP发送的数据没有边界,发送方发送的数据就看起来像粘在一起形成一个包一样。
2、接收端在接受数据的时候,由于缓存的存在,并不会直接把接受的数据直接移交上层应用层。而是会考虑时间和缓存容量从缓存中读取数据,如果TCP接收数据包的缓存的速度大于应用层从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接像是粘到一起的包。

粘”包”问题也并不是一直都需要解决,如果发送方发送的多组数据本来就是同一块数据的不同部分,比如说一个文件被分成多个部分发送,这时则不需要处理粘包现象。当时更多的情况下,发送的多个数据包并不相关,则需要去解决粘包问题。

比如甲和乙要进行通信,甲先后给乙发送大小为200字节和100字节的数据包A和B。如果将数据包A分为a1和a2两个负载量更小的数据包,那么这两个数据包之间就不存在粘包问题,因为它们本来就属于同一组数据。但是由于是顺序发送的,a2和B就可能产生粘包问题,发送端应用层知道A和B的边界,但是对于接收端TCP接受的是字节流,所以乙的应用层并不知道要把哪些作为一个有效的数据包。

所以粘包根本问题还是在于,TCP是面向字节流的,而字节流是没有边界的。因此要解决粘包问题就要发送端和接收端约定某个字节作为数据包的边界或者规定固定大小的数据作为一个包。放在了上层应用层来实现。

方案一:结束标志控制。以指定字符(串)为包的结束标志,这种协议就是接收端在字节流中每次遇到标志符号,比如”\r\n”就认为是一个包的末尾,把每次遇到”\r\n”之前的数据进行封装当做一个数据包。但是有时候发送的数据本身就携带这些标志字符,因此需要做转义,以免接收方误地当成包结束标志而错误的进行数据打包。
方案二:固定数据包长度。就是每次发送的数据包的长度固定,如果数据不够,需要用特殊填充填满数据包。如果过长,则需要分包。
方案三:包头包体格式数据(TLV:Type, Lenght, Value),也就是发送方接收方事先约定好,每个包由包头和数据负载部分组成。包头长度固定,包含数据类型和数据长度,这两个字段占用的长度固定,假设分别为4个字节,数据负载部分占用的长度由包头中数据长度字段的值决定。比如一个数据包如下,那么接收端的先接收到8个字节的数据就取出包头,从而得到数据的类型,知道数据的长度为个字节,依次从接下来的数据流中读取10个字节,就可以得到该数据包的完整内容。

Type(消息类型) Length(数据部分的字节长度) Value(Data实际的数据部分) 1 10(4+6) asdf大小

上述例子假设采用UTF-8编码,一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。

无法解析?
那会不会出现个别字节的丢失,导致某些数据包的包头无法解析,从而错误封包呢?
至少在发送端和发送过程中不会,因为TCP是可靠通信,可以通过序列号和重传机制保证数据包有序并且正确的到达接收端。

二、Golang代码实现

基于上述方案三,代码实现采用的是发送端和接收端两方约定好数据(消息)的封包和拆包机制,那么接收方发送的时候按照消息头(消息ID或者消息类型+消息长度)和消息实体部分发送,接收方按照同样的格式读取,从消息头中读取消息类型和消息长度,然后从管道中读取消息长度的字节数。

先定义数据打包工具的抽象接口

/*
定义一个解决TCP粘包问题的封包和拆包的模块
——针对Message进行TLV格式的封装
  ——先后Message的长度,ID和内容
——这对Message进行TLV格式的拆包
  ——先读取固定长度的head-->消息内容长度和消息的类型
  ——再根据消息的长度,进行一次读写,从conn中读取消息的内容
 */

//封包,拆包模块,直接面向TCP连接中的数据流,用于处理TCP粘包的问题

type IDataPack interface {
    // 获取包的长度
    GetHeadLen() uint32
    //封包方法
    Pack(msg IMessage) ([]byte, error)
    //拆包方法
    Unpack([]byte) (IMessage, error)
}

数据封装成消息

//消息包含消息ID,消息长度,消息内容三部分
type Message struct {
    Id      uint32 //消息的ID
    DataLen uint32    // 消息长度
    Data    []byte //消息内容
}

//创建一个Message消息包
func NewMsgPackage(id uint32, data []byte) *Message{
    return &Message{
        Id: id,
        DataLen: uint32(len(data)),
        Data: data,
    }
}

//获取消息ID
func (m *Message) GetMessageID() uint32{
    return m.Id
}

//获取消息内容
func (m *Message) GetMessageData() []byte{
    return m.Data
}

//获取消息长度
func (m *Message) GetMessageLen() uint32{
    return m.DataLen
}

//设置消息相关
func (m *Message) SetMessageID(id uint32){
    m.Id = id
}

//设置消息相关
func (m *Message) SetMessageData(data []byte){
    m.Data = data
}

//设置消息长度
func (m *Message) SetMessageLen(length uint32){
    m.DataLen = length
}

具体的拆包和封包逻辑实现

//拆包封包的具体模块
type DataPack struct {
    dataHeadLen uint32
}

//拆包封包实例的初始化方法
func NewDataPack() *DataPack {
    return &DataPack{}
}

// 获取包的长度
func (dp *DataPack) GetHeadLen() uint32{
    //DataHeadLen:uint32(4个字节)+ID:uint32(4个字节)=8个字节
    return 8
}

//封包方法, Message结构体变成二进制序列化的格式数据
func (dp *DataPack) Pack(msg *IMessage) ([]byte, error){
    //创建一个存放bytes字节的缓冲
    dataBuff := bytes.NewBuffer([]byte{})

    //注意写入的顺序
    //将dataLen写入databuff中
    //这里涉及到一个网络传输的大小端问题,大端序,小端序
    if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetMessageLen()); err !=nil{
        return nil, err
    }

    //将MessageID写入databuff中
    if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetMessageID()); err !=nil{
        return nil, err
    }

    //将data写入databuff中
    if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetMessageData()); err !=nil{
        return nil, err
    }

    //二进制的序列化返回
    return dataBuff.Bytes(), nil
}

//拆包方法()
func (dp *DataPack) Unpack(binaryData []byte) (*Message, error){
    //创建一个输入二进制数据的ioReader
    dataBuff := bytes.NewBuffer(binaryData)

    //接受消息,直解压head,获得datalen和id
    msg := &Message{}

    //读取dataLen
    //这里的&msg.DataLen是为了写入地址
    if err := binary.Read(dataBuff, binary.LittleEndian, &msg.DataLen); err!=nil{
        return nil, err
    }

    //这里的从dataBuff读取数据,应该是连续读,先读len,然后读id,不会重复
    //读取dataID
    if err := binary.Read(dataBuff, binary.LittleEndian, &msg.Id); err != nil{
        return nil, err
    }

    //这里还可以加一个判断datalen是否超出定义的长度的逻辑

    //只需拆包湖区msg的head,然后通过head的长度,从conn中读取一次数据
    return msg, nil
}

封包拆包的时候还涉及到大小端的问题,具体是指,一个字符需要多个字节才能表示,在内存中这些字节是按照从大到小的地址空间存储还是从小到大。发送接收双方事先约定好,否则就会不同的顺寻着对接收数据的解析顺序不同出错。还有从Socket中读取数据流的时候是按照顺序的,因此一旦读出来socket中就没了。

其他:具体的建立socket链接,创建数组接收数据就不写了= _ =…, 博客仅作为学习笔记的记录,如果说的不对,及时改正,轻喷轻喷,感谢感谢

三、参考

Original: https://www.cnblogs.com/welan/p/15522972.html
Author: weilanhanf
Title: TCP粘”包”问题浅析及解决方案Golang代码实现

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/516532/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 为开源项目 go-gin-api 增加后台任务模块

    任务管理界面 (WEB) 任务调度器 任务执行器 小结 推荐阅读 任务管理界面 (WEB) 支持在 WEB 界面 中对任务进行管理,例如…

    Go语言 2023年5月25日
    0113
  • 盘点Go中的开发神器

    本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star。 在Java中,我们用Junit做单元测试,用JMH做性能基准测试(benc…

    Go语言 2023年5月25日
    084
  • Go语言程序记录日志

    许多软件系统运行中需要日志文件。Go语言程序中,输出日志需要使用包”log”,编写程序十分简单。 像Java语言程序,输出日志时,往往需要使用开源的软件包来…

    Go语言 2023年5月29日
    044
  • go语言调用everything的SDK接口

    介绍 官方SDK地址 本项目会将官方dll编译到可执行程序中,运行时无需考虑dll问题。 根据官方介绍,使用SDK前需要运行 everything程序。 执行 go build -…

    Go语言 2023年5月25日
    083
  • Go语言之数组与切片基础

    数组是同一类型元素的集合,可以放多个值,但是类型一致,内存中连续存储 Go 语言中不允许混合不同类型的元素,而且数组的大小,在定义阶段就确定了,不能更改 1、数组的定义 // 定义…

    Go语言 2023年5月25日
    083
  • 惨,给Go提的代码被批麻了

    hello大家好,我是小楼。 不知道大家还记不记得我上次找到了一个Go的Benchmark执行会超时的Bug?就是这篇文章《我好像发现了一个Go的Bug?》。 之后我就向Go提交了…

    Go语言 2023年5月25日
    083
  • muduo源码分析之回调模块

    这次我们主要来说说 muduo库中大量使用的回调机制。 muduo主要使用的是利用 Callback的方式来实现回调,首先我们在自己的 EchoServer构造函数中有这样几行代码…

    Go语言 2023年5月25日
    045
  • go语言异常处理

    go语言异常处理 go语言引入了一个关于错误错里的标准模式,即error接口,该接口的定义如下: type error interface{ Error() string } 对于…

    Go语言 2023年5月29日
    066
  • [grpc快速入门] 一 grpc生成与调用

    下载通用编译器 地址:https://github.com/protocolbuffers/protobuf/releases选择对应的版本,解压后将文件夹下bin目录配置到环境变…

    Go语言 2023年5月25日
    087
  • 服务注册与发现的原理和实现

    什么是服务注册发现? 对于搞微服务的同学来说,服务注册、服务发现的概念应该不会太陌生。 简单来说,当服务A需要依赖服务B时,我们就需要告诉服务A,哪里可以调用到服务B,这就是服务注…

    Go语言 2023年5月25日
    060
  • 读 Go 源码,可以试试这个工具

    原文链接: 读 Go 源码,可以试试这个工具 编程发展至今,从面向过程到面向对象,再到现在的面向框架。写代码变成了一件越来越容易的事情。 学习基础语法,看看框架文档,几天时间搞出一…

    Go语言 2023年5月25日
    050
  • go语言 函数return值的几种情况

    分三种情况 (以下 “指定返回值”这句话, 仅指return后面直接跟着的返回值) 退出执行,不指定返回值 *(1) 函数没有返回值 package mai…

    Go语言 2023年5月29日
    089
  • Go – 如何编写 ProtoBuf 插件 (三) ?

    上篇文章《Go – 如何编写 ProtoBuf 插件 (二) 》,分享了基于 自定义&#x90…

    Go语言 2023年5月25日
    069
  • 关于Golang的学习路线

    基础 安装golang环境Golang基础,流程控制,函数,方法,面向对象网络编程(自己做一个简单的tcp的聊天室,websocket,http,命令行工具)并发(可以看一下并发爬…

    Go语言 2023年5月25日
    069
  • 使用Go搭建并行排序处理管道笔记

    go;collapse:true;;gutter:true; package main</p> <p>import ( "bufio" …

    Go语言 2023年5月25日
    060
  • Go语言实现线程安全访问队列

    这个例子用Go语言的包”container/list”实现一个线程安全访问的队列。其中不少细节耐人寻味,做出它则是花费了不少精力,找不到样例啊! Go语言的…

    Go语言 2023年5月29日
    051
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球