目录

1.粘包现象

每个TCP 长连接都有自己的socket缓存buffer,默认大小是8K,可支持手动设置。粘包是TCP长连接中最常见的现象,如下图

socket缓存中有5帧(或者说5包)心跳数据,包头即F0 AA 55 0F(十六进制),通过数包头数据我们确认出来缓存里有5帧心跳包,但是5帧数据彼此头尾相连粘合在了一起,这种常见的TCP缓存现象,我们称之为粘包。

2.粘包原因

2.1. 同一客户端连续发送

同一客户端连续发送心跳数据,当TCP服务端还来不及解析(如果解析完会把缓存清掉)。造成了同一缓存数据包的粘合。

2.2. 网络拥塞造成粘包

当某一时刻发生了网络拥塞,一会之后,突然网络畅通,TCP服务端收到同一客户端的多个心跳包,多个数据包会在TCP服务端的缓存中进行了粘合。

2.3. 服务端卡死了

当服务端因为计算量过大或者其他的原因,计算缓慢,来不及处理TCP Socket缓存中的数据,多个心跳包(或者其他报文)也会在socket缓存中首尾相连,粘包。

总而言之,就是多个数据包在同一个TCP socket缓存中进行了首尾相连现象,即为粘包现象。

3. 粘包的危害

由于粘包现象存在的客观性,我们必须人为地在程序逻辑里将其区分,如果不去区分,任由各个数据包进行粘连,有以下几点危害:

3.1. 无法正确解析数据包

服务端会不断识别为无效包,告诉客户端,客户端会再次上报,因此会增加客户端服务端的运行压力,如果本身运算量很大,则会出现一些异常奔溃现象。

3.2. 错误数据包被错误解析

无巧不成书,如果错误的粘包,凑巧被服务端进行成功解析,则会进行错误的Handler 处理。这样的错误处理方式危害会超过3.1。

3.3. 进入死循环

如果频率过快,则会出现这种现象,服务器不断识别粘包为无效包,客户端不断上报,以此消耗CPU的占用率。

综上,我们必须要进行TCP的粘包处理,这是软件系统健壮性跟异常处理机制的基础。

4. 粘包的逻辑处理方式

4.1. 根据包尾特征参数进行区分

规定几个字节为每帧TCP报文的包尾特征(比如4个字节),检索整个socket缓存字节,每当检测到包尾特征字节的时候,就划分报文,以此来正确分割粘包。

特征:需要检测每个字节,效率较低,适合短报文,如果报文很长则不适合。

4.2. 根据包头包尾特征参数进行区分

与4.1相似,多了包头检测部分。

特征:只需检测第一帧的每个字节,第二帧只需检测包头部分,适合长报文

4.3. 根据报文长度来进行粘包区分

根据报文长度偏置值,读第一帧的报文,从粘包中(socket缓存)划分出第一帧正确报文,找第二帧的报文长度,划分第二帧,以此划分到底。

举例:如下长度偏置为5(从0开始计算),即第6,第7字节为报文长度字节。

特征:只需检测报文长度部分,适合长短报文的粘包划分。

5. 根据报文长度来区分粘包的代码落地——基于NewLife.Net的管道处理

5.1. NewLife.Net管道架构处理方式

Newlife.Net管道架构的设计,参考了java的Netty开源框架,因此大部分Netty的编解码器都可以在此使用。

具体在代码中的表现为

 _pemsServer.Add(new StickPackageSplit { Size = 2 });

即将LengthCodec这个编解码器加入到了管道中去,所有的message都会经过LengthCodec这里主要是解码功能,没有进行编码,解码成功后(粘包根据长度划分出多个有效包)推送到OnReceive方法中去。Size = 2表示报文长度是2个字节。

5.2. 跟http的管道类比

与Net Core 的WEBAPI项目的管道添加,是否发现似曾相识?

  app.UseAuthentication();
app.UseRequestLog();
app.UseCors(_defaultCorsPolicyName);
app.UseMvc();

管道添加的先后顺序即数据流流经管道的顺序。只是没去追求是先有socket的管道处理机制,还是http 上下文的管道处理机制。但是道理是相同的。

5.3.拆分粘包解码器(根据长度解码)

5.3.1. 长度偏移地址Offset属性

长度所在位置的偏移地址。默认为5,解释详见4.3。

        //
// 摘要:
// 长度所在位置
public int Offset
{
get;
set;
} = 5;

