Unity中使用Protobuf进行网络通讯

Update 2019

安装

Google.Protobuf.dll

https://www.nuget.org/packages/Google.Protobuf下载最新的Google.Protobuf.dll (Zip解压即可)
我用的版本是google.protobuf.3.8.0

拷贝net45到工程目录

protobuf-unity

同旧版,直接拖进工程即可

本地存储

可以直接使用 ProtoBinaryManager<T, LocalSave> 不过就是单例了. 其内部实现如下

Save

1
2
3
4
5
6
7
8

var data = $你需要序列化的Proto类

//加密的Key
var key = new Rfc2898DeriveBytes(Encoding.ASCII.GetBytes("eran"), Encoding.ASCII.GetBytes("oldking.wang"), 5555).GetBytes(16);

var stream = ProtoBinaryManager.ProtoToStream(data, key);
ProtoBinaryManager.StreamToFile(stream, Application.dataPath + "/ProtoTest", "Outline.data");

Load

1
2
3
4
5

//同加密的Key
var key = new Rfc2898DeriveBytes(Encoding.ASCII.GetBytes("eran"), Encoding.ASCII.GetBytes("oldking.wang"), 5555).GetBytes(16);

var save = ProtoBinaryManager.ProtoFromFile<ProtoOutline>(key, Application.dataPath + "/ProtoTest", "Outline.data");

环境搭建

Protocol Buffers是Google定义的一种数据结构.

本地安装

安装Protobuf

1
brew install protobuf

安装Unity插件protobuf-unity

15130039913882

测试Proto

注意,最好按照 插件中指定的命名方法命名.比如我的Proto名字叫game_proto.proto,最后导出的C#类的名字就是GameProto

game_proto.proto

1
2
3
4
5
6
7
8
9
10
11
12
syntax = "proto3";

//commandID 9001
message TestSyncRequest1 {
string user = 1;
}

//commandID 9001
message TestSyncResponse1 {
string id = 1;
string user = 2;
}

插件很高级,当Proto文件变化时候就会直接更新C#的类.很方便.

注意前面需要指定syntax = "proto3"

Proto 2 和 3 的区别可以参考 这篇文章

简单序列与反序列化

单独使用Proto进行序列化和反序列化很简单,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestProto : MonoBehaviour
{
public void OnClickTestBtn()
{
//序列化
var req1 = new TestSyncRequest1();
req1.User = "ABC";
var data = req1.ToByteArray();

//反序列化
var c = TestSyncRequest1.Parser.ParseFrom(data);
Debug.Log(c.User);
}
}

网络通讯

配合网络通讯会稍微有些复杂.主要是有两个问题.

大端(Encoding.BigEndianUnicode)小端(Encoding.Unicode)

大端小端就是数据解析的顺序, 比如我的Command定义为9001. Debug byte array 时候会按照8位一转换

十进制35 = 二进制 00101101
十进制41 = 二进制 00101001
十进制9001 = 二进制 00100011 00101001

大端显示为35,41,小端显示41,35

C#使用BinaryWriter写入MemoryStream时候为小端. 这个要前后端定义好.

而Protobuf的二进制是不分大小端的,具体的可以参考这篇文章

也就是说Protobuf部分的数据,直接交给相应的Parser即可进行转换,不用关心大小端的问题.

如果服务器使用的是大端解析的方式,可以使用Be.IO这个类库去取代相应的BinaryWriterBinaryReader

Encode和Decode

从服务器得到的是二进制数据,如果不附加其他信息是无法直接解析的.最基本的需要附加两个信息.

一个信息是CommandID,比如9001.通过这个和9001对应Proto文件知道需要用TestSyncResponse1进行解析该二进制. 第二个是二进制长度(一次需要处理多个数据包儿时候需要用到)

具体示意代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private byte[] Encode(short _commandID, byte[] _data)
{
var fs = new MemoryStream();
var writer = new BeBinaryWriter(fs, Encoding.BigEndianUnicode);

writer.Write(_commandID);
writer.Write((uint) _data.Length);
writer.Write(_data);

var result = fs.ToArray();
return result;
}

private T Decode<T>(byte[] _data) where T : IMessage, new()
{
var fs = new MemoryStream(_data);
var reader = new BeBinaryReader(fs, Encoding.BigEndianUnicode);

var command = reader.ReadInt16();
var length = reader.ReadInt32();
var rawData = reader.ReadBytes(length);

T message = new T();
message.MergeFrom(rawData);
return message;
}

泛型

以上是在知道具体解析类情(泛型T)况下才可,当对网络层进行独立封装时候.往往无法知晓具体类型
此时可以单独存储生成ProtoClass的MessageParser.然后向上返回IMessage

Decode函数改为

1
2
3
4
5
6
7
8
9
10
11
private IMessage Decode(byte[] _data, MessageParser _parser)
{
var fs = new MemoryStream(_data);
var reader = new BeBinaryReader(fs, Encoding.BigEndianUnicode);

var command = reader.ReadInt16();
var length = reader.ReadInt32();
var rawData = reader.ReadBytes(length);

return _parser.ParseFrom(rawData);
}

上层封装所有生成类的MessageParser引用

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ProtoConfig
{
public void Initialize()
{
p = new ProtoInfo();
p.command = 9001;
p.request = TestSyncRequest1.Parser;
p.response = TestSyncResponse1.Parser;
mList.Add(p);
}

public ProtoInfo p;
}

调用方式为

1
2
3
var a = Decode(response.Data, ProtoConfig.instance.p.response);
var b = a as TestSyncResponse1;
Debug.Log("Response ID : " + b.Id);

Update 2019

不用这么复杂,可以直接传入Proto的定义Class即可

1
2
3
4
5
public static T ReadProtoFromPath<T>(string _path) where T : IMessage<T>, new()
{
var bytes = $YourByteArray;
return new MessageParser<T>(() => new T()).ParseFrom(bytes);
}
坚持原创技术分享,您的支持将鼓励我继续创作!