TCP粘包處理現象及其解決方案——基於NewLife.Net網路庫的管道式幀長粘包處理方法

来源:https://www.cnblogs.com/JerryMouseLi/archive/2020/04/08/12659903.html
-Advertisement-
Play Games

[toc] 1.粘包現象 每個TCP 長連接都有自己的socket緩存buffer,預設大小是8K,可支持手動設置。粘包是TCP長連接中最常見的現象,如下圖 socket緩存中有5幀(或者說5包)心跳數據,包頭即F0 AA 55 0F(十六進位),通過數包頭數據我們確認出來緩存里有5幀心跳包,但是5 ...


目錄

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前端和後端哪個工資高?事實上,兩個都是屬於技術研發崗位,都是高薪有前途的職業,不存在說哪個工資更高些,都基本在一萬到五萬之間,工資的差別主要體現在個人技術上。要問做前端好還是做後端好?其實無論做前端還是做後端,只要有實力,其實差別並不大。主要還是要看你喜歡哪個,適合哪個。 前端工作內容: 前端開發主 ...
  • 非同步調用 非同步 JavaScript的執行環境是 單線程 。 所謂單線程,是指JS引擎中負責解釋和執行JavaScript代碼的線程只有一個,也就是一次只能完成一項任務,這個任務執行完後才能執行下一個,它會「阻塞」其他任務。這個任務可稱為主線程。 非同步模式可以一起執行 多個任務 。 常見的非同步模式有 ...
  • 武漢加油!中國加油! 想必許多學vue的小伙伴想連接資料庫,對數據進行增刪改查吧,奈何不知道怎麼實現。作為一路踩坑的我,為大家帶來我的一些踩坑經歷,水平有限,其中錯誤,望請指正。 前言: 本篇主要講述的是如何把零件湊在一起讓車跑起來,不會去關註如何製造零件,等車跑起來了我們再去瞭解造零件。 先看一下 ...
  • 1.config/index.js 修改 proxyTable proxyTable: { '/api': { target: 'http://shuige.wicp.vip/', //目標介面功能變數名稱 changeOrigin: true, //是否跨域 pathRewrite: { '^/api': ...
  • 現在,幾乎整個互聯網行業都缺前端工程師,不僅是剛起步的創業公司,對上市公司乃至巨頭這個問題也一樣存在。 據統計,國外的前端開發人員和後端開發人員比例約1:1,但是在國內比例卻在1:3以下,Web前端開發職位人才缺口巨大,前端工程師的發展之路十分有“錢”景。 每天,HR 群都有人在吐槽招不到前端工程師 ...
  • 現在用戶對於產品的選擇不僅僅是內容的完善,同時也更加註重產品的體驗以及交互,因此前端開發工程師的重要作用愈發明顯。2019年已經接近一半,很多準備入行前端開發工程師的或者還在猶豫小伙伴們,不知道準備得怎麼樣了呢?今天來給大家講講,在2019年,我們學習前端開發,如何才能高效學會前端開發? 零基礎起步 ...
  • 前言 new關鍵字在實例化獲取對象時都做了什麼?是一道經常出現在前端面試時的問題。如果只是簡單的瞭解new關鍵字是實例化構造函數獲取對象,是萬萬不能夠的。更深入的層級發生了什麼呢?同時面試官想從這道題裡面考察什麼呢?下麵胡哥為各位小伙伴一一來解密。 一、new關鍵字 new關鍵字的作用:通過new關 ...
  • 在上一篇《b2b2c系統jwt許可權源碼分享part1》中和大家分享了b2b2c系統中jwt許可權的基礎設計及源碼,本文繼續和大家分享jwt和spring security整合部分的思路和源碼。 在上一篇文章中已經分享了關鍵的類圖: 如上圖所示,許可權的校驗主要涉及到四個類: AbstractAuthen ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...