跨平臺數(shù)據(jù)通信的選擇:Google ProtoBuf
簡稱protobuf,google開源項目谭确,是一種數(shù)據(jù)交換的格式帘营,google 提供了多種語言的實現(xiàn):php、JavaScript逐哈、java芬迄、c#、c++昂秃、go 和 python等禀梳。 由于它是一種二進制的格式,比使用 xml, json 進行數(shù)據(jù)交換快許多肠骆。以上描述太官方不好理解算途,通俗點來解釋一下,就是通過protobuf定義好數(shù)據(jù)結(jié)構(gòu)生成一個工具類蚀腿,這個工具類可以把數(shù)據(jù)封裝成二進制數(shù)據(jù)來進行傳輸嘴瓤,在另一端收到二進制數(shù)據(jù)再用工具類解析成正常的數(shù)據(jù)。
protobuf是google的一個開源項目莉钙,可用于以下兩種用途:
- 數(shù)據(jù)的存儲(序列化和反序列化)廓脆,類似于xml、json等磁玉;
- 制作網(wǎng)絡(luò)通信協(xié)議停忿。
正宗(Google 自己內(nèi)部用的)的protobuf支持三種語言:Java 、c++和Pyton蜀涨,很遺憾的是并不支持.Net 或者 Lua 等語言瞎嬉,但社區(qū)的力量是不容忽視的蝎毡,由于protobuf確實比Json、XML有速度上的優(yōu)勢和使用的方便氧枣,并且可以做到向前兼容沐兵、向后兼容等眾多特點,所以protobuf社區(qū)又弄了個protobuf.net的組件并且還支持眾多語言便监。
.net 版的protobuf來源于proto社區(qū)扎谎,有兩個版本。一個版本叫protobuf-net烧董,官方站點:http://code.google.com/p/protobuf-net/ 寫法上比較符合c#一貫的寫法毁靶。另一個版本叫protobuf-csharp-sport ,
官方站點:http://code.google.com/p/protobuf-csharp-port/ 寫法上跟java上的使用極其相似逊移,比較遵循Google 的原生態(tài)寫法预吆,所以做跨平臺還是選擇第二版本吧。因為你會發(fā)現(xiàn)幾乎和java的寫法沒啥兩樣胳泉。
ProtoBuf的原理
Socket通信中拐叉,客戶端與服務(wù)器之間傳遞的是字節(jié)流。而在現(xiàn)實的應(yīng)用中我們需要傳遞有一定含義的結(jié)構(gòu)扇商,使得通信的雙方都能夠識別該結(jié)構(gòu)凤瘦。實現(xiàn)對象(Class和Struct)Socket傳輸?shù)年P(guān)鍵就在于Class或Struct的序列和反序列化。
Protobuf是google制定的一種對象序列化格式,開源地址:ProtoBuf:http://code.google.com/p/protobuf/
googleProtobuf的官方開源實現(xiàn)由Java案铺。c++和Python蔬芥。而在.net下的實現(xiàn)有protobuf-net.(官方站點:http://code.google.com/p/protobuf-net/)而protobuf-net在序列化方面有著出色的性能,效率是.net二進制序列化幾倍,而序列化后所占的空間也少于.net二進制序列化;除了以上兩個優(yōu)勢外Protobuf有著一個更大的優(yōu)勢就是和其他平臺交互的兼容性,在現(xiàn)有大部分流行的語言平臺中基本都有Protobuf的實現(xiàn).因此采用protobuf進行對象序列化是個不錯的選擇.
使用protobuf協(xié)議
定義protobuf協(xié)議必須創(chuàng)建一個以.proto為后綴的文件,下面是一個proto腳本的簡單例子:
message MyData {
//個人簡介
optional string resume = 1[default="I'm goodman"];
}
message MyRequest {
//版本號
required int32 version = 1;
//姓名
required string name = 2;
//個人網(wǎng)站
optional string website = 3[default="http://www.apache.org/"];
//附加數(shù)據(jù)
optional bytes data = 4;
}
message MyResponse {
//版本號
required int32 version = 1;
//響應(yīng)結(jié)果
required int32 result = 2;
}
requied是必須有的字段控汉、optional是可有可無的字段笔诵、repeated是可以重復(fù)的字段(數(shù)組或列表),同時枚舉字段都必須給出默認值暇番。
接下來就可以使用ProgoGen來根據(jù)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文件包含在項目中恨课,代碼示例(服務(wù)端與客戶端)
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()
{
// 啟動服務(wù)端
var server = new TcpListener(IPAddress.Parse("127.0.0.1"), 9527);
server.Start();
server.BeginAcceptTcpClient(ClientConnected, server);
Console.WriteLine("SERVER : 等待數(shù)據(jù) ---");
// 啟動客戶端
ThreadPool.QueueUserWorkItem(RunClient);
AllDone.WaitOne();
Console.WriteLine("SERVER : 退出 ---");
server.Stop();
}
// 服務(wù)端處理
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 : 客戶端已連接舆乔,讀取數(shù)據(jù) ---");
/*
* 服務(wù)端接收對象;
* 從代碼中可以發(fā)現(xiàn)protobuf-net已考慮的非常周到剂公,不論是客戶端發(fā)送對象還是服務(wù)端接收對象希俩,均只需一行代碼就可實現(xiàn):
*
* 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}");
// 響應(yīng)(MyResponse)
var myResponse = new MyResponse
{
version = myRequest.version,
result = 99
};
Serializer.SerializeWithLengthPrefix(stream, myResponse, PrefixStyle.Base128);
Console.WriteLine("SERVER : 響應(yīng)成功 ---");
Console.WriteLine("SERVER: 關(guān)閉連接 ---");
stream.Close();
client.Close();
}
}
finally
{
AllDone.Set();
}
}
// 客戶端請求
private static void RunClient(object state)
{
try
{
// 構(gòu)造MyData
var myData = new MyData {resume = "我的個人簡介"};
// 構(gòu)造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 : 對象構(gòu)造完畢 ...");
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())
{
// 發(fā)送纲辽,客戶端發(fā)送對象颜武;
Console.WriteLine("CLIENT : 發(fā)送數(shù)據(jù) ...");
ProtoBuf.Serializer.SerializeWithLengthPrefix(stream, myRequest, PrefixStyle.Base128);
// 接收
Console.WriteLine("CLIENT : 等待響應(yīng) ...");
var myResponse = ProtoBuf.Serializer.DeserializeWithLengthPrefix<MyResponse>(stream, PrefixStyle.Base128);
Console.WriteLine($"CLIENT : 成功獲取結(jié)果, version={myResponse.version}, result={myResponse.result}");
// 關(guān)閉
stream.Close();
}
client.Close();
Console.WriteLine("CLIENT : 關(guān)閉 ...");
}
}
catch (Exception error)
{
Console.WriteLine($@"CLIENT ERROR : {error.ToString()}");
}
}
}
}
運行結(jié)果: