Protocol Buffer(ProtoBuf)在U3D的使用

C#使用Protocol Buffer(ProtoBuf)进行Unity中的Socket通信使用U3D 


注:在文章最后将附上在U3D客户端,进行通过ProtoBuf进行数据的序列化以及发序列化的代码。

文件下载地址: protobuf


准备:

http://code.google.com/p/protobuf/下载protobuf-2.5版本

预备知识: 已经使用过protobuf, 熟练应用protobuf序列化在各语言间交互信息

目标: 获取proto内容而无需手动解析proto文件

为proto文件添加更多的meta信息, 并在运行期获取.

 

protoc编译器准备

通过protobuf-2.5的源码或者从官网下载, 可以获得protoc的protobuf编译器, 这个编译器由C++编写, 官方支持完整的protobuf特性. 编译器默认支持C++, python和java 三种语言的代码生成. 如需生成更多的语言, 可以通过官网的第三方页面获取.

 

protoc插件原理

但我们在日常使用中, 可能需要提取proto信息, 例如: 所有的枚举,消息等信息, 字段名称和导出号. 自己编写词法解析器来做是费力不讨好的. 官方推荐的方法是使用protoc外挂插件来实现.

protoc的插件设计比较独特, 不使用动态链接库或者java的jar包导入方式, 而是直接使用了命令行来交换数据.查看protobuf源码我们可以发现这样一个文件:

protobuf-2.5.0\src\google\protobuf\descriptor.proto

这个文件描述了一个proto文件的格式, 消息组成及枚举等完整信息. 这是一种自我描述的方法.

在找到这样一个文件

protobuf-2.5.0\src\google\protobuf\compiler\plugin.proto

这样一个文件描述: 插件如何与protoc进行交互的协议

protoc编译器在给定指定proto文件及搜索路径后, 将各种信息填充为descriptor.proto描述的结构后通过CodeGeneratorRequest消息系列化为二进制流后输出到命令行. 插件只用捕获protoc命令行输出的二进制流, 序列化化回CodeGeneratorRequest即可获得解析后的proto文件内容

这里需要注意的是: 插件可执行文件很有讲究, 必须为protoc-gen-$NAME,  而且输出文件名参数必须为--${NAME}_out

看一个栗子:

protoc.exe foo.proto --plugin=protoc-gen-go=..\tools\protoc-gen-go.exe --go_out foo.go --proto_path "."

这个栗子里: $NAME=go

protoc将foo.proto文件(搜索路径为当前路径)的内容通过命令行输出给位于..\tools\的插件protoc-gen-go.exe,  输出文件名字为 foo.go

descriptor.proto信息挖掘

我们注意到在descriptor.proto文件中包含有这样的一个message: SourceCodeInfo, 这个消息体里有如下字段

optional string leading_comments = 3;
    optional string trailing_comments = 4;

这两个字段对于我们获取proto文件的meta信息尤为重要, 所谓的meta信息, 理解理解为C#语言中的attribute

这个attribute功能可以为一个字段, 一个消息扩充一些描述. 比如: 当一个字段通过反射显示在gui上时, gui需要获取这个字段的中文描述

那么只需要如下编写

optional int32 somevalue = 1 //@ desc=”中文描述”

位于字段尾部的描述, 会被填充到SourceCodeInfo的 trailing_comments中, 而位于字段上方的字段, 会被填充到leading_comments中

 

SourceCodeInfo 并没有直接挂载在message或者字段的附近, 而是通过其下的path字段来描述与字段的关系, 这是个极为麻烦的设计.

其原理如下:

假设我有如下一个message

message foo

{

     optional int32 v = 1;  // comments

}

要获取v后的注释, 对应的path为 4, 0, 2, 0

4 表示descriptor中message_type所在的序号,由于message_type对应的类型DescriptorProto是一个数组, 所以0表示foo是在FileDescriptorProto的message_type数组类型的索引为0;

如此类推: 2, 0 表示 v在DescriptorProto结构体的field成员序号为2的数组元素的索引为0

 