5.3.2.长度字节数Size属性

本文讨论长度字节数为2,详见4.3

        //
// 摘要:
// 长度占据字节数,1/2/4个字节,0表示压缩编码整数,默认2
public int Size
{
get;
set;
} = 2;

5.3.3. 编码方法Encode

        //
// 摘要:
// 编码,此应用不需要编码,只需解码,
// 按长度将粘包划分成多个数据包
//
// 参数:
// context:
//
// msg:
protected override object Encode(IHandlerContext context, Packet msg)
{
return msg;
}

这里无需编码,故直接返回msg。

5.3.4. 解码方法Decode

        //
// 摘要:
// 解码
//
// 参数:
// context:
//
// pk:
protected override IList<Packet> Decode(IHandlerContext context, Packet pk)
{
IExtend extend = context.Owner as IExtend; LengthCodec packetCodec = extend["Codec"] as LengthCodec; if (packetCodec == null)
{
IExtend extend2 = extend;
LengthCodec obj = new LengthCodec
{
Expire = Expire,
GetLength = ((Packet p) => MessageCodec<Packet>.GetLength(p, Offset, Size))
};
packetCodec = obj;
extend2["Codec"] = obj;
} Console.WriteLine("报文解码前:{0}", BitConverter.ToString(pk.ToArray()));
IList<Packet> list = packetCodec.Parse(pk);
Console.WriteLine("报文解码");
foreach (var item in list)
{
Console.WriteLine("粘包处理结果:{0}", BitConverter.ToString(item.ToArray()));
} return list;
}

5.3.4.1.解码步骤1——实例化长度解码器对象

实例化长度解码器完成之后,并将其添加到字典中去。

    IExtend extend2 = extend;
LengthCodec obj = new LengthCodec
{
Expire = Expire,
GetLength = ((Packet p) => MessageCodec<Packet>.GetLength(p, Offset, Size))
};
packetCodec = obj;
extend2["Codec"] = obj;

5.3.4.2.解码步骤2——将解码前的报文打印

此步骤非必须,为了最后能让读者看到效果增加。

    Console.WriteLine("报文解码前:{0}", BitConverteToString(pk.ToArray()));

5.3.4.3.解码步骤3——将报文进行解码

 IList<Packet> list = packetCodec.Parse(pk);

解码代码如下:

        //
// 摘要:
// 分析数据流,得到一帧数据
//
// 参数:
// pk:
// 待分析数据包
public virtual IList<Packet> Parse(Packet pk)
{
MemoryStream stream = Stream;
bool num = stream == null || stream.Position < 0 || stream.Position >= stream.Length;
List<Packet> list = new List<Packet>(); if (num)
{ if (pk == null)
{
return list.ToArray();
}
int i;
int num2; for (i = 0; i < pk.Total; i += num2)
{
Packet packet = pk.Slice(i); num2 = GetLength(packet); Console.WriteLine(" pk. GetLength(packet):{0}", num2); if (num2 <= 0 || num2 > packet.Total)
{
break;
}
packet.Set(packet.Data, packet.Offset, num2);
list.Add(packet);
} if (i == pk.Total)
{ return list.ToArray();
}
pk = pk.Slice(i);
} lock (this)
{
CheckCache();
stream = Stream;
if (pk != null && pk.Total > 0)
{
long position = stream.Position;
stream.Position = stream.Length;
pk.CopyTo(stream);
stream.Position = position;
}
while (stream.Position < stream.Length)
{
Packet packet2 = new Packet(stream);
int num3 = GetLength(packet2);
if (num3 <= 0 || num3 > packet2.Total)
{
break;
}
packet2.Set(packet2.Data, packet2.Offset, num3);
list.Add(packet2);
stream.Seek(num3, SeekOrigin.Current);
}
if (stream.Position >= stream.Length)
{
stream.SetLength(0L);
stream.Position = 0L;
} return list;
}
}

解码核心代码如下:

即获得每帧报文的长度,通过委托方法 GetLength(packet),然后循环所有粘包报文,根据每帧报文的长度分割保存到list中去,最后返回list。list的每个元素会触发message接收事件。

委托的使用请敬请关注下一篇,委托代码详见6.

    for (i = 0; i < pk.Total; i += num2)
{
Packet packet = pk.Slice(i); num2 = GetLength(packet); Console.WriteLine(" pk. GetLength(packet):{0}", num2); if (num2 <= 0 || num2 > packet.Total)
{
break;
}
packet.Set(packet.Data, packet.Offset, num2);
list.Add(packet);
}

