跨平台数据通信的选择: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()}");
}
}
}
}
运行结果: