每一个TCP 长链接都有本身的socket缓存buffer,默认大小是8K,可支持手动设置。粘包是TCP长链接中最多见的现象,以下图html
socket缓存中有5帧(或者说5包)心跳数据,包头即F0 AA 55 0F(十六进制),经过数包头数据咱们确认出来缓存里有5帧心跳包,可是5帧数据彼此头尾相连粘合在了一块儿,这种常见的TCP缓存现象,咱们称之为粘包。java
同一客户端连续发送心跳数据,当TCP服务端还来不及解析(若是解析完会把缓存清掉)。形成了同一缓存数据包的粘合。缓存
当某一时刻发生了网络拥塞,一会以后,忽然网络畅通,TCP服务端收到同一客户端的多个心跳包,多个数据包会在TCP服务端的缓存中进行了粘合。服务器
当服务端由于计算量过大或者其余的缘由,计算缓慢,来不及处理TCP Socket缓存中的数据,多个心跳包(或者其余报文)也会在socket缓存中首尾相连,粘包。网络
总而言之,就是多个数据包在同一个TCP socket缓存中进行了首尾相连现象,即为粘包现象。架构
因为粘包现象存在的客观性,咱们必须人为地在程序逻辑里将其区分,若是不去区分,任由各个数据包进行粘连,有如下几点危害:app
服务端会不断识别为无效包,告诉客户端,客户端会再次上报,所以会增长客户端服务端的运行压力,若是自己运算量很大,则会出现一些异常奔溃现象。框架
无巧不成书,若是错误的粘包,凑巧被服务端进行成功解析,则会进行错误的Handler 处理。这样的错误处理方式危害会超过3.1。socket
若是频率过快,则会出现这种现象,服务器不断识别粘包为无效包,客户端不断上报,以此消耗CPU的占用率。ide
综上,咱们必需要进行TCP的粘包处理,这是软件系统健壮性跟异常处理机制的基础。
规定几个字节为每帧TCP报文的包尾特征(好比4个字节),检索整个socket缓存字节,每当检测到包尾特征字节的时候,就划分报文,以此来正确分割粘包。
特征:须要检测每一个字节,效率较低,适合短报文,若是报文很长则不适合。
与4.1类似,多了包头检测部分。
特征:只需检测第一帧的每一个字节,第二帧只需检测包头部分,适合长报文
根据报文长度偏置值,读第一帧的报文,从粘包中(socket缓存)划分出第一帧正确报文,找第二帧的报文长度,划分第二帧,以此划分到底。
举例:以下长度偏置为5(从0开始计算),即第6,第7字节为报文长度字节。
特征:只需检测报文长度部分,适合长短报文的粘包划分。
Newlife.Net管道架构的设计,参考了java的Netty开源框架,所以大部分Netty的编解码器均可以在此使用。
具体在代码中的表现为
_pemsServer.Add(new StickPackageSplit { Size = 2 });
即将LengthCodec这个编解码器加入到了管道中去,全部的message都会通过LengthCodec这里主要是解码功能,没有进行编码,解码成功后(粘包根据长度划分出多个有效包)推送到OnReceive方法中去。Size = 2表示报文长度是2个字节。
与Net Core 的WEBAPI项目的管道添加,是否发现似曾相识?
app.UseAuthentication(); app.UseRequestLog(); app.UseCors(_defaultCorsPolicyName); app.UseMvc();
管道添加的前后顺序即数据流流经管道的顺序。只是没去追求是先有socket的管道处理机制,仍是http 上下文的管道处理机制。可是道理是相同的。
长度所在位置的偏移地址。默认为5,解释详见4.3。
// // 摘要: // 长度所在位置 public int Offset { get; set; } = 5;
本文讨论长度字节数为2,详见4.3
// // 摘要: // 长度占据字节数,1/2/4个字节,0表示压缩编码整数,默认2 public int Size { get; set; } = 2;
// // 摘要: // 编码,此应用不须要编码,只需解码, // 按长度将粘包划分红多个数据包 // // 参数: // context: // // msg: protected override object Encode(IHandlerContext context, Packet msg) { return msg; }
这里无需编码,故直接返回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; }
实例化长度解码器完成以后,并将其添加到字典中去。
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}", BitConverteToString(pk.ToArray()));
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); }
foreach (var item in list) { Console.WriteLine("粘包处理结果:{0}"BitConverter.ToString(item.ToArray())); }
该方法由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); }
// 摘要: // 长度字段做为头部 // 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); } }
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; }