5.3.4.4.将粘包处理结果进行打印

    foreach (var item in list)
{
Console.WriteLine("粘包处理结果:{0}"BitConverter.ToString(item.ToArray()));
}

5.3.5.清空粘包编码器

该方法由NewLife.Net网络库调用,我们无需关心。

    //
// 摘要:
// 连接关闭时,清空粘包编码器
//
// 参数:
// context:
//
// reason:
public override bool Close(IHandlerContext contextstring reason)
{
IExtend extend = context.Owner as IExtend;
if (extend != null)
{
extend["Codec"] = null;
}
return base.Close(context, reason);
}

5.3.6.完整拆分粘包解码器代码

    // 摘要:
// 长度字段作为头部
//
public class StickPackageSplit : MessageCodec<Packet>
{
//
// 摘要:
// 长度所在位置
public int Offset
{
get;
set;
} = 5; //
// 摘要:
// 长度占据字节数,1/2/4个字节,0表示压缩编码整数,默认2
public int Size
{
get;
set;
} = 2; //
// 摘要:
// 过期时间,超过该时间后按废弃数据处理,默认500ms
public int Expire
{
get;
set;
} = 500; //
// 摘要:
// 编码,此应用不需要编码,只需解码,
// 按长度将粘包划分成多个数据包
//
// 参数:
// context:
//
// msg:
protected override object Encode(IHandlerContext context, Packet msg)
{
return msg;
} //
// 摘要:
// 解码
//
// 参数:
// context:
//
// pk:
protected override IList<Packet> Decode(IHandlerContext context, Packet pk)
{
IExtend extend = context.Owner as IExtend; LengthCodec packetCodec = extend["Codec"] as LengthCodec; if (packetCodec == null)
{
IExtend extend2 = extend;
LengthCodec obj = new LengthCodec
{
Expire = Expire,
GetLength = ((Packet p) => MessageCodec<Packet>.GetLength(p, Offset, Size))
};
packetCodec = obj;
extend2["Codec"] = obj;
} Console.WriteLine("报文解码前:{0}", BitConverter.ToString(pk.ToArray()));
IList<Packet> list = packetCodec.Parse(pk);
Console.WriteLine("报文解码");
foreach (var item in list)
{
Console.WriteLine("粘包处理结果:{0}", BitConverter.ToString(item.ToArray()));
} return list;
} //
// 摘要:
// 连接关闭时,清空粘包编码器
//
// 参数:
// context:
//
// reason:
public override bool Close(IHandlerContext context, string reason)
{
IExtend extend = context.Owner as IExtend;
if (extend != null)
{
extend["Codec"] = null;
}
return base.Close(context, reason);
}
}

6.长度计算委托GetLength

5.3.6中会调用如下每个包的长度计算委托。关于委托的使用方法会在下一篇讲解,这里不再展开。

//
// 摘要:
// 从数据流中获取整帧数据长度
//
// 参数:
// pk:
//
// offset:
//
// size:
//
// 返回结果:
// 数据帧长度(包含头部长度位)
protected static int GetLength(Packet pk, int offsetint size)
{
if (offset < 0)
{
return pk.Total - pk.Offset;
}
int offset2 = pk.Offset;
if (offset >= pk.Total)
{
return 0;
}
int num = 0;
switch (size)
{
case 0:
{
MemoryStream stream = pk.GetStream();
if (offset > 0)
{
stream.Seek(offset, SeekOrigiCurrent);
}
num = stream.ReadEncodedInt();
num += (int)(stream.Position - offset);
break;
}
case 1:
num = pk[offset];
break;
case 2:
num = pk.ReadBytes(offset, 2).ToUInt16();
break;
case 4:
num = (int)pk.ReadBytes(offset, 4).ToUInt32;
break;
case -2:
num = pk.ReadBytes(offset, 2).ToUInt16(0isLittleEndian: false);
break;
case -4:
num = (int)pk.ReadBytes(offset, 4).ToUInt(0, isLittleEndian: false);
break;
default:
throw new NotSupportedException();
}
if (num > pk.Total)
{
return 0;
}
return num;
}

7.最终粘包拆分效果图


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://www.cnblogs.com/JerryMouseLi/p/12659903.html

