Unity中使用ProtoBuff3.0,與netty服務(wù)器通信的粘包、拆包處理(二)

3. 建立與服務(wù)端通信連接,使用protobuff編碼解碼

廢話少說,先上代碼,注釋的也比較清晰了

using Google.Protobuf;
using Google.Protobuf.Examples.AddressBook;
using Google.Protobuf.WellKnownTypes;
using System;
using System.Net.Sockets;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour {

    // Use this for initialization
    void Start () {

        StartConnect();
    }

    TcpClient tcpClient;                // 
    byte[] receive_buff;                // 專門用來接收Socket里面的數(shù)據(jù)的
    byte[] data_buff;                   // 用來存當(dāng)前未處理的數(shù)據(jù)

    CodedOutputStream outputStream;     // 用來綁定SocketStream,方便把proto對象轉(zhuǎn)換成字節(jié)流Stream輸送給服務(wù)器

    void StartConnect()
    {
        TcpClient client = new TcpClient();
        tcpClient = client;
 
        //這里寫上你自己服務(wù)器的ip和端口
        client.Connect("192.168.1.1", 8800);
        
        receive_buff = new byte[client.ReceiveBufferSize];

        outputStream = new CodedOutputStream(client.GetStream());

        // 監(jiān)聽一波服務(wù)器消息
        client.GetStream().BeginRead(receive_buff, 0, client.ReceiveBufferSize, ReceiveMessage, null);
    }

    int nFalg = 0;        // 這個變量主要是為了防止和服務(wù)端無休無止互發(fā)消息,測試代碼
    void Update () {

        // 因?yàn)镽eceiveMessage接收數(shù)據(jù)是異步的方式,不是在主線程,有些方法不能用,比如ToString,所以消息處理放在這里處理
        // 但主要是因?yàn)楹竺嬉由舷V播,可以添加在這里
        if (data_buff != null && ++nFalg < 5)
        {
            // 把數(shù)據(jù)傳給CodedInputStream計(jì)算本次包的長度
            CodedInputStream inputStream = new CodedInputStream(data_buff);
            int length = inputStream.ReadLength();
            // 計(jì)算"包長度"占用的字節(jié)數(shù),后面取數(shù)據(jù)的時候扣掉這個字節(jié)數(shù),就是真實(shí)數(shù)據(jù)長度
            int lengthLength = CodedOutputStream.ComputeLengthSize(length);

            // 當(dāng)前數(shù)據(jù)足夠解析一個包了
            if (length + lengthLength <= data_buff.Length)
            {
                byte[] real_data = new byte[length];
                // 拷貝真實(shí)數(shù)據(jù)
                Array.Copy(data_buff, lengthLength, real_data, 0, length);

                // 假設(shè)服務(wù)器給你發(fā)了個AddressBook
                AddressBook ab = AddressBook.Parser.ParseFrom(real_data);

                // 把這個數(shù)據(jù)直接還給服務(wù)器,驗(yàn)證客戶端發(fā)給服務(wù)器的情況
                SendMsg(ab);

                // 數(shù)據(jù)剛剛好,沒有多余的
                if (length + lengthLength == data_buff.Length)
                {
                    data_buff = null;
                }
                else
                {
                    // 數(shù)據(jù)有剩余,保存剩余數(shù)據(jù),等下一個Update解析
                    byte[] t = new byte[data_buff.Length - length - lengthLength];
                    Array.Copy(data_buff, lengthLength + length, t, 0, t.Length);
                    data_buff = t;
                }
            }
        }
    }

    // 發(fā)送數(shù)據(jù)
    public void SendMsg(IMessage message)
    {
        if (outputStream != null)
        {
            // WriteMessage 里面會先write一個長度,然后再write真實(shí)數(shù)據(jù)
            outputStream.WriteMessage(message);
            outputStream.Flush();       // 把buffer數(shù)據(jù)寫入到tcpClient的流里面
        }
    }

    public void ReceiveMessage(IAsyncResult ar)
    {
        try
        {
            // 本次接收到的數(shù)據(jù)長度
            int bytesRead = tcpClient.GetStream().EndRead(ar);
            if (bytesRead < 1)
            {
                Debug.LogError("bytesRead < 1");
                return;
            }
            else
            {
                if (data_buff == null)
                {
                    // buff里面沒有數(shù)據(jù)
                    data_buff = new byte[bytesRead];
                    Array.Copy(receive_buff, data_buff, bytesRead);
                }
                else
                {
                    // buff里面有數(shù)據(jù),要和新數(shù)據(jù)整合起來
                    byte[] new_data = new byte[bytesRead + data_buff.Length];
                    Array.Copy(data_buff, new_data, data_buff.Length);

                    Array.Copy(receive_buff, 0, new_data, data_buff.Length, bytesRead);

                    data_buff = new_data;
                }
            }

            // 繼續(xù)監(jiān)聽下一波數(shù)據(jù)
            tcpClient.GetStream().BeginRead(receive_buff, 0, tcpClient.ReceiveBufferSize, ReceiveMessage, null);
        }
        catch (Exception ex)
        {
            // 為了防止報(bào)ex沒被使用的警告
            Debug.Log(ex);
        }
    }
}

