Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Unity3D —— protobuf網絡框架

Unity3D —— protobuf網絡框架

編輯:關於Android編程

前言:

protobuf是google的一個開源項目,主要的用途是:

1.數據存儲(序列化和反序列化),這個功能類似xml和json等;

2.制作網絡通信協議;

一、資源下載:

1.github源碼地址:https://github.com/mgravell/protobuf-net

2.google項目源碼下載地址(訪問需翻牆):https://code.google.com/p/protobuf-net/

二、數據存儲:

C#語言方式的導表和解析過程,在之前的篇章中已經有詳細的闡述:Unity —— protobuf 導excel表格數據,建議在看後續的操作之前先看一下這篇文檔,因為後面設計到得一些操作與導表中是一致的,而且在理解了導表過程之後,能夠快速地理解協議數據序列化和反序列化的過程。

三、網絡協議:

1.設計思想:

有兩個必要的數據:協議號和協議類型,將這兩個數據分別存儲起來

當客戶端向服務器發送數據時,會根據協議類型加上協議號,然後使用protobuf序列化之後再發送給服務器; 當服務器發送數據給客戶端時,根據協議號,用protobuf根據協議類型反序列化數據,並調用相應回調方法。

由於數據在傳輸過程中,都是以數據流的形式存在的,而進行解析時無法單從protobuf數據中得知使用哪個解析類進行數據反序列化,這就要求我們在傳輸protobuf數據的同時,攜帶一個協議號,通過協議號和協議類型(解析類)之間的對應關系來確定進行數據反序列化的解析類。

\

此處協議號的作用就是用來確定用於解析數據的解析類,所以也可能稱之為協議類型名,可以是string和int類型的數據。

2.特點分析:

使用protobuf作為網絡通信的數據載體,具有幾個優點:

通過序列化之後數據量比較小; 而且以key-value的方式存儲數據,這對於消息的版本兼容比較強; 此外,由於protobuf提供的多語言支持,所以使用protobuf作為數據載體定制的網絡協議具有很強的跨語言特性。

四、樣例實現:

1.協議定義:

在之前導表的時候,我們得到了.proto的解析類,這是protobuf提供的一種特殊的腳本,具有格式簡單、可讀性強和方便拓展的特點,所以接下來我們就是使用proto腳本來定義我們的協議。例如:

// 物品
message Item
{
    required int32 Type 	= 1;	//游戲物品大類
    optional int32 SubType 	= 2;	//游戲物品小類
    required int32 num 		= 3;	//游戲物品數量
}

// 物品列表
message ItemList
{
    repeated Item item 	= 1;	//物品列表
}
上述例子中,Item相當於定義了一個數據結構或者是類,而ItemList是一個列表,列表中的每個元素都是一個Item對象。注意結構關鍵詞:

 

 

required:必有的屬性 optional:可選屬性 repeated:數組 其實protobuf在這裡只是提供了一個數據載體,通過在.proto中定義數據結構之後,需要使用與導表時一樣的操作,步驟為:

 

 

使用protoc.exe將.proto文件轉化為.protodesc中間格式; 使用protogen.exe將中間格式為.protodesc生成指定的高級語言類,我們在Unity中使用的是C#,所以結果是.cs類 經過上述步驟之後,我們得到了協議類型對應的C#反序列化類,當我們收到服務器數據時,根據協議號找到協議類型,從而使用對應的反序列化的類對數據進行反序列化,得到最終的服務器數據內容。   在這裡,我們以登錄為例,首先要清楚登錄需要幾個數據,正常情況下至少包含兩個數據,即賬號和密碼,都是字符串類型,即定義cs_login.proto協議腳本,內容如下:
package cs;

message CSLoginInfo
{
	required string UserName = 1;//賬號
	required string Password = 2;//密碼
}

//發送登錄請求
message CSLoginReq
{
	required CSLoginInfo LoginInfo = 1; 
}
//登錄請求回包數據
message CSLoginRes
{
	required uint32 result_code = 1; 
}
package關鍵字後面的名稱為.proto轉為.cs之後的命名空間namespace的值,用message可以定義類,這裡定義了一個CSLoginInfo的數據類,該類包含了賬號和密碼兩個字符串類型的屬性。然後定義了兩個消息結構: CSLoginReq登錄請求消息,攜帶的數據是一個CSLoginInfo類型的對象數據; CSLoginRes登錄請求服務器返回的數據類型,返回結果是一個uint32無符號的整型數據,即結果碼。 上面定義的是協議類型,除此之外我們還需要為每一個協議類型定義一個協議號,這裡可以用一個枚舉腳本cs_enum.proto來保存,腳本內容為:
package cs;