粘包处理现象及其解决方案——基于NewLife.Net网络库的管道式帧长粘包处理方法的更多相关文章

  1. 使用NewLife网络库构建可靠的自动售货机Socket服务端(一)

    最近有个基于tcp socket 协议和设备交互需求,想到了新生命团队的各种组件,所以决定用NewLife网络库作为服务端来完成一系列的信息交互. 第一,首先说一下我们需要实现的功能需求吧 1,首先客 ...

  2. 区域医疗移动医疗影像解决方案--基于HTML5的PACS--HTML5图像处理【转】

    基于HTML5的PACS--图像伪彩 摘要: 要查看此系统更多的图像处理功能请参考:区域医疗移动医疗影像解决方案--基于HTML5的PACS--HTML5图像处理套用句广告语:哪里不会点哪里,so e ...

  3. 基于tcp协议下粘包现象和解决方案,socketserver

    一.缓冲区 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区.write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送 ...

  4. python 全栈开发,Day35(TCP协议 粘包现象 和解决方案)

    一.TCP协议 粘包现象 和解决方案 黏包现象让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)执行远程命令的模块 需要用到模块subprocess sub ...

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

    一.粘"包"问题简介 在socket网络编程中,都是端到端通信,客户端端口+客户端IP+服务端端口+服务端IP+传输协议就组成一个可以唯一可以明确的标识一条连接.在TCP的sock ...

  6. Netty中粘包和拆包的解决方案

    粘包和拆包是TCP网络编程中不可避免的,无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包和拆包 TCP是个“流”协议,所谓流,就是没有界限的一串 ...

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

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

  8. UNIX网络编程——tcp流协议产生的粘包问题和解决方案

    我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体 ...

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

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

随机推荐

  1. Java8 新特性2——强大的Stream API

    强大的Stream API Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找.过滤和映射数据等操作.简而言之,Stream API 提供 ...

  2. 如何提升.NET控制台应用体验?

    原文:Upgrade Your .NET Console App Experience 作者:Khalid Abuhakmeh 译文:Lamond Lu 在.NET生态系统中,控制台程序的表现相对较差 ...

  3. Redis系列一 - 入门篇

    问:项目中为何要选用Redis? 答:传统的关系型数据库(如MySQL)已经不适用所有的场景了,比如美云销抢单活动的库存扣减,APP首页的访问流量高峰等等,都容易把数据库打崩,所以引入了缓存中间件,目 ...

  4. TP5使用Redis处理电商秒杀

    本篇文章介绍了ThinkPHP使用Redis实现电商秒杀的处理方法,具有一定的参考价值,希望对学习ThinkPHP的朋友有帮助! TP5使用Redis处理电商秒杀 1.首先在TP5中创建抢购活动所需要 ...

  5. 测试必知必会系列- Linux常用命令 - mkdir

    21篇测试必备的Linux常用命令,每天敲一篇,每次敲三遍,每月一循环,全都可记住!! 新建一个文件夹 yyTest mkdir yyTest 新建三个文件夹 yyTest1 yyTest2 yyTe ...

  6. 深入理解JS引擎的执行机制

    深入理解JS引擎的执行机制 1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 2.JS中的event loop(1) 3.JS中的event loop(2) 4 ...

  7. 前端基础面试题(js部分)

      前端基础面试题(JS部分)   1.几种基本数据类型?复杂数据类型?值类型和引用数据类型?堆栈数据结构? 基本数据类型:Undefined.Null.Boolean.Number.String值类 ...

  8. 初识Flask、快速启动

    目录 一.初识Flask 1.1 什么是flask? 1.2 为什么要有flask? 二.Flask快速启动 一.初识Flask 1.1 什么是flask? Flask 本是作者 Armin Rona ...

  9. ES6 第七节 ES6中新增的数组知识(1)

    目录 ES6 第七节 ES6中新增的数组知识(1) 第七节 ES6中新增的数组知识(1) JSON数组格式转换 Array.of()方法: find()实例方法: ES6 第七节 ES6中新增的数组知 ...

  10. vmware导入ova文件踩坑记

    问题来源 众所周知,所有的网络行为都会产生相应的网络流量,那么所有的网络攻击行为也有其对应的流量特点,那么是否能根据流量特点进而分析出其对应的是什么攻击行为呢? 我在虚拟机上使用vulnhub的靶场环 ...