《Unity 3D游戏客户端基础框架》 protobuf网络框架

前言:

        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脚本来定义我们的协议。例如:

 

 
  1. // 物品

  2. message Item

  3. {

  4. required int32 Type = 1; //游戏物品大类

  5. optional int32 SubType = 2; //游戏物品小类

  6. required int32 num = 3; //游戏物品数量

  7. }

  8.  
  9. // 物品列表

  10. message ItemList

  11. {

  12. repeated Item item = 1; //物品列表

  13. }

        上述例子中,Item相当于定义了一个数据结构或者是类,而ItemList是一个列表,列表中的每个元素都是一个Item对象。注意结构关键词:

 

 

  • required:必有的属性
  • optional:可选属性
  • repeated:数组

        其实protobuf在这里只是提供了一个数据载体,通过在.proto中定义数据结构之后,需要使用与导表时一样的操作,步骤为:

 

 

  • 使用protoc.exe将.proto文件转化为.protodesc中间格式;
  • 使用protogen.exe将中间格式为.protodesc生成指定的高级语言类,我们在Unity中使用的是C#,所以结果是.cs类

        经过上述步骤之后,我们得到了协议类型对应的C#反序列化类,当我们收到服务器数据时,根据协议号找到协议类型,从而使用对应的反序列化的类对数据进行反序列化,得到最终的服务器数据内容。

 

        在这里,我们以登录为例,首先要清楚登录需要几个数据,正常情况下至少包含两个数据,即账号和密码,都是字符串类型,即定义cs_login.proto协议脚本,内容如下:

 
  1. package cs;

  2.  
  3. message CSLoginInfo

  4. {

  5. required string UserName = 1;//账号

  6. required string Password = 2;//密码

  7. }

  8.  
  9. //发送登录请求

  10. message CSLoginReq

  11. {

  12. required CSLoginInfo LoginInfo = 1;

  13. }

  14. //登录请求回包数据

  15. message CSLoginRes

  16. {

  17. required uint32 result_code = 1;

  18. }

       package关键字后面的名称为.proto转为.cs之后的命名空间namespace的值,用message可以定义类,这里定义了一个CSLoginInfo的数据类,该类包含了账号和密码两个字符串类型的属性。然后定义了两个消息结构:

  • CSLoginReq登录请求消息,携带的数据是一个CSLoginInfo类型的对象数据;
  • CSLoginRes登录请求服务器返回的数据类型,返回结果是一个uint32无符号的整型数据,即结果码。

        上面定义的是协议类型,除此之外我们还需要为每一个协议类型定义一个协议号,这里可以用一个枚举脚本cs_enum.proto来保存,脚本内容为:

 
  1. package cs;

  2.  
  3. enum EnmCmdID

  4. {

  5. CS_LOGIN_REQ = 10001;//登录请求协议号

  6. CS_LOGIN_RES = 10002;//登录请求回包协议号

  7. }

        使用protoc.exe和protogen.exe将这两个protobuf脚本得到C#类,具体步骤参考导表使用的操作,这里我直接给出自动化导表使用的批处理文件general_all.bat内容,具体文件目录可以根据自己放置情况进行调整:

 
  1. ::---------------------------------------------------

  2. ::第二步:把proto翻译成protodesc

  3. ::---------------------------------------------------

  4. call proto2cs\protoc protos\cs_login.proto --descriptor_set_out=cs_login.protodesc

  5. call proto2cs\protoc protos\cs_enum.proto --descriptor_set_out=cs_enum.protodesc

  6. ::---------------------------------------------------

  7. ::第二步:把protodesc翻译成cs

  8. ::---------------------------------------------------

  9. call proto2cs\ProtoGen\protogen -i:cs_login.protodesc -o:cs_login.cs

  10. call proto2cs\ProtoGen\protogen -i:cs_enum.protodesc -o:cs_enum.cs

  11. ::---------------------------------------------------

  12. ::第二步:把protodesc文件删除

  13. ::---------------------------------------------------

  14. del *.protodesc

  15.  
  16. pause

        转换结束后,我们的得到了两个.cs文件分别是:cs_enum.cs和cs_login.cs,将其放入到我们的Unity项目中,以便于接下来序列化和反序列化数据的使用。

 

 

2.协议数据构建:

        直接在项目代码中通过usingcs引入协议解析类的命名空间,然后创建消息对象,并对对象的属性进行赋值,即可得到协议数据对象,例如登录请求对象的创建如下:

 

 
  1. CSLoginInfo mLoginInfo = new CSLoginInfo();

  2. mLoginInfo.UserName = "linshuhe";

  3. mLoginInfo.Password = "123456";

  4. CSLoginReq mReq = new CSLoginReq();

  5. mReq.LoginInfo = mLoginInfo;

        从上述代码,可以得到登录请求对象mReq,里面包含了一个CSLoginInfo对象mLoginInfo,再次枚举对象中找到与此协议类型对应的协议号,即:EnmCmdID.CS_LOGIN_REQ

 

 

3.数据的序列化和反序列化:

        数据发送的时候必须以数据流的形式进行,所以这里我们需要考虑如何将要发送的protobuf对象数据进行序列化,转化为byte[]字节数组,这就需要借助ProtoBuf库为我们提供的Serializer类的Serialize方法来完成,而反序列化则需借助Deserialize方法,将这两个方法封装到PackCodec类中:

 

 
  1. using UnityEngine;

  2. using System.Collections;

  3. using System.IO;

  4. using System;

  5. using ProtoBuf;

  6.  
  7. /// <summary>

  8. /// 网络协议数据打包和解包类

  9. /// </summary>

  10. public class PackCodec{

  11. /// <summary>

  12. /// 序列化

  13. /// </summary>

  14. /// <typeparam name="T"></typeparam>

  15. /// <param name="msg"></param>

  16. /// <returns></returns>

  17. static public byte[] Serialize<T>(T msg)

  18. {

  19. byte[] result = null;

  20. if (msg != null)

  21. {

  22. using (var stream = new MemoryStream())

  23. {

  24. Serializer.Serialize<T>(stream, msg);

  25. result = stream.ToArray();

  26. }

  27. }

  28. return result;

  29. }

  30.  
  31. /// <summary>

  32. /// 反序列化

  33. /// </summary>

  34. /// <typeparam name="T"></typeparam>

  35. /// <param name="message"></param>

  36. /// <returns></returns>

  37. static public T Deserialize<T>(byte[] message)

  38. {

  39. T result = default(T);

  40. if (message != null)

  41. {

  42. using (var stream = new MemoryStream(message))

  43. {

  44. result = Serializer.Deserialize<T>(stream);

  45. }

  46. }

  47. return result;

  48. }

  49. }

        使用方法很简单,直接传入一个数据对象即可得到字节数组:

 

 

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

 

        为了检验打包和解包是否匹配,我们可以直接做一次本地测试:将打包后的数据直接解包,看看数据是否与原来的一致:

 

 
  1. using UnityEngine;

  2. using System.Collections;

  3. using System;

  4. using cs;

  5. using ProtoBuf;

  6. using System.IO;

  7.  
  8. public class TestProtoNet : MonoBehaviour {

  9.  
  10. // Use this for initialization

  11. void Start () {

  12. CSLoginInfo mLoginInfo = new CSLoginInfo();

  13. mLoginInfo.UserName = "linshuhe";

  14. mLoginInfo.Password = "123456";

  15. CSLoginReq mReq = new CSLoginReq();

  16. mReq.LoginInfo = mLoginInfo;

  17.  
  18. byte[] pbdata = PackCodec.Serialize(mReq);

  19. CSLoginReq pReq = PackCodec.Deserialize<CSLoginReq>(pbdata);

  20. Debug.Log("UserName = " + pReq.LoginInfo.UserName + ", Password = " + pReq.LoginInfo.Password);

  21. }

  22.  
  23. // Update is called once per frame

  24. void Update () {

  25.  
  26. }

  27. }

 

        将此脚本绑到场景中的相机上,运行得到以下结果,则说明打包和解包完全匹配:
        

 

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类型的数据:

 

 
  1. /// <summary>

  2. /// 数据转换,网络发送需要两部分数据,一是数据长度,二是主体数据

  3. /// </summary>

  4. /// <param name="message"></param>

  5. /// <returns></returns>

  6. private static byte[] WriteMessage(byte[] message)

  7. {

  8. MemoryStream ms = null;

  9. using (ms = new MemoryStream())

  10. {

  11. ms.Position = 0;

  12. BinaryWriter writer = new BinaryWriter(ms);

  13. ushort msglen = (ushort)message.Length;

  14. writer.Write(msglen);

  15. writer.Write(message);

  16. writer.Flush();

  17. return ms.ToArray();

  18. }

  19. }

 

       协议号:用于对应解析类,这里我们使用的是int类型的数据:

 
  1. private byte[] CreateData(int typeId,IExtensible pbuf)

  2. {

  3. byte[] pbdata = PackCodec.Serialize(pbuf);

  4. ByteBuffer buff = new ByteBuffer();

  5. buff.WriteInt(typeId);

  6. buff.WriteBytes(pbdata);

  7. return buff.ToBytes();

  8. }

        客户端发送登录数据时测试脚本TestProtoNet如下,测试需要将此脚本绑定到当前场景的相机上:

 

 
  1. using UnityEngine;

  2. using System.Collections;

  3. using System;

  4. using cs;

  5. using Net;

  6. using ProtoBuf;

  7. using System.IO;

  8.  
  9. public class TestProtoNet : MonoBehaviour {

  10.  
  11. // Use this for initialization

  12. void Start () {

  13.  
  14.  
  15. CSLoginInfo mLoginInfo = new CSLoginInfo();

  16. mLoginInfo.UserName = "linshuhe";

  17. mLoginInfo.Password = "123456";

  18. CSLoginReq mReq = new CSLoginReq();

  19. mReq.LoginInfo = mLoginInfo;

  20.  
  21. byte[] data = CreateData((int)EnmCmdID.CS_LOGIN_REQ, mReq);

  22. ClientSocket mSocket = new ClientSocket();

  23. mSocket.ConnectServer("127.0.0.1", 8088);

  24. mSocket.SendMessage(data);

  25. }

  26.  
  27. private byte[] CreateData(int typeId,IExtensible pbuf)

  28. {

  29. byte[] pbdata = PackCodec.Serialize(pbuf);

  30. ByteBuffer buff = new ByteBuffer();

  31. buff.WriteInt(typeId);

  32. buff.WriteBytes(pbdata);

  33. return WriteMessage(buff.ToBytes());

  34. }

  35.  
  36. /// <summary>

  37. /// 数据转换,网络发送需要两部分数据,一是数据长度,二是主体数据

  38. /// </summary>

  39. /// <param name="message"></param>

  40. /// <returns></returns>

  41. private static byte[] WriteMessage(byte[] message)

  42. {

  43. MemoryStream ms = null;

  44. using (ms = new MemoryStream())

  45. {

  46. ms.Position = 0;

  47. BinaryWriter writer = new BinaryWriter(ms);

  48. ushort msglen = (ushort)message.Length;

  49. writer.Write(msglen);

  50. writer.Write(message);

  51. writer.Flush();

  52. return ms.ToArray();

  53. }

  54. }

  55.  
  56. // Update is called once per frame

  57. void Update () {

  58.  
  59. }

  60. }

        服务器接受数据解包过程参考打包数据的格式,在RecieveMessage(object clientSocket)中,解析数据的核心代码如下:

 

 

 
  1. ByteBuffer buff = new ByteBuffer(result);

  2. int datalength = buff.ReadShort();

  3. int typeId = buff.ReadInt();

  4. byte[] pbdata = buff.ReadBytes();

  5. //通过协议号判断选择的解析类

  6. if(typeId == (int)EnmCmdID.CS_LOGIN_REQ)

  7. {

  8. CSLoginReq clientReq = PackCodec.Deserialize<CSLoginReq>(pbdata);

  9. string user_name = clientReq.LoginInfo.UserName;

  10. string pass_word = clientReq.LoginInfo.Password;

  11. Console.WriteLine("数据内容:UserName={0},Password={1}", user_name, pass_word);

  12. }

  13. }

        上面通过typeId来找到匹配的数据解析类,协议少的时候可以使用这种简单的使用if语句分支判断来实现,但是假如协议类型多了,则需要进一步封装查找方法,常用的方法有:定义一个Dictionary<int,Type>字典来存放协议号(int)和协议类型(Type)的对应关系。

 

 

6.运行结果:

        启动服务器,然后运行Unity中的客户端,得到正确的结果应该如下:

        

        项目服务器和客户端的完整代码可以前往此处下载:protobuf-net网络协议的定制

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值