4. 處理與Netty服務(wù)器通信的粘包、拆包

服務(wù)器的粘包拆包是Netty本身支持的解碼編碼器,如下圖


服務(wù)器粘包、拆包處理方式

總共四行,其中第一行作用在拆包的時候,第三行作用在粘包的時候(我猜的)。
它這個拆包粘包不是普通的那種固定4個字節(jié)標(biāo)示長度的,而是有時候1個字節(jié),有時候是2、3、4、5個字節(jié),根據(jù)當(dāng)前發(fā)送的真實(shí)數(shù)據(jù)的長度定的。

在普通的方案粘包方案,數(shù)據(jù)是這樣的:4個字節(jié)+真實(shí)數(shù)據(jù)
有的是用換行回車作為標(biāo)識符拆包、粘包

那在Netty的方案里,包長度究竟是幾個字節(jié)呢?
其實(shí)它也是用到了Protobuff里面的數(shù)據(jù)讀取、保存方式,感興趣的可以打開protobuf3-for-unity-3.0.0\src\Google.Protobuf.sln工程看一下,在Google.Protobuf項(xiàng)目中,打開CodedInputStream.cs

SlowReadRawVarint32

包頭占用幾個字節(jié)是由下面這個函數(shù)計(jì)算的:
這個是計(jì)算一個uint數(shù)據(jù)的真實(shí)長度的方法

這也是protobuff對象編碼后數(shù)據(jù)會比較小的主要原因。比如一個對象編碼后得到的是440個字節(jié)數(shù)據(jù),那么調(diào)用ComputeRawVarint32Size(440)的返回值是2,也就是服務(wù)器和客戶端發(fā)送的數(shù)據(jù)最終長度是440+2=442個字節(jié)。明白了這些,拆包和粘包就都不是問題了。

上面的代碼里,粘包是這一段:

public void SendMsg(IMessage message)
    {
        if (outputStream != null)
        {
            // WriteMessage 里面會先write一個長度,然后再write真實(shí)數(shù)據(jù)
            outputStream.WriteMessage(message);
            outputStream.Flush();       // 把buffer數(shù)據(jù)寫入到tcpClient的流里面
        }
    }

乍一看,好像沒有在真實(shí)數(shù)據(jù)前面加長度啊?其實(shí),在outputStream的WriteMessage里面已經(jīng)有WriteLength了,幫我們做好了。

image.png

再看拆包:

            // 把數(shù)據(jù)傳給CodedInputStream計(jì)算本次包的長度
            CodedInputStream inputStream = new CodedInputStream(data_buff);
            int length = inputStream.ReadLength();
            // 計(jì)算"包長度"占用的字節(jié)數(shù),后面取數(shù)據(jù)的時候扣掉這個字節(jié)數(shù),就是真實(shí)數(shù)據(jù)長度
            int lengthLength = CodedOutputStream.ComputeLengthSize(length);

            // 當(dāng)前數(shù)據(jù)足夠解析一個包了
            if (length + lengthLength <= data_buff.Length)
            {
                byte[] real_data = new byte[length];
                // 拷貝真實(shí)數(shù)據(jù)
                Array.Copy(data_buff, lengthLength, real_data, 0, length);

                // 假設(shè)服務(wù)器給你發(fā)了個AddressBook
                AddressBook ab = AddressBook.Parser.ParseFrom(real_data);
                ...
            }

先用CodedInputStream 看看這個“包大小”值是多少,再用CodedOutputStream.ComputeLengthSize計(jì)算這個“包大小”占幾個字節(jié),然后就明白真實(shí)數(shù)據(jù)從哪里開始,占多少字節(jié)了。

結(jié)束語

測試并不是非常非常充分,僅供參考。

參考:http://blog.csdn.net/u010841296/article/details/50957471?locationNum=2&fps=1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容