每一個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; }