目录

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. String字符串位置移动

    有规律的String字符串位置移动 1.自定义一个有规律的String字符串 String numstr = "1,2,3,x,y,4,5"; 2.按逗号拆分numstr字符串 S ...

  2. Androidstudio实现一个简易的加法器——分享两种方法实现(日常作业练习)

    Androidstudio实现一个简易的加法器——分享两种方法实现(日常作业练习)                                                           ...

  3. 使用ajax提交登录

    引入jquery-1.10.2.js或者jquery-1.10.2.min.js 页面 <h3>后台系统登录</h3> <form name="MyForm&q ...

  4. python第一次作业

    import turtle turtle.pensize(2) turtle.pencolor("black") turtle.fillcolor("red") ...

  5. vector的初始化方式及用法笔记(不断更新)

    vector的初始化方式 1)第一种,类似于数组的方式:vector<string> letter(3);letter[0] = "find";letter[1] = ...

  6. php中的进程

    pcntl扩展:主要的进程扩展,完成进程创建于等待操作. posix扩展:完成posix兼容机通用api,如获取进程id,杀死进程等. sysvmsg扩展:实现system v方式的进程间通信之消息队 ...

  7. Spark实战--寻找5亿次访问中,访问次数最多的人

    问题描述 对于一个大型网站,用户访问量尝尝高达数十亿.对于数十亿是一个什么样的概念,我们这里可以简单的计算一下.对于一个用户,单次访问,我们通常会记录下哪些数据呢? 1.用户的id 2.用户访问的时间 ...

  8. 从火箭发场景来学习Java多线程并发闭锁对象

    从火箭发场景来学习Java多线程并发闭锁对象 倒计时器场景 在我们开发过程中,有时候会使用到倒计时计数器.最简单的是:int size = 5; 执行后,size—这种方式来实现.但是在多线程并发的情 ...

  9. IPv4地址表示法详解

    在TCP/IP协议中,IP地址是一个最基本的概念,本文就来参考<计算机网络>谢希仁 这本书,总结一下IPv4地址表示法的发展阶段,做个读书笔记. IP地址的编址方法共经过了三个历史阶段: ...

  10. Spring扩展:替换IOC容器中的Bean组件 -- @Replace注解

    1.背景:     工作中是否有这样的场景?一个软件系统会同时有多个不同版本部署,比如我现在做的IM系统,同时又作为公司的技术输出给其他银行,不同的银行有自己的业务实现(比如登陆验证.用户信息查询等) ...