enum EnmCmdID
{
	CS_LOGIN_REQ = 10001;//登錄請求協議號
	CS_LOGIN_RES = 10002;//登錄請求回包協議號
}
使用protoc.exe和protogen.exe將這兩個protobuf腳本得到C#類,具體步驟參考導表使用的操作,這裡我直接給出自動化導表使用的批處理文件general_all.bat內容,具體文件目錄可以根據自己放置情況進行調整:
::---------------------------------------------------
::第二步:把proto翻譯成protodesc
::---------------------------------------------------
call proto2cs\protoc protos\cs_login.proto --descriptor_set_out=cs_login.protodesc
call proto2cs\protoc protos\cs_enum.proto --descriptor_set_out=cs_enum.protodesc
::---------------------------------------------------
::第二步:把protodesc翻譯成cs
::---------------------------------------------------
call proto2cs\ProtoGen\protogen -i:cs_login.protodesc -o:cs_login.cs
call proto2cs\ProtoGen\protogen -i:cs_enum.protodesc -o:cs_enum.cs
::---------------------------------------------------
::第二步:把protodesc文件刪除
::---------------------------------------------------
del *.protodesc

pause
轉換結束後,我們的得到了兩個.cs文件分別是:cs_enum.cs和cs_login.cs,將其放入到我們的Unity項目中,以便於接下來序列化和反序列化數據的使用。

 

 

2.協議數據構建:

直接在項目代碼中通過using cs引入協議解析類的命名空間,然後創建消息對象,並對對象的屬性進行賦值,即可得到協議數據對象,例如登錄請求對象的創建如下:

 

        CSLoginInfo mLoginInfo = new CSLoginInfo();
        mLoginInfo.UserName = "linshuhe";
        mLoginInfo.Password = "123456";
        CSLoginReq mReq = new CSLoginReq();
        mReq.LoginInfo = mLoginInfo;
從上述代碼,可以得到登錄請求對象mReq,裡面包含了一個CSLoginInfo對象mLoginInfo,再次枚舉對象中找到與此協議類型對應的協議號,即:EnmCmdID.CS_LOGIN_REQ

 

 

3.數據的序列化和反序列化:

數據發送的時候必須以數據流的形式進行,所以這裡我們需要考慮如何將要發送的protobuf對象數據進行序列化,轉化為byte[]字節數組,這就需要借助ProtoBuf庫為我們提供的Serializer類的Serialize方法來完成,而反序列化則需借助Deserialize方法,將這兩個方法封裝到PackCodec類中:

 

using UnityEngine;
using System.Collections;
using System.IO;
using System;
using ProtoBuf;

/// 
/// 網絡協議數據打包和解包類
/// 
public class PackCodec{
    /// 
    /// 序列化
    /// 
    /// 
    ///
    /// 
    static public byte[] Serialize(T msg)
    {
        byte[] result = null;
        if (msg != null)
        {
            using (var stream = new MemoryStream())
            {
                Serializer.Serialize(stream, msg);
                result = stream.ToArray();
            }
        }
        return result;
    }

    /// 
    /// 反序列化
    /// 
    /// 
    ///
    /// 
    static public T Deserialize(byte[] message)
    {
        T result = default(T);
        if (message != null)
        {
            using (var stream = new MemoryStream(message))
            {
                result = Serializer.Deserialize(stream);
            }
        }
        return result;
    }
}
使用方法很簡單,直接傳入一個數據對象即可得到字節數組:

 

 

        byte[] buf = PackCodec.Serialize(mReq);

 

為了檢驗打包和解包是否匹配,我們可以直接做一次本地測試:將打包後的數據直接解包,看看數據是否與原來的一致:

 

using UnityEngine;
using System.Collections;
using System;
using cs;
using ProtoBuf;
using System.IO;

public class TestProtoNet : MonoBehaviour {

	// Use this for initialization
	void Start () {
        CSLoginInfo mLoginInfo = new CSLoginInfo();
        mLoginInfo.UserName = "linshuhe";
        mLoginInfo.Password = "123456";
        CSLoginReq mReq = new CSLoginReq();
        mReq.LoginInfo = mLoginInfo;

        byte[] pbdata = PackCodec.Serialize(mReq);
        CSLoginReq pReq = PackCodec.Deserialize(pbdata);
        Debug.Log("UserName = " + pReq.LoginInfo.UserName + ", Password = " + pReq.LoginInfo.Password);
	}

    // Update is called once per frame
    void Update () {
	
	}
}

 

將此腳本綁到場景中的相機上,運行得到以下結果,則說明打包和解包完全匹配:
\

 

4.數據發送和接收:

