編輯:關於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()); } ///服務器接受數據解包過程參考打包數據的格式,在RecieveMessage(object clientSocket)中,解析數據的核心代碼如下:/// 數據轉換,網絡發送需要兩部分數據,一是數據長度,二是主體數據 /// /// ///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 () { } }
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上面通過typeId來找到匹配的數據解析類,協議少的時候可以使用這種簡單的使用if語句分支判斷來實現,但是假如協議類型多了,則需要進一步封裝查找方法,常用的方法有:定義一個Dictionary(pbdata); string user_name = clientReq.LoginInfo.UserName; string pass_word = clientReq.LoginInfo.Password; Console.WriteLine("數據內容:UserName={0},Password={1}", user_name, pass_word); } }
6.運行結果:
啟動服務器,然後運行Unity中的客戶端,得到正確的結果應該如下:
一、搭建Android開發環境准備工作:下載Eclipse、JDK、Android SDK、ADT插件1、安裝和配置JAVA開發環境: ①把准備好的
與滾動視圖(ScrollView)類似的還有一種列表組件(ListView),可以將多個組件加入到ListView之中以達到組件的滾動顯示效果,ListVi
過去的兩天,在項目中,拋棄了ListView, 想試一試RecyclerView, 在用的過程中,遇到了一些問題,比如:如何為RecyclerView添加Header和F
在Android3.0之後,Google對UI導航設計上進行了一系列的改革,其中有一個非常好用的新功能就是引入的ActionBar,他用於取代3.0之前的標題欄,並提供更