TCP粘"包"问题浅析及解决方案Golang代码实现
一、粘“包”问题简介
在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代码实现的更多相关文章
- TCP 粘包问题浅析及其解决方案
最近一直在做中间件相关的东西,所以接触到的各种协议比较多,总的来说有TCP,UDP,HTTP等各种网络传输协议,因此楼主想先从协议最基本的TCP粘包问题搞起,把计算机网络这部分基础夯实一下. TCP协 ...
- 查漏补缺:socket编程:TCP粘包问题和常用解决方案(上)
1.TCP粘包问题的产生(发送端) 由于TCP协议是基于字节流并且无边界的传输协议,因此很容易产生粘包问题.TCP的粘包可能发生在发送端,也可能发生在接收端.发送端的粘包是TCP协议本身引起的,TCP ...
- TCP 粘包 - 拆包问题及解决方案
目录 TCP粘包拆包问题 什么是粘包 - 拆包问题 为什么存在粘包 - 拆包问题 粘包 - 拆包 演示 粘包 - 拆包 解决方案 方式一: 固定缓冲区大小 方式二: 封装请求协议 方式三: 特殊字符结 ...
- 【游戏开发】网络编程之浅谈TCP粘包、拆包问题及其解决方案
引子 现如今手游开发中网络编程是必不可少的重要一环,如果使用的是TCP协议的话,那么不可避免的就会遇见TCP粘包和拆包的问题,马三觉得haifeiWu博主的 TCP 粘包问题浅析及其解决方案 这篇博客 ...
- TCP粘包、拆包
TCP粘包.拆包 熟悉tcp编程的可能都知道,无论是服务端还是客户端,当我们读取或发送数据的时候,都需要考虑TCP底层的粘包/拆包机制. TCP是一个“流”协议,所谓流就是没有界限的遗传数据.可以想象 ...
- TCP粘包处理通用框架--C代码
说明:该文紧接上篇博文“ linux epoll机制对TCP 客户端和服务端的监听C代码通用框架实现 ”讲来 (1)TCP粘包处理数据结构设计 #define MAX_MSG_LEN 65535 ty ...
- Socket编程(4)TCP粘包问题及解决方案
① TCP是个流协议,它存在粘包问题 TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的.这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的.T ...
- 6行代码解决golang TCP粘包
转自:https://studygolang.com/articles/12483 什么是TCP粘包问题以及为什么会产生TCP粘包,本文不加讨论.本文使用golang的bufio.Scanner来实现 ...
- golang 解决 TCP 粘包问题
什么是 TCP 粘包问题以及为什么会产生 TCP 粘包,本文不加讨论.本文使用 golang 的 bufio.Scanner 来实现自定义协议解包. 协议数据包定义 本文模拟一个日志服务器,该服务器接 ...
随机推荐
- 浏览器+css基础+选择器+权重+匹配规则
浏览器的组成: shell+内核 shell:用户能看得到的界面就叫shell 内核:渲染rendering引擎和js引擎 现在主流拥有自己开发内核的浏览器:opera现在属于360和昆仑万维 CSS ...
- Java集合框架总览
Java集合 Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射.Collection 接口有 3 种子类型,Lis ...
- Python就业方向
0.WEB开发 Python拥有很多免费数据函数库.免费web网页模板系统.以及与web服务器进行交互的库,可以实现web开发,搭建web框架,目前比较有名气的Python web框架为Django. ...
- 『Python』matplotlib常用图表
这里简要介绍几种统计图形的绘制方法,其他更多图形可以去matplotlib找examples魔改 1. 柱状图 柱状图主要是应用在定性数据的可视化场景中,或是离散数据类型的分布展示.例如,一个本科班级 ...
- [源码解析] PyTorch 流水线并行实现 (3)--切分数据和运行时系统
[源码解析] PyTorch 流水线并行实现 (3)--切分数据和运行时系统 目录 [源码解析] PyTorch 流水线并行实现 (3)--切分数据和运行时系统 0x00 摘要 0x01 分割小批次 ...
- P7518-[省选联考2021A/B卷]宝石【主席树,二分】
正题 题目链接:https://www.luogu.com.cn/problem/P7518 题目大意 给出\(n\)个点的一棵树,每个点上有不大于\(m\)的数字. 然后给出一个长度为\(c\)的各 ...
- 13万字详细分析JDK中Stream的实现原理
前提 Stream是JDK1.8中首次引入的,距今已经过去了接近8年时间(JDK1.8正式版是2013年底发布的).Stream的引入一方面极大地简化了某些开发场景,另一方面也可能降低了编码的可读性( ...
- C++ Primer 查漏补缺 —— C++ 中的各种初始化
初学者在刚开始读 C++ Primer 的时候,总是容易被书中各种初始化搞得头大:默认初始化.列表初始化.值初始化.类内初始值.构造函数初始值列表.new int 和 new int() 的区别... ...
- FastAPI(64)- Settings and Environment Variables 配置项和环境变量
背景 在许多情况下,应用程序可能需要一些外部设置或配置,例如密钥.数据库凭据.电子邮件服务凭据等. 大多数这些设置都是可变的(可以更改),例如数据库 URL,很多可能是敏感数据,比如密码 出于这个原因 ...
- CEF使用过程问题合集
CEF使用过程问题合集 1.Couldn't mmap icu data file 解决方案:检查程序执行目录下是否有icudtl.dat文件,如果没有请从cef的Resources文件夹中复制一份. ...