[toc]本篇主要内容是:如何使用Google.Protobuf作为通信协议. 本篇也作为学习ET服务端框架的前置内容.

安装Google.Protobuf

安装MongoDB.Driver一样,这里可以选择Nuget安装和编译安装. Nuget安装搜索:Google.Protobuf 编译安装源代码:github,选择protobuf-csharp-<版本号>.zip 我这里选择的是3.6.1版本,选择编译安装的话,还可以获得附赠的一套教程”AddressBook”项目,谷歌官方的代码建议不要错过.

AddressBook

在编译安装时,可以在工程中找到这个项目,是Google.Protobuf的教学项目. “AddressBook”名字意思就是地址博,程序会反复提示用户接下来如何操作,并将用户输入的”联系人”信息保存为本地文件. 初次运行”AddressBook”可能会报错,将.csproj文件中的<TargetFramework>设置为正确版本即可,比如我这里是2.1版本. 将”AddressBook”单独复制出来,删除原来的工程依赖使用Nuget安装Google.Protobuf也一样可以运行. 运行AddressBook项目提示如下 程序可以设置保存为本地文件的路径,但是直接调试运行程序是不带参数的,如果要带参数可以使用cmd. 运行cmd程序,输入d:移动到D盘,输入cd D:\dotnet\Bin移动到D:\dotnet\Bin目录 输入:dotnet AddressBokk.dll sekia 联系人文件将会设置为”sekia” 建立个人信息时会检查是否有对应文件 输入:dotnet AddressBokk.dll 将使用默认设置”addressbook.data”

对象-字节流的转化

“AddressBook”中,用户输入A会执行添加联系人,用户输入L会执行列出联系人列表. 先实例化一个AddressBook对象,再对对象进行初始化/添加联系人/保存为文件/列出联系人操作. 在本项目的SampleUsage.cs中,则是描述了用成员去实例化对象/将字节转化为对象/将成员转化为字节流/将对象转化为字节流. 所以学习重点将会是:声明中对抽象目标的结构进行描述(如玩家数据)/在操作函数中完成指定处理(增删查改)/类对象的存储/字节流传输

How to定义class…

我们再来看Addressbook.cs,这个文件的可读性非常糟糕,但是我们可以在文件上方看到这个: // // Generated by the protocol buffer compiler. DO NOT EDIT! // source: addressbook.proto // 意思就是说让我们去编辑addressbook.proto文件,然后使用Google.Protobuf的编译器生成Addressbook.cs.

.protoc编译器

要编辑.proto文件,得下载.protoc编译器,下载地址依然在github,这次要下载的是protoc-<版本号>-<平台>.zip,我这里选择的是protoc-3.6.1-win32.zip. 根据文件夹里的readme.txt,这个编译器是给非C++语言用Google.Protobuf但是不想自己编译protoc的人提供的. 大概说的就是我这样的人. 使用cmd工具运行bin目录下的protoc.exe,会弹出很长的一段帮助文档,加入指定的额外参数就可以使用编译功能. Usage: protoc [OPTION] PROTO_FILES 命令结构大概是:protoc –命令A=设置A –命令B=设置B proto文件 不过直接使用protoc命令会提示:’protoc’ 不是内部或外部命令,也不是可运行的程序,就得添加环境变量了,记得重启哦.

为.exe文件设置环境变量

这样做的好处是,在任何地方都可以运行.exe文件.此电脑-属性-高级系统设置-高级-环境变量-编辑Path变量-添加protoc.exe的所在目录.比如我的目录是:D:\soft\protoc 然后就可以在cmd中输入protoc运行protoc.exe了. 新设置的环境变量要重启电脑才能生效!先重启一波.

迁移include文件夹下的文件

如果没有include文件夹下的文件,在使用protoc.exe时会提示缺少google\protobuf\timestamp.proto 解决办法是将google文件夹移动到指定了环境变量的文件夹,比如:我们刚刚设置了环境变量的那个文件夹.

编译导出C#文件

为了演示编译步骤,我这里创建了一个文件夹D:\1,并将addressbook.proto移动到这里. 运行cmd,并将目录移动至D:\1,运行以下命令: protoc –csharp_out=./ addressbook.proto 即可得到编译好的.cs文件 上面的命令中,–csharp_out=./设置了输出模式为C#文件.此编译中必须同时指定输入文件/输出方式/输出目录. 上面的命令是最简化版本,关于protoc.exe的更多参数请参考其他资料.

编辑.protoc文件

知道了protoc.exe的使用方法以后,就可以放心的学习如何定义class,我们的学习对象依然是addressbook.proto,通过了解.proto文件的结构来编译出自己的.cs文件. Notepad++打开addressbook.proto,在上方可以看到教程地址:Google.Protobuf官方C#指南

