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