解析網絡包

摘要:做網絡應用,封包,解包是家常便飯,但如何做到準確、穩定而且性能好,卻不太容易做到,這次和大家分享一下我在解析網絡包上的經驗。

思路:設計一個網絡協議,一般都會分包,一個包就相當於一個邏輯上的命令。
1、如果我們用udp協議,省事的多,一次會收到一個完整的包,但UDP不可靠,順序也不能保證,當然像QQ對UDP封裝的很好,模擬了TCP的可靠性。網上也有一些封裝好的可靠的UDP組件,大家用的話可以找找。關於用什麼協議好這個問題,本貼不討論。
2、如果我們用TCP協議不是長連接,像HTTP(不考慮KeepAlive)那樣,一個連接上只發送一個包,我們也會很清晰的區分出接受到的每一個包。
3、還有就是我們還用TCP長連接,但每次發送固定長度的包,如果要發送的數據長度不夠就用\0補齊,如果大於固定長度,就分成幾個發,這個也很簡單實用。
4、再有就是一個包有特定的開始和結尾,比如包頭是<bof>包尾是<eof>,我們在可以從頭讀到尾,並把一個一個的包放入隊列,由處理線程去處理。
5、再有一種就是每個包有固定長度的header,這個header裏包含一個包的長度信息,我們可以先從頭裏讀出長度信息,然後再借着讀這麼長的數據,完了這就是一個包。

關於封包的幾種類型我就想到這麼多,其中的利弊大家一看便知,我就不忽悠了,本文主要介紹最後一種方式,好多網絡協議用的都是這種,包括CMPP協議,我們自己設計協議的時候一般不用像CMPP協議那樣,因爲二進制協議雖然雖然節省網絡流量,但可讀性不好。出問題,抓個包分析起來太麻煩。我們可以用.net自帶的序列功能把要發送的類序列化成XML字符串發送出去,這多好看呀。

由於Socket緩衝區設置及其他的原因,Socket在接受數據的時候有時候不能完整的收到一個包,就是你讀出包的長度後,可能不能一次就讀取這麼多數據。而如果讀個半截兒的包就用UTF8Encoding等來解析,會解析出亂碼的,我們這裏用Encoding.UTF8.GetDecoder()來對包進行成塊兒的解析,它就是用來做這種事情的。

下面就來看一下代碼,代碼的註釋很全,演示了一個包從發到接受、解析的全過程,其中接受的過程沒有一次收全所有的包,而是收了好幾次,但我們最終還是成功的解析了收到的包。

 

public static void UnPack()
{
    
//1、聲明通過socket發送的字符串

    string toSendStringBySocket = "娃娃士大夫%#¥%My name is 蛙蛙王子!!";
    
//2、轉換成utf-8字節數組

    byte[] bsInput = Encoding.UTF8.GetBytes(toSendStringBySocket);

    
//
3、計算要發送的字節數組的長度,並寫到第一塊兒字節數組的開頭
    
//一般協議設計裏都有一個長度的Header,這裏就是寫這個Header

    int inputBytesCount = bsInput.Length;
    
byte[] bs1 = new byte[4 + 3]; //4是一個int的長度,3是底一塊字節數組除了Header剩餘的大小

    Buffer.BlockCopy(BitConverter.GetBytes(inputBytesCount), 0, bs1, 04);

    
//
4、把要發送的字節數組拆分成3塊兒發出去,因爲socket在接受字節數組的時候
    
//
也可能半截半截兒的接收,我們就是要模擬這種效果下的拆包,因爲第一塊包寫了
    
//一個4個字節的Header,而第一塊字節數組長度是7,所以再寫三個字節長度的數據

    int offSet = 0;
    Buffer.BlockCopy(bsInput, offSet, bs1, 
43
);
    offSet 
+= bs1.Length - 4
;

    
//5、寫第二塊兒數據

    byte[] bs2 = new byte[8];
    Buffer.BlockCopy(bsInput, offSet, bs2, 
0
, bs2.Length);
    offSet 
+=
 bs2.Length;

    
//
6、寫第三塊兒數據,我們這裏模擬在最後一塊數據的末尾加一些亂七八糟的數據
    
//這些亂七八糟的數據有可能是下一個包的header。

    byte[] bs3 = new byte[bsInput.Length - offSet + 4];
    Buffer.BlockCopy(bsInput, offSet, bs3, 
0, bsInput.Length -
 offSet);
    Buffer.BlockCopy(
new byte[] 1234 }0, bs3, bs3.Length - 44
);

    
//
7、Socket的接收方在執行BeginReceive函數,並回調函數裏把收到的數據放入一個隊列裏
    
//dotNet的隊列內部就是一個環形數組,這裏直接就當環形緩衝區來用了。

    Queue<byte[]> bufferPool = new Queue<byte[]>();
    bufferPool.Enqueue(bs1);
    bufferPool.Enqueue(bs2);
    bufferPool.Enqueue(bs3);

    
//
8、初始化一些變量準備解包
    
    
//聲明一個字符串緩衝區,大小是你的協議裏規定的最大的包體長度

    char[] chars = new char[256]; 
    
//
定義一個UTF-8的Decoder,它可以成塊的解包,內部自動維護解析狀態
    
//關於它的使用請參考MSDN或者《.net框架設計》

    Decoder d = Encoding.UTF8.GetDecoder();
    
int charLen = 0//定義每次解包返回的字符長度

    int parseBytesCount = 0;  //定義已解包的字節數
    int LenghHeader = 0//定義收到包的長度
    bool needReadLengthHeader = true//是否需要讀取長度的頭

    
int srcOffSet = 0//定義要解析的數據塊的偏移量
    byte[] tempBuffer;
    
//9、當環形緩衝裏有數據的時候就一直解析

    while (bufferPool.Count > 0)
    
{
        
//
10、讀取數據包的長度信息,LengthHeader
        
//
因爲第一塊兒包包含長度信息,所以要先讀出來
        
//讀了長度包後,要把數據庫解析偏移量加4

        if(needReadLengthHeader)
        
{
            LenghHeader 
=
 BitConverter.ToInt32(bs1, parseBytesCount);
            needReadLengthHeader 
= false
;
            srcOffSet 
= 4
;
        }

        
        
//11、從環形緩衝區取出一塊兒數據
        tempBuffer = bufferPool.Dequeue();
        parseBytesCount += tempBuffer.Length-srcOffSet; //
更改已解析的字節數

        
//12、如果已解析的字節數大於數據的長度,那麼只解需要解析的字節

        if (parseBytesCount > LenghHeader)
        
{
            parseBytesCount 
-=
 tempBuffer.Length;
            d.GetChars(tempBuffer, srcOffSet, inputBytesCount 
-
 parseBytesCount, chars, charLen);
            
//這裏記錄下當前的臨時緩衝區已解析到了什麼位置,準備解析下一個包

            srcOffSet = inputBytesCount - parseBytesCount; //
            break
;
        }

        
//13、解析這半拉包
        charLen += d.GetChars(tempBuffer, srcOffSet, tempBuffer.Length-srcOffSet, chars, charLen);
        srcOffSet 
= 0
;
    }


    
string s = new string(chars);
    
//14、通知包處理線程來處理這個包

    Console.WriteLine(s); 
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章