今天主角是来自谷歌的——Protocol Buffer,简称protobuf。英文直译过来就是协议缓冲区。是一种独立于语言,独立于平台的数据交换格式。
protobuf有自己专用的描述文件(.proto),有自己的编译器protoc与对多种编程语言提供的API。
主要作用在于为数据储存、网络通信等提供一种基于二进制的格式,是一种高效率的序列化工具。
想必熟悉编程的小伙伴们一定对序列化有所认识,特别是Json,XML这一类比较常用的数据交换格式。
在之前的网络游戏教程中,我们使用了C#提供的序列化工具——Serializable特性来实现类型,结构的序列化。并且自己利用C#实现了反序列化工具封装在自定义类型NetworkUtils中(之前的教程有实现)。
没错,这套纯C#编写的工具用起来是很方便,但是依然留下很多问题:
第一,如果我的游戏服务器不是用C#编写的呢?使用C++或者Go编写的话,我还能用这套工具跨语言进行正反序列化吗?
答案是:当然不能,而且C# Serializable特性不仅会序列化类中的数据,还会把类的信息也序列化。其他的语言无法识别这套编码协议。
第二,如果甚至连我的游戏客户端都是用UE4跟C++编写的,那我是不是又得去学一套C++的库作为序列化工具呢?
答案是:你只要学会protobuf本身的语法与对特定语言提供的接口,基本上不用操心其他的事情。
第三,就是序列化速度与效率上的问题了,下面直接上代码测试。
C#:以下是.cs文件的实现,仅有一个消息最基本的两个字段的.cs文件(Message)
[Serializable]
public class Message
{
public int Id;
public string Msg;
}
然后就是序列化环节,我们先从C#开始:
首先利用NetworkUtils序列化Message得到byte数组csData。
//在Main方法中初始化
private static void Main()
{
Message message = new Message();
message.Id = 1;
message.Msg = "Hello Networking!";
byte[] csData = NetworkUtils.Serialize(message);
}
然后我们一执行代码,一看csData数组的长度,我也是吓到了。
以下是C# NetworkUtils序列化的结果:
原本(一个int 4字节,字符串总共 17字节)硬是膨胀到了145字节。要是在带宽不足的网络通信中这就容易导致用户体验极差的问题。
Protobuf:以下是.proto文件的实现。利用protoc编译器把.proto自动转换成.cs文件。详细的语法与细节后面会介绍。
syntax = "proto3";
package PBMessage;
message Message
{
int32 Id = 1;
string Msg = 2;
}
使用神秘API——PBConverter序列化PBMessage.Message得到byte数组protoData。
using System;
using Google.Protobuf;
private static void Main()
{
PBMessage.Message message = new PBMessage.Message();
message.Id = 1;
message.Msg = "Hello Networking!";
byte[] protoData = PBConverter.Serializer(message);
}
proto PBConverter序列化的结果:
果真是没有对比就没有伤害,看来protobuf更适合做这方面的工作。
在网上还有一些与Json,XML的对比,有兴趣可自行搜索。嗯,我们马上开始实现需要的功能。
好了,直奔主题,首先从安装Protobuf开始:
google/protobufgithub.com
在这里我们选择安装单独安装最新版的protobuf-csharp-3.6.1
解压后我们按照 csharp -> src-> Google.Protobuf.sln,打开解决方案后然后生成解决方案,之后我们就可以在Google.Protobuf的bin目录下找到我们的Google.Protobuf.dll,有了这个Dll还不行。
我们还得需要一个把.proto文件变成.cs文件的编译器protoc.exe。但是这个编译器对于C#用户很不友好非常难安装(我当初也是用cmake折腾了好久),网上也难以找到合适的编译方法。
贴心附上编译器与工具:
https://github.com/ProcessCA/Protobuf2CSgithub.com
OK,工具准备齐全。我们直接开搞。
首先打开我们的编译器目录,然后在当前目录下新建一个.proto后缀的文件。
按照protobuf的语法,我们在开头声明一些必备(否则过不了编译)的属性,它的语法跟C风格的语言类似,不过在关键字跟数据类型上有区别,同样也有注释。
syntax = "proto3"; //使用proto3语法编译
package Test; //声明包名
message Message //message关键字声明类型
{
int32 id = 1; //字段得从1开始声明
string msg = 2;//然后依次递增,不能跳跃。
}
然后我们打开Protobuf一键转换工具并输入同目录下.proto文件的名字来快捷启动编译器。
或者使用cmd命令,先cd该目录下然后输入:protoc.exe --csharp_out=./ 文件名.proto
来编译.proto文件。如果编写的proto文件有异常,protoc编译器会报错。如果是用的Protobuf一键转换工具,那么就不会输出程序结束的提示。编译失败后必须在.proto文件中查找问题并解决。如果成功编译后,会在与.proto相同目录下生成一个.cs文件。
记得把生成出来的.cs文件添加到项目中。同时在项目中添加Google.protobuf.dll引用
按照之前的做法,先实现一个序列化工具类——PBConverter
using System;
using Test; //protobuf中的包名
using Google.Protobuf; //谷歌对C#提供的语言接口
class PBConverter
{
public static byte[] Serialize(T obj) where T : IMessage
{
byte[] data = obj.ToByteArray();
return data;
}
public static T Deserialize(byte[] data) where T : class, IMessage, new()
{
T obj = new T();
IMessage message = obj.Descriptor.Parser.ParseFrom(data);
return message as T;
}
private static void Main()
{
Message message = new Message();
message.Id = 1;
message.Msg = "Hello World";
byte[] data = PBConverter.Serialize(message);
Message msg = PBConverter.Deserialize(data);
Console.WriteLine($"ID:{msg.Id}, Message:{msg.Msg}");
Console.ReadKey();
}
}
通过结果,可以看出这套序列化功能没有问题。既然已经实现了基本功能,我们就试试更高级的功能,比如如何在protobuf语言中声明List,Dictionary这种容器还有枚举。
syntax = "proto3"; //使用proto3语法编译
package Test; //声明包名
message Message2 //message关键字声明类型
{
repeated string names = 1; //repeated修饰的相当于C#中的list
map peaples = 2;//map相当于声明字典, key只能是int类型或者string
Type type = 3; //使用自定义枚举类型
enum Type
{
A = 0; //枚举得从0开始声明
B = 1;
}
}
这里要注意:map修饰的字段同时也可以被repeated修饰,相当于List
以下是Test.Message2在C#中的使用:
using System;
using Test;
using Google.Protobuf;
class PBConverter
{
public static byte[] Serialize(T obj) where T : IMessage
{
byte[] data = obj.ToByteArray();
return data;
}
public static T Deserialize(byte[] data) where T : class, IMessage, new()
{
T obj = new T();
IMessage message = obj.Descriptor.Parser.ParseFrom(data);
return message as T;
}
private static void Main()
{
Message2 message2 = new Message2();
message2.Type = Message2.Types.Type.A;
message2.Names.Add("Ele");
message2.Names.Add("Cia");
message2.Peaples.Add(1,"Fuc");
byte[] data = PBConverter.Serialize(message2);
Message2 msg2 = PBConverter.Deserialize(data);
Console.WriteLine(msg2.Type);
foreach (var each in msg2.Names)
{
Console.WriteLine(each);
}
Console.WriteLine(message2.Peaples[1]);
Console.ReadKey();
Console.ReadKey();
}
}
以下是运行结果:
到这里我们已经通过实践中了解了protobuf,并且通过其提供的API成功完成了序列化操作。
在学习初期的确很容易踩坑,但好在网络上已经有较多的问题回答。本篇文章仅做一个入门参考,有情趣了解更多protobuf本身或者想在实际项目中运用它可以查看官方API:
https://developers.google.com/protocol-buffers/developers.google.com
如果觉得英文阅读效率较低,可以查看这篇:
Protobuf3语言指南 - CSDN博客blog.csdn.net
转载于:https://zhuanlan.zhihu.com/p/41206307