跨平台数据通信的选择:Google ProtoBuf

跨平台数据通信的选择:Google ProtoBuf

简称protobuf,google开源项目,是一种数据交换的格式,google 提供了多种语言的实现:php、JavaScript、java、c#、c++、go 和 python等。 由于它是一种二进制的格式,比使用 xml, json 进行数据交换快许多。以上描述太官方不好理解,通俗点来解释一下,就是通过protobuf定义好数据结构生成一个工具类,这个工具类可以把数据封装成二进制数据来进行传输,在另一端收到二进制数据再用工具类解析成正常的数据。

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

  • 数据的存储(序列化和反序列化),类似于xml、json等;
  • 制作网络通信协议。

正宗(Google 自己内部用的)的protobuf支持三种语言:Java 、c++和Pyton,很遗憾的是并不支持.Net 或者 Lua 等语言,但社区的力量是不容忽视的,由于protobuf确实比Json、XML有速度上的优势和使用的方便,并且可以做到向前兼容、向后兼容等众多特点,所以protobuf社区又弄了个protobuf.net的组件并且还支持众多语言。

.net 版的protobuf来源于proto社区,有两个版本。一个版本叫protobuf-net,官方站点:http://code.google.com/p/protobuf-net/ 写法上比较符合c#一贯的写法。另一个版本叫protobuf-csharp-sport ,
官方站点:http://code.google.com/p/protobuf-csharp-port/ 写法上跟java上的使用极其相似,比较遵循Google 的原生态写法,所以做跨平台还是选择第二版本吧。因为你会发现几乎和java的写法没啥两样。

ProtoBuf的原理

Socket通信中,客户端与服务器之间传递的是字节流。而在现实的应用中我们需要传递有一定含义的结构,使得通信的双方都能够识别该结构。实现对象(Class和Struct)Socket传输的关键就在于Class或Struct的序列和反序列化。
 Protobuf是google制定的一种对象序列化格式,开源地址:ProtoBuf:http://code.google.com/p/protobuf/
googleProtobuf的官方开源实现由Java。c++和Python。而在.net下的实现有protobuf-net.(官方站点:http://code.google.com/p/protobuf-net/)而protobuf-net在序列化方面有着出色的性能,效率是.net二进制序列化几倍,而序列化后所占的空间也少于.net二进制序列化;除了以上两个优势外Protobuf有着一个更大的优势就是和其他平台交互的兼容性,在现有大部分流行的语言平台中基本都有Protobuf的实现.因此采用protobuf进行对象序列化是个不错的选择.

使用protobuf协议

定义protobuf协议必须创建一个以.proto为后缀的文件,下面是一个proto脚本的简单例子:

message MyData {
    //个人简介
    optional string resume = 1[default="I'm goodman"];
}
message MyRequest {
    //版本号
    required int32 version = 1;
    //姓名
    required string name = 2;
    //个人网站
    optional string website = 3[default="http://www.apache.org/"];
    //附加数据
    optional bytes data = 4;
}
message MyResponse {
    //版本号
    required int32 version = 1;
    //响应结果
    required int32 result = 2;
}

requied是必须有的字段、optional是可有可无的字段、repeated是可以重复的字段(数组或列表),同时枚举字段都必须给出默认值。
 接下来就可以使用ProgoGen来根据proto脚本生成源代码cs文件了,命令行如下:

 protogen -i:ProtoMyData.proto -o:ProtoMyData.cs -ns:MyProtoBuf
 protogen -i:ProtoMyRequest.proto -o:ProtoMyRequest.cs -ns:MyProtoBuf
 protogen -i:ProtoMyResponse.proto -o:ProtoMyResponse.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: ProtoMyData.proto
namespace MyProtoBuf
{
  [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"MyData")]
  public partial class MyData : global::ProtoBuf.IExtensible
  {
    public MyData() {}
    

    private string _resume = @"I'm goodman";
    [global::ProtoBuf.ProtoMember(1, IsRequired = false, Name=@"resume", DataFormat = global::ProtoBuf.DataFormat.Default)]
    [global::System.ComponentModel.DefaultValue(@"I'm goodman")]
    public string resume
    {
      get { return _resume; }
      set { _resume = value; }
    }
    private global::ProtoBuf.IExtension extensionObject;
    global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
      { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); }
  }
  
}
//------------------------------------------------------------------------------
// <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: ProtoMyRequest.proto
namespace MyProtoBuf
{
  [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"MyRequest")]
  public partial class MyRequest : global::ProtoBuf.IExtensible
  {
    public MyRequest() {}
    
    private int _version;
    [global::ProtoBuf.ProtoMember(1, IsRequired = true, Name=@"version", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)]
    public int version
    {
      get { return _version; }
      set { _version = value; }
    }
    private string _name;
    [global::ProtoBuf.ProtoMember(2, IsRequired = true, Name=@"name", DataFormat = global::ProtoBuf.DataFormat.Default)]
    public string name
    {
      get { return _name; }
      set { _name = value; }
    }

    private string _website = @"http://www.apache.org/";
    [global::ProtoBuf.ProtoMember(3, IsRequired = false, Name=@"website", DataFormat = global::ProtoBuf.DataFormat.Default)]
    [global::System.ComponentModel.DefaultValue(@"http://www.apache.org/")]
    public string website
    {
      get { return _website; }
      set { _website = value; }
    }

    private byte[] _data = null;
    [global::ProtoBuf.ProtoMember(4, IsRequired = false, Name=@"data", DataFormat = global::ProtoBuf.DataFormat.Default)]
    [global::System.ComponentModel.DefaultValue(null)]
    public byte[] data
    {
      get { return _data; }
      set { _data = value; }
    }
    private global::ProtoBuf.IExtension extensionObject;
    global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
      { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); }
  }
  
}
//------------------------------------------------------------------------------
// <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: ProtoMyResponse.proto
namespace MyProtoBuf
{
  [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"MyResponse")]
  public partial class MyResponse : global::ProtoBuf.IExtensible
  {
    public MyResponse() {}
    
    private int _version;
    [global::ProtoBuf.ProtoMember(1, IsRequired = true, Name=@"version", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)]
    public int version
    {
      get { return _version; }
      set { _version = value; }
    }
    private int _result;
    [global::ProtoBuf.ProtoMember(2, IsRequired = true, Name=@"result", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)]
    public int result
    {
      get { return _result; }
      set { _result = value; }
    }
    private global::ProtoBuf.IExtension extensionObject;
    global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
      { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); }
  }
  
}