1
2
3
4
syntax = "proto3";
package tutorial;

import "google/protobuf/timestamp.proto";

头部声明部分 使用proto3语法; 据说是设置这个可以有助于阻止不同项目之间命名冲突.那么package设置为项目名好了. option java_package = "com.example.tutorial"; option java_outer_classname = "AddressBookProtos"; java声明部分,C#项目为什么要声明java…爱填不填吧 option csharp_namespace = "Google.Protobuf.Examples.AddressBook"; C#声明部分,设置.cs文件的namespace声明,比如玩家数据:Player.Data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
message Person {
string name = 1;
int32 id = 2;
string email = 3;

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

message PhoneNumber {
string number = 1;
PhoneType type = 2;
}

repeated PhoneNumber phones = 4;

google.protobuf.Timestamp last_updated = 5;
}

message AddressBook {
repeated Person people = 1;
}

message部分 在上面的message结构中,消息Person包含了一个enum枚举字段PhoneType,和子消息PhoneNumber,这样可以得到多种手机号码,如WORk PhoneNumber或者MOBILE PhoneNumber. 字段上”=1”,”=2”的标记是字段在二进制编码中的唯一”tag”,标签1-15号相比更高的数字在编码时会少一个字节.作为优化,将高于16的tag分配给不常用的字段. 如果未设置字段值,则使用默认值:数字类型为0,字符串为空,bool为false. repeated字段可以重复任意次(包括0),重复值的顺序将保留在缓冲区.将repeated字段视为动态大小的数组. 在这里可以访问proto3完整语言指南.

addressbook类

生成Addressbook.cs将提供5种 一个静态类Addressbook,包含了Protobuf消息的数据结构 一个AddressBook类,具体只读的People属性 一个Person类,有Name/Id/Email/Phones属性 一个PhoneNumber类,嵌套在Person.Types类中 一个枚举PhoneType,嵌套在Person.Types类中 重复字段中的任何属性都是只读的,可以添加或删除元素,但是不能进行替换. 重复字段的collection类型一直是RepeatedField,与List类似,但是有一些外的便利方法,例如Add重载接收一个collection,用于初始化集合. 下面演示将创建的Person实例: Person john = new Person { Id = 1234, Name = “John Doe”, Email = “jdoe@example.com“, Phones = { new Person.Types.PhoneNumber { Number = “555-4321”, Type = Person.Types.PhoneType.HOME } } }; 注意:在C#6中,使用using static 可以去除Person.Types前缀 //添加到其他using指令中 using static Google.Protobuf.Examples.AddressBook.Person.Types; //上面的手机号码部分可以简写为 Phones = { new PhoneNumber { Number = “555-4321”, Type = PhoneType.HOME } }

解析和序列化

使用Protobuf的全部目的是为了序列化数据,以便在其他地方解析数据. 每个被生存的类都有一个WriteTo(CodedOutputStream)方法,CodedOutputStream是Protobuf运行时中的一个方法. 但是,通常你将使用某个扩展方法,写入到常规的System.IO.Steam或者转化message为为byte数组/字符串. 这些扩展方法在Google.Protobuf.MessageExtensions类中,需要using命名空间使用:

1
2
3
4
5
6
7
8
using Google.Protobuf;

...
Person john = ...; //对实例的赋值
using (var output = File.Create("john.dat"))
{
john.WriteTo(output);
}

解析也很简单,每个生成的类都有一个静态Parser属性,为类返回一个MessageParser. 反过来也有方法用来解析流/字节/字符串.要解析我们刚创建的john.dat文件,我们可以用:

1
2
3
4
5
Person john;
using (var input = File.OpenRead("john.dat"))
{
john = Person.Parser.ParseFrom(input);
}

映射

每个生成的类都有一个静态Descriptor属性,可以使用该IMessage.Descriptor属性检索任何实例的描述符。 一种打印所有顶级字段的简单方法:

1
2
3
4
5
6
7
8
9
10
11
12
public void PrintMessage(IMessage message)
{
var descriptor = message.Descriptor;
foreach (var field in descriptor.Fields.InDeclarationOrder())
{
Console.WriteLine(
"Field {0} ({1}): {2}",
field.FieldNumber,
field.Name,
field.Accessor.GetValue(message));
}
}

应用

通过Protobuf可以便捷的处理IMessage对象的解析/序列化. 如果我们直接将一个IMessage对象写入字节流NETStream.write(byte[]),发送到其他机器,其他机器则可以根据反解析获得这个IMessage,实现了通信. IMessage→byte[]→socket→byte[]→IMessage