ProtoBuff3 unity_TCP網絡發包解包

using Google.Protobuf;
//using Google.Protobuf.Examples.AddPerson;
using Google.Protobuf.WellKnownTypes;
using System;
using System.Net.Sockets;
using UnityEngine;
using ARProto;
using pb = global::Google.Protobuf;
public class NewBehaviourScript : MonoBehaviour
{


    void OnGUI()
    {
        if (GUI.Button(new Rect(100, 10, 120, 100), new GUIContent("Button", "Go")))
        {
            UIGo();
        }

    }

    public void UIGo()
    {
        Person person =new    Person();
        person.Id=01;
        AddPerson addPerson =new AddPerson();
      SendMsg(addPerson);

    }
    // Use this for initialization
    void Start()
    {

        StartConnect();
    }

    TcpClient tcpClient;                // 
    byte[] receive_buff;                // 專門用來接收Socket裏面的數據的
    byte[] data_buff;                   // 用來存當前未處理的數據

    CodedOutputStream outputStream;     // 用來綁定SocketStream,方便把proto對象轉換成字節流Stream輸送給服務器

    void StartConnect()
    {
        TcpClient client = new TcpClient();
        tcpClient = client;

        //這裏寫上你自己服務器的ip和端口
        client.Connect("106.2.124.243", 9090);

        receive_buff = new byte[client.ReceiveBufferSize];

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

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

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

        // 因爲ReceiveMessage接收數據是異步的方式,不是在主線程,有些方法不能用,比如ToString,所以消息處理放在這裏處理
        // 但主要是因爲後面要加上消息廣播,可以添加在這裏
        if (data_buff != null && ++nFalg < 5)
        {
            // 把數據傳給CodedInputStream計算本次包的長度
            CodedInputStream inputStream = new CodedInputStream(data_buff);
            int length = inputStream.ReadLength();
            // 計算"包長度"佔用的字節數,後面取數據的時候扣掉這個字節數,就是真實數據長度
            int lengthLength = CodedOutputStream.ComputeLengthSize(length);

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

                // 假設服務器給你發了個AddressBook
                AddPerson ab = AddPerson.Parser.ParseFrom(real_data);

                // 把這個數據直接還給服務器,驗證客戶端發給服務器的情況
                 SendMsg(ab);

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

    // 發送數據
    public void SendMsg( pb::IMessage<AddPerson> message)
    {
        if (outputStream != null)
        {
            // WriteMessage 裏面會先write一個長度,然後再write真實數據
            ////     outputStream.WriteMessage(message);
            outputStream.Flush();       // 把buffer數據寫入到tcpClient的流裏面
        }
    }

    public void ReceiveMessage(IAsyncResult ar)
    {
          Debug.Log("消息:" + receive_buff);
        try
        {
            // 本次接收到的數據長度
            int bytesRead = tcpClient.GetStream().EndRead(ar);
            if (bytesRead < 1)
            {
                Debug.LogError("bytesRead < 1");
                return;
            }
            else
            {
                if (data_buff == null)
                {
                    // buff裏面沒有數據
                    data_buff = new byte[bytesRead];
                    Array.Copy(receive_buff, data_buff, bytesRead);
                }
                else
                {
                    // buff裏面有數據,要和新數據整合起來
                    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;
                  
                }
            }

            // 繼續監聽下一波數據
            tcpClient.GetStream().BeginRead(receive_buff, 0, tcpClient.ReceiveBufferSize, ReceiveMessage, null);
        }
        catch (Exception ex)
        {
            // 爲了防止報ex沒被使用的警告
            Debug.Log(ex);
        }
    }
}

 

處理與Netty服務器通信的粘包、拆包

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

 

服務器粘包、拆包處理方式

 

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

在普通的方案粘包方案,數據是這樣的:4個字節+真實數據
有的是用換行回車作爲標識符拆包、粘包

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

SlowReadRawVarint32


包頭佔用幾個字節是由下面這個函數計算的:

這個是計算一個uint數據的真實長度的方法


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

 

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

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

乍一看,好像沒有在真實數據前面加長度啊?其實,在outputStream的WriteMessage裏面已經有WriteLength了,幫我們做好了。

image.png

再看拆包:

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

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

                // 假設服務器給你發了個AddressBook
                AddressBook ab = AddressBook.Parser.ParseFrom(real_data);
                ...
            }

先用CodedInputStream 看看這個“包大小”值是多少,再用CodedOutputStream.ComputeLengthSize計算這個“包大小”佔幾個字節,然後就明白真實數據從哪裏開始,佔多少字節了。

 

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