C#使用Protocol Buffer(ProtoBuf)进行Unity中的Socket通信使用U3D
注:在文章最后将附上在U3D客户端,进行通过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;
}
}
}