一、粘“包”问题简介

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

1、部分字符和乱码的可能原因

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

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

2、粘“包”的可能原因

TCP并没有包这一概念,而所谓的包可能是报文段或者,发送端一次发送的数据被误称为包。而粘包的现象主要表现在两方面:

1、发送端在发送数据的时候,为了避免频繁发送负载量极小的报文段导致的传输性价比低的问题,默认使用优化算法,在收集多个小的报文之后,在适当的条件一次发送。由于TCP发送的数据没有边界,发送方发送的数据就看起来像粘在一起形成一个包一样。

2、接收端在接受数据的时候,由于缓存的存在,并不会直接把接受的数据直接移交上层应用层。而是会考虑时间和缓存容量从缓存中读取数据,如果TCP接收数据包的缓存的速度大于应用层从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接像是粘到一起的包。

3、粘“包”的发生

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

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

4、解决方案

所以粘包根本问题还是在于,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或者消息类型+消息长度)和消息实体部分发送,接收方按照同样的格式读取,从消息头中读取消息类型和消息长度,然后从管道中读取消息长度的字节数。

1、数据打包接口

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

/*
定义一个解决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)
}

2、消息封装

数据封装成消息

package znet

//消息包含消息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
}

3、封包拆包实现

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

//拆包封包的具体模块
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 ziface.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) (ziface.IMessage, 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链接,创建数组接收数据就不写了= _ =...,博客仅作为学习笔记的记录,如果说的不对,及时改正,轻喷轻喷,感谢感谢

三、参考

粘包问题:详解传送门1

粘包问题:详解传送门2

大小端问题:详解传送门

TCP粘"包"问题浅析及解决方案Golang代码实现的更多相关文章

  1. TCP 粘包问题浅析及其解决方案

    最近一直在做中间件相关的东西,所以接触到的各种协议比较多,总的来说有TCP,UDP,HTTP等各种网络传输协议,因此楼主想先从协议最基本的TCP粘包问题搞起,把计算机网络这部分基础夯实一下. TCP协 ...

  2. 查漏补缺:socket编程:TCP粘包问题和常用解决方案(上)

    1.TCP粘包问题的产生(发送端) 由于TCP协议是基于字节流并且无边界的传输协议,因此很容易产生粘包问题.TCP的粘包可能发生在发送端,也可能发生在接收端.发送端的粘包是TCP协议本身引起的,TCP ...

  3. TCP 粘包 - 拆包问题及解决方案

    目录 TCP粘包拆包问题 什么是粘包 - 拆包问题 为什么存在粘包 - 拆包问题 粘包 - 拆包 演示 粘包 - 拆包 解决方案 方式一: 固定缓冲区大小 方式二: 封装请求协议 方式三: 特殊字符结 ...

  4. 【游戏开发】网络编程之浅谈TCP粘包、拆包问题及其解决方案

    引子 现如今手游开发中网络编程是必不可少的重要一环,如果使用的是TCP协议的话,那么不可避免的就会遇见TCP粘包和拆包的问题,马三觉得haifeiWu博主的 TCP 粘包问题浅析及其解决方案 这篇博客 ...

  5. TCP粘包、拆包

    TCP粘包.拆包 熟悉tcp编程的可能都知道,无论是服务端还是客户端,当我们读取或发送数据的时候,都需要考虑TCP底层的粘包/拆包机制. TCP是一个“流”协议,所谓流就是没有界限的遗传数据.可以想象 ...

  6. TCP粘包处理通用框架--C代码

    说明:该文紧接上篇博文“ linux epoll机制对TCP 客户端和服务端的监听C代码通用框架实现 ”讲来 (1)TCP粘包处理数据结构设计 #define MAX_MSG_LEN 65535 ty ...

  7. Socket编程(4)TCP粘包问题及解决方案

    ① TCP是个流协议,它存在粘包问题 TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的.这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的.T ...

  8. 6行代码解决golang TCP粘包

    转自:https://studygolang.com/articles/12483 什么是TCP粘包问题以及为什么会产生TCP粘包,本文不加讨论.本文使用golang的bufio.Scanner来实现 ...

  9. golang 解决 TCP 粘包问题

    什么是 TCP 粘包问题以及为什么会产生 TCP 粘包,本文不加讨论.本文使用 golang 的 bufio.Scanner 来实现自定义协议解包. 协议数据包定义 本文模拟一个日志服务器,该服务器接 ...

随机推荐

  1. 搞不定 NodeJS 内存泄漏?先从了解垃圾回收开始

    通常来说,内存管理有两种方式,一种是手动管理,一种是自动管理. 手动管理需要开发者自己管理内存,什么时候申请内存空间,什么时候释放都需要小心处理,否则容易形成内存泄漏和指针乱飞的局面.C 语言开发是典 ...

  2. ubuntu系统执行生成密匙命令后,home目录下面没有生成.ssh目录

    ubuntu系统配置git ssh时,执行:ssh-keygen -trsa -C "youremail@example.com",home目录下面没有生成.ssh目录. .ssh ...

  3. phpspider PHP 爬虫

    * 通过composer下载 composer require owner888/phpspider // composer.json { "require": { "o ...

  4. frida的安装教程-配合夜神模拟器

    Frida安装 一.PC端安装 1. 安装frida 默认安装最新版的Frida pip install frida 因为我用的是夜神模拟器,可能不支持最新版,所以下载的之前版本. pip insta ...

  5. CF700E-Cool Slogans【SAM,线段树合并,dp】

    正题 题目链接:https://www.luogu.com.cn/problem/CF700E 题目大意 给出一个字符串\(S\),求一个最大的\(k\)使得存在\(k\)个字符串其中\(s_1\)是 ...

  6. SDA 课程

    课件总览 Slide1:Data Science & Digital Society Slide2:DEDA Digital Economy & Decision Analytics ...

  7. SpringCloud升级之路2020.0.x版-26.OpenFeign的组件

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 首先,我们给出官方文档中的组件结构图: 官方文档中的组件,是以实现功能为维度的,我们这里是 ...

  8. Unity——FSM有限状态机

    FSM有限状态机 一.设计思路 1.共同的状态父类,提供可重写的进入,保持,退出该状态的生命周期方法: 2.状态机,管理所有状态(增删查改),状态机运行方法(Run): 3.在角色控制器中,实例化状态 ...

  9. 题解 「BZOJ3636」教义问答手册

    题目传送门 Description 作为泉岭精神的缔造者.信奉者.捍卫者.传承者,Pear决定印制一些教义问答手册,以满足泉岭精神日益增多的信徒.Pear收集了一些有关的诗选.语录,其中部分内容摘录在 ...

  10. AutoCAD云产品平台ForgeViewer格式离线部署思路分析

    背景 在上一篇博文中CAD图DWG解析WebGIS可视化技术分析总结提到,利用AutoCAD的自有云产品 Autodesk Forge,能在浏览器中渲染 3D 和 2D 模型数据,实现DWG图形的We ...