Protobuf

Protobuf相关

什么是Protobuf

        protobuf全称是 protocol-buffers(协议缓冲区)
        是谷歌提供给开发者的一个开源的协议生成工具
        它的主要工作原理和我们之前做的自定义协议工具类似
        只不过它更加的完善,可以基于协议配置文件生成
        c++、java、c#、objective-c、php、python、ruby、go
        等等语言的代码文件

        它是商业游戏开发中常常会选择的协议生成工具
        有很多游戏公司选择它作为协议工具来进行网络游戏开发
        因为它通用性强,稳定性高,可以节约出开发自定义协议工具的时间

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

Protobuf的使用流程

        1.下载对应语言要使用Protobuf相关内容
        2.根据配置规则编辑协议配置文件
        3.用Protobuf编译器,利用协议配置文件生成对应语言的代码文件
        4.将代码文件导入工程中进行使用

下载Protobuf相关内容——准备DLL文件

        1.在官网中前往下载地址
          protocol - buffers官网
          https://developers.google.com/protocol-buffers
        2.下载protobuf - csharp
        3.解压后打开csharp\src中的Google.Protobuf.sln
        4.选择Google.Protobuf右键生成 dll文件
        5.在csharp\src\Google.Protobuf\bin\Debug路径下找到对应.net版本的Dll文件(我们使用4.5即可)
        6.将net45中的dll文件导入到Unity工程中的Plugins插件文件夹中

下载Protobuf相关内容——准备编译器

        1.在官网中前往下载地址
          protocol - buffers官网
          https://developers.google.com/protocol-buffers
        2.下载protoc - 版本 - win32或者64(根据操作系统而定)
        3.解压后获取bin文件夹中的protoc.exe可执行文件,
          可将其放入Unity工程中,方便之后的使用(你也可以不放入Unity工程,记住它的路径即可)

Protobuf配置

        Protobuf中配置文件的后缀统一使用
        .proto

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson39 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

        #region 配置规则

        #region 规则1 注释方式
        //方式1
        /*方式2*/
        #endregion

        #region 规则2 第一行版本号
        //syntax = "proto3";
        //如果不写 默认使用proto2
        #endregion

        #region 规则3 命名空间
        //package 命名空间名;
        #endregion

        #region 规则4 消息类
        //message 类名{
        //字段声明
        //}
        #endregion

        #region 规则5 成员类型和 唯一编号
        //浮点数:
        //float、double
        //整数:
        //变长编码-int32,int64,uint32,uint64,
        //固定字节数-fixed32,fixed64,sfixed32,sfixed64
        //其它类型:
        //bool,string,bytes

        //唯一编号 配置成员时 需要默认给他们一个编号 从1开始
        //这些编号用于标识中的字段消息二进制格式

        #endregion

        #region 规则6 特殊标识
        //1:required 必须赋值的字段
        //2:optional 可以不赋值的字段
        //3:repeated 数组
        #endregion

        #region 规则7 枚举
        //enum 枚举名{
        //    常量1 = 0;//第一个常量必须映射到0
        //    常量2 = 1;
        //}
        #endregion

        #region 规则8 默认值
        //string-空字符串
        //bytes-空字节
        //bool-false
        //数值-0
        //枚举-0
        //message-取决于语言 C#为空
        #endregion

        #region 规则9 允许嵌套

        #endregion

        #region 规则10 保留字段
        //如果修改了协议规则 删除了部分内容
        //为了避免更新时 重新使用 已经删除了的编号
        //我们可以利用 reserved 关键字来保留字段
        //这些内容就不能再被使用了
        //message Foo {
        //    reserved 2, 15, 9 to 11;
        //    reserved "foo", "bar";
        //}
        #endregion

        #region 规则11 导入定义
        //import "配置文件路径";
        //如果你在某一个配置中 使用了另一个配置的类型
        //则需要导入另一个配置文件名
        #endregion

        #endregion

    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Protobuf配置文件

test.proto

syntax = "proto3";//决定了proto文档的版本号
//规则二:版本号

//规则一:注释方式
//注释方式一
/*注释方式二*/

//规则11:导入定义
import "test2.proto";

//规则三:命名空间
package GamePlayerTest;//这决定了命名空间


//规则四:消息类
message TestMsg{
	//规则五:成员类型 和 唯一编号

	//浮点数
	// = 1 不代表默认值 而是代表唯一编号 方便我们进行序列化和反序列化的处理
	//required 必须赋值的字段
	required float testF = 1; //C# - float
	//optional 可以不赋值的字段
	optional double testD = 2; //C# - double

	//变长编码
	//所谓变长 就是会根据 数字的大小 来使用对应的字节数来存储  1 2 4 
	//Protobuf帮助我们优化的部分 可以尽量少的使用字节数 来存储内容
	int32 testInt32 = 3; //C# - int 它不太适用于来表示负数 请使用sint32
	//1 2 4 8
	int64 testInt64 = 4; //C# - long  它不太适用于来表示负数 请使用sint64

	//更实用与表示负数类型的整数
	sint32 testSInt32 = 5; //C# - int 适用于来表示负数的整数
	sint64 testSInt64 = 6; //C# - long 适用于来表示负数的整数

	//无符号 变长编码
	//1 2 4
	uint32 testUInt = 7; //C# - uint 变长的编码
	uint64 testULong = 8; //C# - ulong 变长的编码

	//固定字节数的类型
	fixed32 testFixed32 = 9; //C# -uint 它通常用来表示大于2的28次方的数 ,比uint32更有效 始终是4个字节
	fixed64 testFixed64 = 10; //C# -ulong 它通常用来表示大于2的56次方的数 ,比uint64更有效 始终是8个字节

	sfixed32 testSFixed32 = 11; //C# - int 始终4个字节
	sfixed64 testSFixed64 = 12; //C# - long 始终8个字节

	//其它类型
	bool testBool = 13; //C# - bool 
	string testStr = 14; //C# - string
	bytes testBytes = 15; //C# - BytesString 字节字符串

	//数组List
	repeated int32 listInt = 16; // C# - 类似List<int>的使用
	//字典Dictionary
	map<int32, string> testMap = 17; // C# - 类似Dictionary<int, string> 的使用

	//枚举成员变量的声明 需要唯一编码
	TestEnum testEnum = 18;

	//声明自定义类对象 需要唯一编码
	//默认值是null
	TestMsg2 testMsg2 = 19;

	//规则9:允许嵌套
	//嵌套一个类在另一个类当中 相当于是内部类
	message TestMsg3{
		int32 testInt32 = 1;
	}

	TestMsg3 testMsg3 = 20;

	//规则9:允许嵌套
	enum TestEnum2{
		NORMAL = 0; //第一个常量必须映射到0
		BOSS = 1;
	}

	TestEnum2 testEnum2 = 21;

	//int32 testInt3233333 = 22;

	bool testBool2123123 = 23;

	GameSystemTest.HeartMsg testHeart = 24;

	//告诉编译器 22 被占用 不准用户使用
	//之所以有这个功能 是为了在版本不匹配时 反序列化时 不会出现结构不统一
	//解析错误的问题
	reserved 22;
	reserved testInt3233333;
}

//枚举的声明
enum TestEnum{
	NORMAL = 0; //第一个常量必须映射到0
	BOSS = 5;
}

message TestMsg2{
	int32 testInt32 = 1;
}

test2.proto

syntax = "proto3";//决定了proto文档的版本号
package GameSystemTest;//这决定了命名空间
message HeartMsg
{
	int64 time = 1;
}

利用protoc.exe编译器生成脚本文件

        1.打开cmd窗口
        2.进入protoc.exe所在文件夹(也可以直接将exe文件拖入cmd窗口中)
        3.输入转换指令
        protoc.exe - I = 配置路径--csharp_out = 输出路径 配置文件名

        注意:路径不要有中文和特殊符号,避免生成失败

-I=后面接配置文件所在文件夹 --csharp_out=后面接目标代码要生成的目录 配置文件名是指你要配置哪个文件,下图分别配置了test.proto 和test2.proto

利用上面配置文件生成脚本文件

目标文件会出现在对于文件目录下(桌面上的csharp文件目录)

 

 测试生成对象的使用

将生成的Test/Test2文件放入到对应unity工程文件夹下,进行测试 

Test中成员变量在新生成的代码当中以属性形式存在,List/Dictionary不是原本C#对应类,但是protobuf帮我们实现了其中所有逻辑。

using GamePlayerTest;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson40 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {


        #region  测试生成对象是否能使用
        TestMsg msg = new TestMsg();
        msg.TestBool = true;
        //对应的和List以及Dictionary使用方式一样的 数组和字典对象
        msg.ListInt.Add(1);
        print(msg.ListInt[0]);
        msg.TestMap.Add(1, "测试");
        print(msg.TestMap[1]);

        //枚举
        msg.TestEnum = TestEnum.Boss;
        //内部枚举
        msg.TestEnum2 = TestMsg.Types.TestEnum2.Boss;

        //其它类对象
        msg.TestMsg2 = new TestMsg2();
        msg.TestMsg2.TestInt32 = 99;
        //其它内部类对象
        msg.TestMsg3 = new TestMsg.Types.TestMsg3();
        msg.TestMsg3.TestInt32 = 55;
        //在另一个生成的脚本当中的类 如果命名空间不同 需要命名空间点出来使用
        msg.TestHeart = new GameSystemTest.HeartMsg();
        #endregion

        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

 protobuf协议使用

using GamePlayerTest;
using Google.Protobuf;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class Lesson41 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 序列化存储为本地文件
        //主要使用
        //1.生成的类中的 WriteTo方法
        //2.文件流FileStream对象
        TestMsg msg = new TestMsg();
        msg.ListInt.Add(1);
        msg.TestBool = false;
        msg.TestD = 5.5;
        msg.TestInt32 = 99;
        msg.TestMap.Add(1, "测试");
        msg.TestMsg2 = new TestMsg2();
        msg.TestMsg2.TestInt32 = 88;
        msg.TestMsg3 = new TestMsg.Types.TestMsg3();
        msg.TestMsg3.TestInt32 = 66;

        msg.TestHeart = new GameSystemTest.HeartMsg();
        msg.TestHeart.Time = 7777;

        print(Application.persistentDataPath);
        using (FileStream fs = File.Create(Application.persistentDataPath + "/TestMsg.tang"))
        {
            msg.WriteTo(fs);
        }
        #endregion

        #region 知识点二 反序列化本地文件
        //主要使用
        //1.生成的类中的 Parser.ParseFrom方法
        //2.文件流FileStream对象
        using (FileStream fs = File.OpenRead(Application.persistentDataPath + "/TestMsg.tang"))
        {
            TestMsg msg2 = null;
            msg2 = TestMsg.Parser.ParseFrom(fs);
            print(msg2.TestMap[1]);
            print(msg2.ListInt[0]);
            print(msg2.TestD);
            print(msg2.TestMsg2.TestInt32);
            print(msg2.TestMsg3.TestInt32);
            print(msg2.TestHeart.Time);
        }
        #endregion

        #region 知识点三 得到序列化后的字节数组
        //主要使用
        //1.生成的类中的 WriteTo方法
        //2.内存流MemoryStream对象
        byte[] bytes = null;
        using (MemoryStream ms = new MemoryStream())
        {
            msg.WriteTo(ms);
            bytes = ms.ToArray();
            print("字节数组长度" + bytes.Length);
        }

        #endregion

        #region 知识点四 从字节数组反序列化
        //主要使用
        //1.生成的类中的 Parser.ParseFrom方法
        //2.内存流MemoryStream对象
        using (MemoryStream ms = new MemoryStream(bytes))
        {
            print("内存流当中反序列化的内容");
            TestMsg msg2 = TestMsg.Parser.ParseFrom(ms);
            print(msg2.TestMap[1]);
            print(msg2.ListInt[0]);
            print(msg2.TestD);
            print(msg2.TestMsg2.TestInt32);
            print(msg2.TestMsg3.TestInt32);
            print(msg2.TestHeart.Time);
        }

        #endregion

        #region 总结
        //Protobuf的 序列化和反序列化都要通过
        //流对象来进行处理
        //如果是进行本地存储 则可以使用文件流
        //如果是进行网络传输 则可以使用内存流获取字节数组
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

 protobuf协议的封装使用

using Google.Protobuf;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEngine;

public static class NetTool 
{
    //序列化Protobuf生成的对象
    public static byte[] GetProtoBytes( IMessage msg )
    {
        //基础写法 
        //byte[] bytes = null;
        //using (MemoryStream ms = new MemoryStream())
        //{
        //    msg.WriteTo(ms);
        //    bytes = ms.ToArray();
        //}
        //return bytes;

        //通过该拓展方法 就可以直接获取对应对象的 字节数组了
        return msg.ToByteArray();
    }

    /// <summary>
    /// 反序列化字节数组为Protobuf相关的对象
    /// </summary>
    /// <typeparam name="T">想要获取的消息类型</typeparam>
    /// <param name="bytes">对应的字节数组 用于反序列化</param>
    /// <returns></returns>
    public static T GetProtoMsg<T>(byte[] bytes) where T:class, IMessage
    {
        //得到对应消息的类型 通过反射得到内部的静态成员 然后得到其中的 对应方法
        //进行反序列化
        Type type = typeof(T);
        //通过反射 得到对应的 静态成员属性对象
        PropertyInfo pInfo = type.GetProperty("Parser");
        object parserObj = pInfo.GetValue(null, null);
        //已经得到了对象 那么可以得到该对象中的 对应方法 
        Type parserType = parserObj.GetType();
        //这是指定得到某一个重载函数
        MethodInfo mInfo = parserType.GetMethod("ParseFrom", new Type[] { typeof(byte[]) });
        //调用对应的方法 反序列化为指定的对象
        object msg = mInfo.Invoke(parserObj, new object[] { bytes });
        return msg as T;
    }
}

 Protobuf-Net

        早期的Protobuf并不支持C#
        所以国外大神Marc Gravell在Protobuf的基础上进行了.net环境下的移植
        并发布到了GitHub
        让我们可以基于Protobuf的规则进行C#的代码生成,对象的序列化和反序列化

        Protobuf - Net的Github地址:https://github.com/protobuf-net/protobuf-net

        注意:
        1.Protobuf不支持.Net3.5及以下版本
           所以如果想在Unity的老版本中使用Protobuf我们只能使用Protobuf - Net
           而在较新版本的Unity中不存在这个问题
        2.如何判断是否支持?
           只要把Protobuf相关dll包导入后能够正常使用不报错,则证明支持

大小端模式

什么是大小端模式

        大端模式
        是指数据的高字节保存在内存的低地址中
        而数据的低字节保存在内存的高地址中
        这样的存储模式有点儿类似于把数据当作字符串顺序处理
        地址由小向大增加,数据从高位往低位放
        符合人类的阅读习惯

        小端模式
        是指数据的高字节保存在内存的高地址中
        而数据的低字节保存在内存的低地址中

        举例说明
        十六进制数据0x11223344

        大端模式存储
         11    22    33    44
         0     1     2     3
        低地址——> 高地址

        小端模式存储
         44    33    22    11
         0     1     2     3
        低地址——> 高地址

为什么有大小端模式

        大小端模式其实是计算机硬件的两种存储数据的方式
        我们也可以称大小端模式为 大小端字节序

        对于我们来说,大端字节序阅读起来更加方便,为什么还要有小端字节序呢?
        原因是,计算机电路先处理低位字节,效率比较高
        计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节
        它只知道按顺序读取字节,先读第一个字节,再读第二个字节
        如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节
        小端字节序正好相反

        因为计算机都是从低位开始的
        所以,计算机的内部处理都是小端字节序

        但是,我们人类的读写习惯还是大端字节序
        所以,除了计算机的内部处理
        其它场合几乎都是大端字节序,比如网络传输和文件存储

        一般情况下,操作系统都是小端模式,而通讯协议都是大端模式
        但是具体的模式,还是要根据硬件平台,开发语言来决定
        主机不同,开发语言不同 可能采用的大小端模式也会不一致

大小端模式对于我们的影响

        我们记住一句话:
        只有读取的时候,才必须区分大小端字节序,其它情况都不用考虑

        因此对于我们来说,在网络传输当中我们传输的是字节数组
        那么我们在收到字节数组进行解析时,就需要考虑大小端的问题
        虽然TCP / IP协议规定了在网络上必须采用网络字节顺序(大端模式)
        但是具体传输时采用哪种模式,都是根据前后端语言、设备决定的

        在进行网络通讯时,前后端语言不同时,可能会造成大小端不统一
        一般情况下
        C# 和 Java/Erlang/AS3 通讯需要进行大小端转换 因为C#是小端模式 Java/Erlang/AS3是大端模式
        C# 与 C++通信不需要特殊处理 他们都是小端模式

大小端转换

        #region 1.判断是大小端哪种模式
        print("是否是小端模式:" + BitConverter.IsLittleEndian);
        #endregion

        #region 2.简单的转换API 只支持几种类型
        //转换为网络字节序 相当于就是转为大端模式
        //1. 本机字节序转网络字节序
        int i = 99;
        byte[] bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(i));
        //2. 网络字节序转本机字节序
        int receI = BitConverter.ToInt32(bytes, 0);
        receI = IPAddress.NetworkToHostOrder(receI);
        #endregion

        #region 3.通用的转换方式
        //数组中的倒序API
        //如果后端需要用到大端模式 那么我们进行判断
        //如果当前是小端模式 就进行一次 大小端转换
        if(BitConverter.IsLittleEndian)
            Array.Reverse(bytes);
        #endregion

        #endregion

        #region 总结
        //大小端模式会根据主机硬件环境不同、语言不同而有所区别
        //当我们前后端是不同语言开发且运行在不同主机上时
        //前后端需要对大小端字节序定下统一的规则

        //一般让前端迎合后端,因为字节序的转换也是会带来些许性能损耗的
        //网络游戏中要尽量减轻后端的负担

        //一般情况下
        //C# 和 Java/Erlang/AS3 通讯需要进行大小端转换 前端C#从小变大
        //C# 与 C++通信不需要特殊处理

        //我们不用死记硬背和谁通讯要注意大小端模式
        //当开发时,发现后端收到的消息和前端发的不一样
        //在协议统一的情况下,往往就是因为大小端造成的
        //这时我们再转换模式即可

        //注意:
        //Protobuf已经帮助我们解决了大小端问题
        //即使前后端语言不统一
        //使用它也不用过多考虑字节序转换的问题
        #endregion

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值