欢迎光临散文网 会员登陆 & 注册

游戏开发好帮手——Protobuf

2018-09-13 17:33 作者:皮皮关做游戏  | 我要投稿

作者:ProcessCA

本篇难度:★★★☆☆


大家好。不写文章不舒服斯基又来跟各位见面了。

今天主角是来自谷歌的——Protocol Buffer,简称protobuf。英文直译过来就是协议缓冲区。是一种独立于语言独立于平台的数据交换格式。

protobuf有自己专用的描述文件(.proto),有自己的编译器protoc与对多种编程语言提供的API。

主要作用在于为数据储存、网络通信等提供一种基于二进制的格式,是一种高效率的序列化工具。

想必熟悉编程的小伙伴们一定对序列化有所认识,特别是JsonXML这一类比较常用的数据交换格式。

在之前的网络游戏教程中,我们使用了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开始:https://github.com/protocolbuffers/protobuf/releases


在这里我们选择安装单独安装最新版的protobuf-csharp-3.6.1

解压后我们按照 csharp -> src-> Google.Protobuf.sln,打开解决方案后然后生成解决方案,之后我们就可以在Google.Protobuf的bin目录下找到我们的Google.Protobuf.dll,有了这个Dll还不行。

我们还得需要一个把.proto文件变成.cs文件的编译器protoc.exe。但是这个编译器对于C#用户很不友好非常难安装(我当初也是用cmake折腾了好久),网上也难以找到合适的编译方法。以下贴心附上编译器与工具的下载链接:

https://pan.baidu.com/s/1tFWz6OXvjxUHeIg4Z2mbBg


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>(T obj) where T : IMessage

    {

        byte[] data = obj.ToByteArray();

        return data;

    }

 

    public static T Deserialize<T>(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<Message>(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<int32, string> peaples = 2;//map相当于声明字典, key只能是int类型或者string

    Type type = 3;                 //使用自定义枚举类型

 

    enum Type

    {

        A = 0;       //枚举得从0开始声明

        B = 1;

    }

}

 

这里要注意:map修饰的字段同时也可以被repeated修饰,相当于List<Dictionary<>>嵌套结构,repeated修饰的字段是有序集合,但是map是无序字典。还有就是enum必须声明在message内部,下标必须从0开始而不是1。


以下是Test.Message2在C#中的使用:

using System;

using Test;

using Google.Protobuf;

 

class PBConverter

{

    public static byte[] Serialize<T>(T obj) where T : IMessage

    {

        byte[] data = obj.ToByteArray();

        return data;

    }

 

    public static T Deserialize<T>(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<Message2>(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:http://link.zhihu.com/?target=https%3A//developers.google.com/protocol-buffers/


如果觉得英文阅读效率较低,可以查看这篇:https://blog.csdn.net/u011518120/article/details/54604615

OK,希望本文对在游戏开发的道路上的你有所启发。


想系统学习游戏开发的童鞋,欢迎访问 http://www.levelpp.com/

游戏开发搅基QQ群:869551769    

微信公众号:皮皮关


游戏开发好帮手——Protobuf的评论 (共 条)

分享到微博请遵守国家法律