摘要:做網絡應用,封包,解包是家常便飯,但如何做到準確、穩定而且性能好,卻不太容易做到,這次和大家分享一下我在解析網絡包上的經驗。
思路:設計一個網絡協議,一般都會分包,一個包就相當於一個邏輯上的命令。
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()來對包進行成塊兒的解析,它就是用來做這種事情的。
下面就來看一下代碼,代碼的註釋很全,演示了一個包從發到接受、解析的全過程,其中接受的過程沒有一次收全所有的包,而是收了好幾次,但我們最終還是成功的解析了收到的包。
{
//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, 0, 4);
//4、把要發送的字節數組拆分成3塊兒發出去,因爲socket在接受字節數組的時候
//也可能半截半截兒的接收,我們就是要模擬這種效果下的拆包,因爲第一塊包寫了
//一個4個字節的Header,而第一塊字節數組長度是7,所以再寫三個字節長度的數據
int offSet = 0;
Buffer.BlockCopy(bsInput, offSet, bs1, 4, 3);
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[] { 1, 2, 3, 4 }, 0, bs3, bs3.Length - 4, 4);
//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);
}