這裡我們使用的網絡通信方式是Socket的強聯網方式,關於如何在Unity中使用Socket進行通信,可以參考我之前的文章:Unity —— Socket通信(C#),Unity客戶端需要復制此項目的ClientSocket.cs和ByteBuffer.cs兩個類到當前項目中。

此外,服務器可以參照之前的方式搭建,唯一不同的是RecieveMessage(object clientSocket)方法解析數據的過程需要進行修改,因為需要使用protobuf-net.dll進行數據解包,所以需要參考客戶端的做法,把protobuf-net.dll復制到服務器項目中的Protobuf_net目錄下:

\
假如由於直接使用源碼而不用.dll會出現不安全保存,需要在Visual Studio中設置允許不安全代碼,具體步驟為:在“解決方案”中選中工程,右鍵“數據”,選擇“生成”頁簽,勾選“允許不安全代碼”:

\

 

當然,解析數據所用的解析類和協議號兩個腳本cs_login.cs和cs_enum.cs也應該添加到服務器項目中,保證客戶端和服務器一直,此外PackCodec.cs也需要添加到服務器代碼中但是要把其中的using UnityEngine給去掉防止報錯,最終服務器目錄結構如下:

\

 

 

5.完整協議數據的封裝:

從之前說過的設計思路分析,我們在發送數據的時候除了要發送關鍵的protobuf數據之外,還需要帶上兩個附件的數據:協議頭(用於進行通信檢驗)和協議號(用於確定解析類)。假設我們的是:

協議頭:用於表示後面數據的長度,一個short類型的數據:

 

        /// 
        /// 數據轉換,網絡發送需要兩部分數據,一是數據長度,二是主體數據
        /// 
        ///
        /// 
        private static byte[] WriteMessage(byte[] message)
        {
            MemoryStream ms = null;
            using (ms = new MemoryStream())
            {
                ms.Position = 0;
                BinaryWriter writer = new BinaryWriter(ms);
                ushort msglen = (ushort)message.Length;
                writer.Write(msglen);
                writer.Write(message);
                writer.Flush();
                return ms.ToArray();
            }
        }

 

協議號:用於對應解析類,這裡我們使用的是int類型的數據:

        private byte[] CreateData(int typeId,IExtensible pbuf)
    {
        byte[] pbdata = PackCodec.Serialize(pbuf);
        ByteBuffer buff = new ByteBuffer();
        buff.WriteInt(typeId);
        buff.WriteBytes(pbdata);
        return buff.ToBytes();
    }
客戶端發送登錄數據時測試腳本TestProtoNet如下,測試需要將此腳本綁定到當前場景的相機上:

 

using UnityEngine;
using System.Collections;
using System;
using cs;
using Net;
using ProtoBuf;
using System.IO;

public class TestProtoNet : MonoBehaviour {

	// Use this for initialization
	void Start () {


        CSLoginInfo mLoginInfo = new CSLoginInfo();
        mLoginInfo.UserName = "linshuhe";
        mLoginInfo.Password = "123456";
        CSLoginReq mReq = new CSLoginReq();
        mReq.LoginInfo = mLoginInfo;

        byte[] data = CreateData((int)EnmCmdID.CS_LOGIN_REQ, mReq);
        ClientSocket mSocket = new ClientSocket();
        mSocket.ConnectServer("127.0.0.1", 8088);
        mSocket.SendMessage(data);
    }

    private byte[] CreateData(int typeId,IExtensible pbuf)
    {
        byte[] pbdata = PackCodec.Serialize(pbuf);
        ByteBuffer buff = new ByteBuffer();
        buff.WriteInt(typeId);
        buff.WriteBytes(pbdata);
        return WriteMessage(buff.ToBytes());
    }

    /// 
    /// 數據轉換,網絡發送需要兩部分數據,一是數據長度,二是主體數據
    /// 
    ///
    /// 
    private static byte[] WriteMessage(byte[] message)
    {
        MemoryStream ms = null;
        using (ms = new MemoryStream())
        {
            ms.Position = 0;
            BinaryWriter writer = new BinaryWriter(ms);
            ushort msglen = (ushort)message.Length;
            writer.Write(msglen);
            writer.Write(message);
            writer.Flush();
            return ms.ToArray();
        }
    }

    // Update is called once per frame
    void Update () {
	
	}
}
服務器接受數據解包過程參考打包數據的格式,在RecieveMessage(object clientSocket)中,解析數據的核心代碼如下:

 

 

        ByteBuffer buff = new ByteBuffer(result);
        int datalength = buff.ReadShort();
        int typeId = buff.ReadInt();
        byte[] pbdata = buff.ReadBytes();
        //通過協議號判斷選擇的解析類
        if(typeId == (int)EnmCmdID.CS_LOGIN_REQ)
        {
                CSLoginReq clientReq = PackCodec.Deserialize(pbdata);
                string user_name = clientReq.LoginInfo.UserName;
                string pass_word = clientReq.LoginInfo.Password;
                Console.WriteLine("數據內容:UserName={0},Password={1}", user_name, pass_word);
                }
        }
上面通過typeId來找到匹配的數據解析類,協議少的時候可以使用這種簡單的使用if語句分支判斷來實現,但是假如協議類型多了,則需要進一步封裝查找方法,常用的方法有:定義一個Dictionary字典來存放協議號(int)和協議類型(Type)的對應關系。

 

 

6.運行結果:

啟動服務器,然後運行Unity中的客戶端,得到正確的結果應該如下:

\

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved