游戏开发好帮手——Protobuf

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

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

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

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

在之前的网络游戏教程中,我们使用了C#提供的序列化工具——Serializable特性来实现类型,结构的序列化。并且自己利用C#实现了反序列化工具封装在自定义类型NetworkUtils中(之前的教程有实现)。

image

没错,这套纯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序列化的结果:

image

嗯,你没有看错,就有这么大!

原本(一个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序列化的结果:

image

百分百原生长度,零添加

果真是没有对比就没有伤害,看来protobuf更适合做这方面的工作。

image

在网上还有一些与Json,XML的对比,有兴趣可自行搜索。嗯,我们马上开始实现需要的功能。


好了,直奔主题,首先从安装Protobuf开始:

google/protobufgithub.com

图标

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

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

image

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

贴心附上编译器与工具:

https://github.com/ProcessCA/Protobuf2CSgithub.com

图标

OK,工具准备齐全。我们直接开搞。

image

首先打开我们的编译器目录,然后在当前目录下新建一个.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文件。

image

记得把生成出来的.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>嵌套结构,repeated修饰的字段是有序集合,但是map是无序字典。还有就是enum必须声明在message内部,下标必须从0开始而不是1。

以下是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();
    }
}

以下是运行结果:

image

到这里我们已经通过实践中了解了protobuf,并且通过其提供的API成功完成了序列化操作。

在学习初期的确很容易踩坑,但好在网络上已经有较多的问题回答。本篇文章仅做一个入门参考,有情趣了解更多protobuf本身或者想在实际项目中运用它可以查看官方API:

https://developers.google.com/protocol-buffers/developers.google.com

如果觉得英文阅读效率较低,可以查看这篇:

Protobuf3语言指南 - CSDN博客blog.csdn.net

转载于:https://zhuanlan.zhihu.com/p/41206307

你可能感兴趣的:(游戏开发好帮手——Protobuf)