一、粘“包”问题简介

在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. Java面向对象系列(8)- Super详解

    场景一 场景二 场景三 场景四 注意:调用父类的构造器,super()必须在子类构造器的第一行 场景五 场景六 super注意点 super调用父类得构造方法(即构造器),必须在构造方法得第一个 su ...

  2. Shell系列(37)- while和until循环

    while循环 只要条件判断式成立则进行循环,并执行循环程序:一旦循环条件不成立,则终止循环 格式 while [ 条件判断式 ] do 程序 done 例子 需求:计算工具,1+2+--100的和 ...

  3. Elasticsearch6.8.6版本 在head插件中 对数据的增删改操作

    一.访问ES方法:http://IP:PORT/ 一.创建索引:head插件创建索引的实例:在"索引"-"新建索引"中创建索引名称,默认了分片与副本情况: 直接 ...

  4. 一生挚友redo log、binlog《死磕MySQL系列 二》

    系列文章 原来一条select语句在MySQL是这样执行的<死磕MySQL系列 一> 一生挚友redo log.binlog<死磕MySQL系列 二> 前言 咔咔闲谈 上期根据 ...

  5. 执行:vim /etc/profile,提示:Command 'vim' not found, but can be installed with:

    root@uni-virtual-machine:/# vim /etc/profile Command 'vim' not found, but can be installed with: apt ...

  6. Python小知识之对象的比较

    好久不见 国庆回了趟老家,躺平了10天.作息时间基本和小学生差不多,8.9点就睡了, 那滋味别提多舒服了.时间也和小时候过得一样慢了...长时间不更新,还是不行滴,粉都快掉没了. 今天就结合日常生活的 ...

  7. 从零入门 Serverless | 函数计算的可观测性

    作者 | 夏莞 阿里巴巴函数计算团队 本文整理自<Serverless 技术公开课>,关注"Serverless"公众号,回复"入门",即可获取 S ...

  8. NX开发 刀路生成

    此段是可以生成程序的完整代码,只有从坐标(10,10,10)到(500,500,500)一根刀轨.motion_ptr->feed_value 的值为0时生成G00,非0时生成G01.此代码只有 ...

  9. 关于 我的博客和Git-hub

    欢迎大家到我的GitHub 热烈讨论 https://github.com/ljj-19951010 由于另一个博客忘了怎么登陆了,换用此博客(仅供个人学习使用,请勿传播) 如果想看 特别详细的教程请 ...

  10. Selenium获取动态图片验证码

    Selenium获取动态图片验证码 关于图片验证码的文章,我想大家都有一定的了解了. 在我们做UI自动化的时候,经常会遇到图片验证码的问题. 当开发不给咱们提供万能验证码,或者测试第三方网站比如知乎的 ...