Unity中使用Protobuf进行网络通讯

环境搭建

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);
坚持原创技术分享,您的支持将鼓励我继续创作!