代码示例

接着将生成的3个.cs文件包含在项目中,代码示例(服务端与客户端)

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MyProtoBuf;
using ProtoBuf;

namespace Tdf.ProtoBufDemo
{
    class Program
    {
        private static readonly ManualResetEvent AllDone = new ManualResetEvent(false);
        static void Main(string[] args)
        {
            // ProtoBufTest.TestMethod2();
            // ProtoBufTest.TestMethod3();
            // TestProtoBuf.TestMethod4();

            BeginDemo();

            Console.ReadLine();
        }

        private static void BeginDemo()
        {
            // 启动服务端
            var server = new TcpListener(IPAddress.Parse("127.0.0.1"), 9527);
            server.Start();
            server.BeginAcceptTcpClient(ClientConnected, server);
            Console.WriteLine("SERVER : 等待数据 ---");

            // 启动客户端
            ThreadPool.QueueUserWorkItem(RunClient);
            AllDone.WaitOne();

            Console.WriteLine("SERVER : 退出 ---");
            server.Stop();
        }

        // 服务端处理
        private static void ClientConnected(IAsyncResult result)
        {
            try
            {
                var server = (TcpListener)result.AsyncState;
                using (var client = server.EndAcceptTcpClient(result))
                using (var stream = client.GetStream())
                {
                    // 获取
                    Console.WriteLine("SERVER : 客户端已连接,读取数据 ---");

                    /*
                     * 服务端接收对象;
                     * 从代码中可以发现protobuf-net已考虑的非常周到,不论是客户端发送对象还是服务端接收对象,均只需一行代码就可实现:
                     * 
                     * proto-buf 使用 Base128 Varints 编码;
                     */
                    var myRequest = Serializer.DeserializeWithLengthPrefix<MyRequest>(stream, PrefixStyle.Base128);

                    // 使用C# BinaryFormatter
                    IFormatter formatter = new BinaryFormatter();
                    var myData = (MyData)formatter.Deserialize(new MemoryStream(myRequest.data));

                    Console.WriteLine($@"SERVER : 获取成功, myRequest.version={myRequest.version}, myRequest.name={myRequest.name}, myRequest.website={myRequest.website}, myData.resume={myData.resume}");

                    // 响应(MyResponse)
                    var myResponse = new MyResponse
                    {
                        version = myRequest.version,
                        result = 99
                    };
                    Serializer.SerializeWithLengthPrefix(stream, myResponse, PrefixStyle.Base128);
                    Console.WriteLine("SERVER : 响应成功 ---");

                    Console.WriteLine("SERVER: 关闭连接 ---");
                    stream.Close();
                    client.Close();
                }
            }
            finally
            {
                AllDone.Set();
            }
        }

        // 客户端请求
        private static void RunClient(object state)
        {
            try
            {
                // 构造MyData
                var myData = new MyData {resume = "我的个人简介"};

                // 构造MyRequest
                var myRequest = new MyRequest
                {
                    version = 1,
                    name = "Bobby",
                    website = "www.apache.org"
                };

                // 使用C# BinaryFormatter
                using (var ms = new MemoryStream())
                {
                    IFormatter formatter = new BinaryFormatter();
                    formatter.Serialize(ms, myData);
                    myRequest.data = ms.GetBuffer();
                    ms.Close();
                }
                Console.WriteLine("CLIENT : 对象构造完毕 ...");

                using (var client = new TcpClient())
                {
                    client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9527));
                    Console.WriteLine("CLIENT : socket 连接成功 ...");

                    using (var stream = client.GetStream())
                    {
                        // 发送,客户端发送对象;
                        Console.WriteLine("CLIENT : 发送数据 ...");
                        ProtoBuf.Serializer.SerializeWithLengthPrefix(stream, myRequest, PrefixStyle.Base128);

                        // 接收
                        Console.WriteLine("CLIENT : 等待响应 ...");
                        var myResponse = ProtoBuf.Serializer.DeserializeWithLengthPrefix<MyResponse>(stream, PrefixStyle.Base128);

                        Console.WriteLine($"CLIENT : 成功获取结果, version={myResponse.version}, result={myResponse.result}");

                        // 关闭
                        stream.Close();
                    }
                    client.Close();
                    Console.WriteLine("CLIENT : 关闭 ...");
                }
            }
            catch (Exception error)
            {
                Console.WriteLine($@"CLIENT ERROR : {error.ToString()}");
            }
        }
    }
}

运行结果:

Tdf.ProtoBuf.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容