如果需要更多的参考, 可以获取https://github.com/golang/protobuf

github.com\golang\protobuf\protoc-gen-go工程内有详细代码解析









protobuf是google的一个开源项目,可用于以下两种用途:

  (1)数据的存储(序列化和反序列化),类似于xml、json等;

  (2)制作网络通信协议。

  源代码下载地址:https://github.com/mgravell/protobuf-net

  开源项目地址如下:https://code.google.com/p/protobuf-net/,下载解压后的目录如下所示,每个文件夹的详细介绍都在最后一个txt文件里面了。

  

  ProtoGen是用来根据***.proto文件生成对应的***.cs文件的,而做数据存储功能只需要用到protobuf-net.dll即可,至于使用哪个版本项目情况决定。下面的例子在Windows平台下新建一个C#的控制台工程,并引入ProtoBufNet\Full\net30\protobuf-net.dll,代码如下所示:

复制代码
namespace TestProtoBuf
{
    [ProtoContract]
    public class Address
    {
        [ProtoMember(1)]
        public string Line1;
        [ProtoMember(2)]
        public string Line2;
    }

    [ProtoContract]
    public class Person
    {
        [ProtoMember(1)]
        public int Id;
        [ProtoMember(2)]
        public string Name;
        [ProtoMember(3)]
        public Address Addr;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person();
            person.Id = 1;
            person.Name = "First";
            person.Addr = new Address { Line1="line1", Line2="line2"};

            // ProtoBuf序列化
            using(var file = System.IO.File.Create("Person.bin"))
            {
                ProtoBuf.Serializer.Serialize(file, person);
            }

            // ProtoBuf反序列化
            Person binPerson = null;
            using(var file = System.IO.File.OpenRead("Person.bin"))
            {
                binPerson = ProtoBuf.Serializer.Deserialize<Person>(file);
            }

            System.Console.WriteLine(binPerson.Name);
        }
    }
}
复制代码

  可以看到序列化和反序列化的代码非常简单。

  protobuf提供了一种proto脚本用来编写***.proto文件,这种脚本格式简单、可读性强、方便扩展,用proto脚本定义网络协议是非常好用的。

  下面是一个proto脚本的简单例子:

复制代码
message Person {
    required string name=1;
    required int32 id=2;
    optional string email=3;

    enum PhoneType {
        MOBILE=0;
        HOME=1;
        WORK=2;
    }

    message PhoneNumber {
        required string number=1;
        optional PhoneType type=2 [default=HOME];
    }

    repeated PhoneNumber phone=4;
}
复制代码

  requied是必须有的字段、optional是可有可无的字段、repeated是可以重复的字段(数组或列表),同时枚举字段都必须给出默认值。

  接下来就可以使用ProgoGen来根据proto脚本生成源代码cs文件了,命令行如下:

  protogen -i:test.proto -0:test.cs -ns:MyProtoBuf

  -i指定了输入,-o指定了输出,-ns指定了生成代码的namespace,上面的proto脚本生成的源码如下:

复制代码
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

