[C#]Socket TCP粘包接收處理

  • 10032
  • 0
  • C#
  • 2017-11-27

 TCP粘包處理經驗分享

近日有幸使用Socket實作一Protocol,該Protocol使用的是TCP/IP的傳輸方式,當Protocol實作完成後進行測試發現當Client端每次提交數據的間隔很短或是Server端接收延遲時,TCP/IP就會將多個封包合併成一個後一次拋向Server,當被合併起來的封包大小大於Server接收數據的緩衝區時可能會造成Server服務異常,就這次實作的經驗與之前接觸過的,輕則數據接收異常,重則CPU使用率衝破天際線,系統Crash

什麼是「粘包」?

我們先來看第一張圖,在一切正常美好的情況下數據的傳送與回應會是下圖這樣:

Client向Server發送數據,Server向數據流讀取128Byte的資料,這樣是能完整的拿到封包資料的,當然Server就能完成收完封包後續的流程

接著來看看發生粘包時數據傳送的情況:

當Server發生接收延遲或Client每次提交的數據不大且間隔很短,封包就會被合成一個封包一次送至Server,這時候Server就會數據流讀取128Byte的資料,可想而知的是Server會開始進行封包拆解得到2個完整的封包,然後進行接下來的邏輯流程

接著我們再看看另一張發生粘包的圖:

這次有3個封包被合起來一次提交至Server,然後Server就會數據流讀取128Byte的資料接續進行封包拆解,但這次送過來共有150個Byte,後面32Byte的封包沒有完整接收,這樣就會導致封包接收不完全的問題

 

為什麼會發生粘包?

1. 發送端需要等緩沖區滿才發送出去,造成粘包  (Nalge算法造成的粘包現象)

日常生活的Nalge算法的概念可以想像成飲料店外送服務;假設飲料店有200個外送員,負責製作飲料的店員每做完1杯飲料就請外送員開車去外送給顧客,但20年前的路比較小條,所以一下子整個馬路就被外送車給塞爆了,結果經過了半天飲料也還在路上 ; 每一杯飲料都要一台車子去外送,這樣的傳輸效率是非常低落的,為了解決這樣的問題,所以發明了Nagle算法

如果將每30分鐘內所有訂單做完後由一台車外送或是30分鐘內接滿20杯飲料就立馬外送,這樣就不用1杯飲料跑一趟了,飲料的獲利也就很快地回到了飲料店裡 :D

※Nagle算法是時代的產物 ->因當時網路頻寬有限,而現在網路頻寬寬裕很多,所以目前的TCP/IP協議Deafult是將Nagle算法關閉

2.  接收方不及時接收緩沖區的包,造成多個包接收 


我們該怎麼處理粘包的問題呢? 

Google了很久大致上找到2種「解決方案」

1.  接收數據時給定超大緩衝區:

接受數據時給定一個超大緩衝區就是把上面圖片的128Byte加大! 例如我給定409600Byte.....

好處是只需要一次將數據流的資料讀出來,接著交由程式的遞迴方法將大封包拆解成一個個完整的小封包,但這是有缺點的:

  • 無法100%肯定緩衝區一定完整接收粘包數據,因為粘包數據會有多大是不可知的,還是有機會大於緩衝取
  • 接收端因為給定非常大的緩衝區,付出的代價就是接收端要承載大量資源的耗

2. 給每個封包加上一固定長度的Header,Header內容包含整個完整的封包長度:

這種方式的好處就是假定Header固定16Byte,接收端每次只需要先讀取16Byte,接著先解析這16Byte的標頭來知道接下來該讀取多少數據為一個完整封包,好處即接收端不需要配置非常大的緩衝區、每次讀取的數據都是已知的長度,可以避免緩衝區不夠大的問題,缺點還是有的:在數據接收的效能上比上一種方式慢了一點,但多少則見仁見智這要看使用情境了


接著來看一段簡易的Sample Code

程式採非同步發送與接收,在IAsyncResult 參數會特別帶一個變數來區分現在讀取的數據是否為Header,如果是Header則解析完Header取的完整封包長度,接著再將剩餘的Body數據讀取出來,讀取完之後就能開始進行邏輯處理了

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Socke.Test
{
    class Program
    {
        static void Main(string[] args)
        {
            TcpClient tcpClient = new TcpClient();
            tcpClient.NoDelay = true;  //關閉Nagle演算法
            SocketTempMode state = new SocketTempMode();
            state.TcpClientObj = tcpClient;
            tcpClient.BeginConnect(IPAddress.Parse("192.168.1.103"), 6666, new AsyncCallback(endConnectCallback), state);
        }

        private static void endConnectCallback(IAsyncResult ar)
        {
            SocketTempMode state = (SocketTempMode)ar.AsyncState;
            TcpClient tcpClient = state.TcpClientObj;
            state.RecvData = new PacketModel();
            tcpClient.EndConnect(ar);
            if (tcpClient.Connected){
                NetworkStream networkStream = tcpClient.GetStream();
                byte[] tempRecvBuffer = new byte[16];
                if (networkStream.CanRead){
                    //接收Server的回應數據
                    networkStream.BeginRead(tempRecvBuffer, 0, 16, new AsyncCallback(endReadCallback), new object[] { state, tempRecvBuffer, true });
                }

                if (networkStream.CanWrite){
                    //發送數據至Server
                    byte[] buffer = Encoding.UTF8.GetBytes("你好Server");
                    networkStream.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(endWriteCallback), state);
                }
            }
        }

        private static void endWriteCallback(IAsyncResult ar)
        {
            SocketTempMode state = (SocketTempMode)ar.AsyncState;
            TcpClient tcpClient = state.TcpClientObj;
            NetworkStream networkStream = tcpClient.GetStream();
            networkStream.EndWrite(ar);
        }

        /// <summary>
        /// 這邊讀取數據流數據
        /// </summary>
        /// <param name="ar"></param>
        private static void endReadCallback(IAsyncResult ar)
        {
            object[] objArr = (object[])ar.AsyncState;
            SocketTempMode state = (SocketTempMode)objArr[0];
            byte[] recvBuffer = (byte[])objArr[1];
            bool isHeader = (bool)objArr[2];

            TcpClient tcpClient = state.TcpClientObj;
            int packetLength = 0;
            NetworkStream networkStream = tcpClient.GetStream();
            packetLength = networkStream.EndRead(ar);
            if (packetLength > 0){
                if (isHeader){
                    #region 假定這邊已完成解析Header數據,Header長度的值為85
                    state.RecvData.Header.Length = 85;
                    #endregion
                    Array.Resize(ref recvBuffer, state.RecvData.Header.Length);
                    networkStream.BeginRead(recvBuffer, 16, (recvBuffer.Length - 16), new AsyncCallback(endReadCallback), new object[] { state, recvBuffer, false });
                }else{

                    #region 這邊已拿到完整的封包數據(是包含Header的),可以進行邏輯處理了!

                    #endregion

                    //拿完Body後接著拿Header
                    Array.Resize(ref recvBuffer, 16);
                    networkStream.BeginRead(recvBuffer, 0, 16, new AsyncCallback(endReadCallback), new object[] { state, recvBuffer, true });
                }
            }
        }
    }

    class SocketTempMode
    {
        public TcpClient TcpClientObj { get; set; }
        public PacketModel RecvData { get; set; }
    }
    class PacketModel
    {
        public Header Header { get; set; }
        public string Body { get; set; }
    }
    class Header
    {
        public int Length { get; set; }
    }

}

Reference : 

https://tools.ietf.org/html/rfc896

https://msdn.microsoft.com/zh-tw/library/system.net.sockets.tcpclient.nodelay(v=vs.110).aspx

如果您是實作某一項Protocol,Protocol可能已經幫您定義好Header,那就可以直接使用Header的Length屬性

假設是自行規劃的介接功能,則可自行定義Header 來處理粘包的問題

如果您也剛好第一次處理Socket採TCP/IP的方式傳輸資料,希望這一篇可以對您有所幫助 ,如有錯誤也歡迎指導 

謝謝 :D

egan2608@gmail.com