框架的前后端通讯

protobuf

1.什么是protobuf?

protobuf是Google公司提出的一种轻便高效的结构化数据存储格式,常用于结构化数据的序列化,具有语言无关、平台无关、可扩展性特性,常用于通讯协议、服务端数据交换场景

2.为什么要使用protobuf作为数据传输协议?
protobuf优势:
1)类型安全
2)易用性好
3)序列化/反序列性能好
4)兼容性好
5)不仅可以定义结构体,还可以定义rpc服务接口

劣势:
1)可读性较差:没有schema的情况下,难以阅读和编辑。
2)灵活性较差:无法动态修改schema。

json: https://www.json.org/json-zh.html
优势:
1)可读性好:方便理解和编辑
2)易用性好:使用简单灵活性好:支持动态修改schema

劣势:序列化/反序列化性能差编码问题导致解析失败之类的

PS:在我做过的几款项目中,前面两款都是用protobuf消息协议,后面的项目使用的是Json,可能是protobuf用惯了,
在使用Json时感受到了其反序列化时的痛苦,而且有时候后端偷偷删掉了某个字段还会导致解析失败,简直是头疼。

3.Protobuf为什么这么快?

一个protobuf消息:

1
2
3
4
5
6
7
message Person
{
required int32 Id = 1;
required string Name = 2;
required string Hobby = 3;
required string Introduction = 4;
}

假如一样的消息通过Json来传,每个消息体需要把某个数据是属于哪个字段的信息一起下发(例如 id : 1), 而protobuf不需要将这个字段名一起带到消息中,那它是如何区分数据属于哪个字段呢:

现在有个Person消息是这样的 id = 1, Name = Cwp, Hobby = Play tennis, Introduction = Hello word

protobuf会将这个消息以这种形式写入

Tag1TagCwpTagPlay tennisTagHello Word
Tag是如何来的?
1
2
3
4
static int makeTag(final int fieldNumber, final int wireType)
{
return(fieldNumber << 3) | wireType
}
fieldNumer:即消息定义时 “=” 号右边的数字,这也就是为什么定义完一个消息后就不允许再修改这个值的原因(修改后Tag也会变,从而导致解析失败)
wireType:字段类型,即int32, string等,protobuf支持的类型如下图, 0-5转为二进制最高用3位就能表示全,故需要将fieldNumber左移三位,后三位用wireType的二进制补全,这样解析的时候只需要取后三位就能知道类型

Tag分隔符为一个字节,如果传输的内容中出现相同的字节,那么不会导致解析错误吗?

实际上是不会的,protobuf避免了这种情况:

  例如有个数据为267, 用二进制表示是 
  
  00000000 00000000 00000001 00001011 (256 + 8 + 2 + 1)
  其中前两个字节都是0,属于无效字节,protobuf会将其舍弃,上述字节变成以下形式
  
  10001011 00000010 ,每个字节的后面七位才是属于数据本身, 第一个字节(10001011)中后七位取的是原数据二进制的低
  七位(大小端?,这里没有进一步了解,先记录下来), 取完后发现再往前还有有效数据(往前还有非0的数据),所以第8位置
  为1, 1代表下一个字节(00000010)还是属于该数据, 第二个字节(00000010)是从原二进制右移七位后截取低7位截取的,
  取完后此时再往前的数据都是0,所以第8位置0,这个时候代表下一个字节的数据已经不是该数据(267)了。
  
  另外,protobuf是以Tag-Length-Value形式编码的(Tag为分隔符,Length为数据长度,Value则是数据),那么如果一开始
  是要传输一个数字,取到第一个tag的时候,解析出它的fieldNumber和writeType, 如果高位为1则表示下一个字节还是该数字,
  如果为0则表示下一个字节是Tag. 而如果一开始传输的是一个字符串,拿到Tag后,就知道下一个数据是一个字符串,所以下一
  个字节开始作为Length解析,遇到0的话就知道value的长度了(字节解析后的值),然后根据这个长度去取出字符串,取完后
  下一个字节就是Tag了,以此类推, protobuf永远知道哪一个字节是Tag
  
  

以上内容参考自:
B站 Protobuf为什么这么快?
知乎Protobuf编码原理

在框架中使用protobuf

在 工程目录/Proto目录 下有四个文件:

InnerMessage.proto :服务器内部通讯的消息数据
MongoMessage.proto :服务器内部通讯的消息数据,可以传输entity
OuterMessage.proto : 用来定义客户端发送到服务端的消息数据
win_startProtoExport.bat :定义完消息数据后执行这个文件,会生成对应的类在项目代码中

OuterMessage

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
27
28
29
//ResponseType M2C_TestResponse // 标明回复的消息类型是M2C_TestResponse
message C2M_TestRequest // IActorLocationRequest
{
int32 RpcId = 90; // RpcId标明这个消息需要后端回复
string request = 1;
}

message M2C_TestResponse // IActorLocationResponse
{
int32 RpcId = 90;
int32 Error = 91;
string Message = 92;
string response = 1;
}

IActorLocationRequest : 标明这个请求需要后端回复
IActorLocationResponse : 标明这个消息是后端回复给客户端的

// 客户端发送的不需要回复的消息
message C2R_Test // IMessage
{
string Test1 = 1:
}

// 后端发送的不需要回复的消息
message R2C_Test // IMessage
{
string Test = 1;
}

ET中前后端通讯的消息分为两类

1) 客户端可以直接发送给服务端的消息
2) 客户端消息必须通过网管服转发到服务端,此类消息在框架中被称为ActorLocation消息

框架中如何使用

客户端发送消息的伪代码如下:

1
2
3
4
5
6
7
8
9
10
Seeion session = zoneScene.GetComponent<NetKcpComponent>().Create(NetWorkHelper.ToIPEndPoint(address));

// 使用后Call是需要后端回复的请求
R2C_Login r2c_Login = (R2C_Login) await session.Call(new R2c_Login() {...});

// 回复的结果 r2c_Login.xxx

// 使用Send是不需要后端回复的消息
session.Send(new C2R_Test());

后端处理消息的伪代码如下:

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
27
28
// 这个标签表示这个函数使用来处理消息请求的
[MessageHandler]

// AMRpcHandle说明是需要回复的消息
public class C2R_LoginHandler : AMRpcHandle<C2R_Login, R2C_Login> // 消息请求以及回复的类型
{
protected override async ETTask Run(Session session, C2R_Login request, R2C_Login response, Action reply)
{
// request 请求的内容
// response要回复的内容
// 处理完毕
reply();
await ETTask.CompletedTask;
}
}

[MessageHandler]
// AMHandler是不需要回复的
public class C2R_Test : AMHandler<C2RTest>
{
protected override async ETTask Run(Session session, C2R_Test message)
{

// 后端也可以直接下推消息
session.Send(new R2C_Test());
await ETTask.CompletedTask;
}
}

客户端处理后端主动推送消息的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11

[MessageHandler]
public class R2C_Test : AMHandler<R2C_Test>
{
protected override async ETTask Run(Session session, R2C_Test message)
{

...
}
}

注:上面的C2R_Login是IActorReueqst类型,在ET中还有一种IActorLocationRequest类型,这两种类型的区分在这里先不记录,在
后面开发有遇到的时候再研究其区分。


框架的前后端通讯
http://example.com/2023/03/25/框架的前后端通讯/
Author
John Doe
Posted on
March 25, 2023
Licensed under