// Generated from: file/pb.proto
namespace MyProtoBuf
{
  [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"Person")]
  public partial class Person : global::ProtoBuf.IExtensible
  {
    public Person() {}
    
    private string _name;
    [global::ProtoBuf.ProtoMember(1, IsRequired = true, Name=@"name", DataFormat = global::ProtoBuf.DataFormat.Default)]
    public string name
    {
      get { return _name; }
      set { _name = value; }
    }
    private int _id;
    [global::ProtoBuf.ProtoMember(2, IsRequired = true, Name=@"id", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)]
    public int id
    {
      get { return _id; }
      set { _id = value; }
    }
    private string _email = "";
    [global::ProtoBuf.ProtoMember(3, IsRequired = false, Name=@"email", DataFormat = global::ProtoBuf.DataFormat.Default)]
    [global::System.ComponentModel.DefaultValue("")]
    public string email
    {
      get { return _email; }
      set { _email = value; }
    }
    private readonly global::System.Collections.Generic.List<Person.PhoneNumber> _phone = new global::System.Collections.Generic.List<Person.PhoneNumber>();
    [global::ProtoBuf.ProtoMember(4, Name=@"phone", DataFormat = global::ProtoBuf.DataFormat.Default)]
    public global::System.Collections.Generic.List<Person.PhoneNumber> phone
    {
      get { return _phone; }
    }
  
  [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"PhoneNumber")]
  public partial class PhoneNumber : global::ProtoBuf.IExtensible
  {
    public PhoneNumber() {}
    
    private string _number;
    [global::ProtoBuf.ProtoMember(1, IsRequired = true, Name=@"number", DataFormat = global::ProtoBuf.DataFormat.Default)]
    public string number
    {
      get { return _number; }
      set { _number = value; }
    }
    private Person.PhoneType _type = Person.PhoneType.HOME;
    [global::ProtoBuf.ProtoMember(2, IsRequired = false, Name=@"type", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)]
    [global::System.ComponentModel.DefaultValue(Person.PhoneType.HOME)]
    public Person.PhoneType type
    {
      get { return _type; }
      set { _type = value; }
    }
    private global::ProtoBuf.IExtension extensionObject;
    global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
      { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); }
  }
  
    [global::ProtoBuf.ProtoContract(Name=@"PhoneType")]
    public enum PhoneType
    {
            
      [global::ProtoBuf.ProtoEnum(Name=@"MOBILE", Value=0)]
      MOBILE = 0,
            
      [global::ProtoBuf.ProtoEnum(Name=@"HOME", Value=1)]
      HOME = 1,
            
      [global::ProtoBuf.ProtoEnum(Name=@"WORK", Value=2)]
      WORK = 2
    }
  
    private global::ProtoBuf.IExtension extensionObject;
    global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
      { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); }
  }
  
}

三,U3D客户端代码实现。

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

public class ProtoBufTest : MonoBehaviour {

	// Use this for initialization
	void Start () {

        //使用protobuf创建一个用户信息
        PBMessage.UserInfo user = new PBMessage.UserInfo() { name = "likai", sex = "男", level = 58, integral = 22, ranking = 2 };


        //序列化此条信息
        byte[] temp = Serialize(user);

        Debug.Log("数据长度  :" + temp.Length);

        PBMessage.UserInfo reslut = DeSerialize<PBMessage.UserInfo>(typeof(PBMessage.UserInfo), temp) as PBMessage.UserInfo;

        Debug.Log( string.Format("数据为 :name = {0} , sex = {1}, level = {2}, integral = {3} , ranking = {4}" , reslut.name,reslut.sex,reslut.level,reslut.integral,reslut.ranking));



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

    /// <summary>
    /// 序列化数据
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="data"></param>
    /// <returns></returns>
    byte[] Serialize<T>(T data)   
    {
        try
        {
            using (MemoryStream ms = new MemoryStream())
            {
                new PBMessageSerializer().Serialize(ms,data);  //使用protobuf带的序列化工具

                byte[] result = new byte[ms.Length];      //创建byte数组
                ms.Position = 0;    //设置流当前的位置
                ms.Read (result,0,result.Length);   //读取流对象,将数值写入其中
                return result;    //返回值
            }
        }
        catch(Exception ex)
        {
            Debug.Log("序列化失败 :" + ex.ToString());
            return null;
        }
    }
    
    object DeSerialize<T>(Type data,byte[] msg)
    {
        try
        {
            MemoryStream ms = new MemoryStream();
            ms.Write(msg,0,msg.Length);
            ms.Position = 0;
            //  T resule = new PBMessageSerializer().Serialize.DeSerialize<T>(ms);
            object resule = new PBMessageSerializer().Deserialize(ms,null, data);
            return resule;

            
        }
        catch(Exception ex)
        {
            Debug.Log("反序列化失败 :" + ex.ToString());
            return null;
        }
    }
    

   
}


   



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值