ET6.0框架下基于Protobuf的网络通讯
文章目录
Protobuf的介绍
序列化和反序列化
序列化:将程序中的对象序列化成结构化数据存储格式文件(json,xml,bytes等)
反序列化:将结构化数据存储格式,反序列化为程序的对象。
序列化和反序列化机制弥补了数据在不同平台或设备之间的差异,经过序列化后的字节流可以在网络中进行传输,且最终可以在其他平台或设备上通过反序列化恢复为传送之前的程序对象。
网络通信最终必须序列化为字节流进行传递
Protobuf简介
Protobuf是一种开源的结构化数据存储格式,常用于结构化序列化,具有语言无关、平台无关、可拓展的特性,通常用于通讯协议、服务端数据交换等场景。
对于开发者只需要定义所要处理的数据的数据结构之后,就可以利用Protobuf所提供的代码生成工具生成相关的代码(通讯消息类)。
初学者定义: Protobuf就是一个用于生成通讯消息类的代码生成器。
Protobuf的使用
结构描述文件基本信息
- proto文件位置:ET/Proto/* 其下有三个.proto文件
- 推荐用vscode查看,添加相关插件高亮代码
- 文件夹下的.bat文件在修改过proto文件后需要双击运行
- OuterMessage.proto 文件存放客户端和服务端通信的消息类型
- InnerMessage.proto 文件存放服务端之间通信的消息类型
- MongoMessage.proto 文件存放服务端之间通信含有实体的消息类型(和InnerMessage序列化机制不同)
结构描述文件使用规则
-
开头的synatx = “proto3” 代表proto版本
-
package ET; 描述了生成的代码文件的命名空间在ET下
-
定义消息的模板 利用关键字message
-
//特性
-
message 消息名 //特性
{
变量类型 变量名 = 标识符id ;
int32 RpcId = 90;
string request = 1;
repeated int Key = 2;
}
-
一个message中的标识符id一定不能重复!
-
不支持字典类型,可通过两个列表repeated替代字典
结构描述文件及生成的源码对比图
普通网络通讯消息(直接通信无需转发)
普通消息的消息类型
-
Request消息
-
需要在发送消息后有回复的消息
-
//ResponseType R2C_LoginTest message C2R_LoginTest //IRequest { int32 RpcId = 90; string Account = 1; string Password = 2; }
-
消息必须施加IRequest特性且在第一行指明回复的消息类型,回复的消息类型必须是IResponse特性
-
Request消息必须具有RpcId变量且标识符必须为90
-
-
Response消息
-
用以回复Request的消息
-
message R2C_LoginTest //IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; string GateAddress = 1; string Key =2; }
-
前三个RpcId Error Message的字段名和标识符固定
-
一般系统所用变量标识符都是90开始递增且固定的,游戏逻辑所用标识符从1开始递增可自定义
-
-
Message消息
-
发送一条不需要回复的消息
-
message C2R_SayHello //IMessage { string Hello = 1; }
-
此类型无需强制添加一些RpcId等标识
-
客户端与服务端通信步骤
由于网络通信的特殊性易错性,以下步骤需在try catch中进行
客户端向服务端发送消息
- 与服务器创建连接—ET框架中即为创建session对象
- 利用session对象提供的
Call(消息类型对象)
方法发送消息需要回复的消息 一定要加上await关键字。 - 利用session对象提供的Send(
消息类型对象
)方法发送的消息不需要回复,无需加await关键字。 - 如果发送的消息需要回复,则创建Response消息类型对象接收Call的返回值,即为回复的消息类型对象。
服务端接收客户端消息 进行处理
-
在Server/Server.Hotfix下创建处理类型 命名规范:消息类型名+Handler,所有消息都需添加[MessageHandler]特性
- 针对Request消息,需要实现AMRpcHandler<Request消息类型,Response消息类型>接口 实现的函数为异步函数
- Run函数的session是连接发送方客户端的session
- request和response参数为接收到的消息和回复的消息
- reply委托调用时就会将response消息发回客户端
- ===========================================
- 针对Message消息,需要实现AMHandler<Message消息类型>接口 Run函数仍为异步函数
- Run函数的session参数是连接发送放客户端的session
- message参数为接收到的消息
示例源码
LoginHelper.cs Client 到 Server
namespace ET
{
public static class LoginHelper
{
public static async ETTask LoginTest(Scene scene,string address)
{
//定义变量
Session session = null;
//用以接收回复的消息
R2C_LoginTest r2cLoginTest = null;
try
{
//与服务器建立连接
session = scene.GetComponent<NetKcpComponent>().Create(NetworkHelper.ToIPEndPoint(address));
{
//发送Request消息并接收返回消息
r2cLoginTest = await session.Call(new C2R_LoginTest()
{
account = "",
password = ""
}) as R2C_LoginTest;
Log.Debug(r2cLoginTest.Key);
//直接向服务端发送消息
session.Send(new C2R_SayHello() { Hello = "Hello World" });
}
}
catch(Exception e)
{
Log.Debug(e.ToString());
}
}
}
}
C2R_LoginTestHandler.cs
namespace ET
{
[MessageHandler]
public class C2R_LoginTestHandler : AMRpcHandler<C2R_LoginTest, R2C_LoginTest>
{
protected async override ETTask Run(Session session, C2R_LoginTest request, R2C_LoginTest response, Action reply)
{
//无需重新new
response.Key = "1111111111";
//回复消息
reply();
await ETTask.CompletedTask;
}
}
}
C2R_SayHelloHandler.cs
namespace ET
{
[MessageHandler]
public class C2R_SayHelloHandler : AMHandler<C2R_SayHello>
{
protected async override ETTask Run(Session session, C2R_SayHello message)
{
Log.Debug(message.Hello);
await ETTask.CompletedTask;
}
}
}
ActorLocation网络通讯消息(经网关进行转发)
Actor模型介绍
Actor就是服务器之间能够传递消息的实体
- Actor之间具有隔离性,有助于消除共享状态,而不用使用复杂的多线程锁机制
- 每个Actor都有一个MailBox邮箱组件用来接收其它Actor发来的消息,MailBox即一个消息队列
- Actor会依次处理MailBox中的消息,其可以阻塞自己处理的逻辑却不能阻塞自己运行的进程。
- Actor之间传递信息依靠的是InstanceId定位到指定Actor下,这是一个结构体里面存有Actor目前所在的进程等信息,可以唯一确定一个Actor当前的位置。
- 由于Actor实体往往可以在服务进程中穿梭,InstanceId会发生改变,这时使用ID作为Actor的唯一且不变的标识,就可以更加准确的找到目标Actor的InstanceId地址,这就是ActorLocation模型,添加一个Location服务器缓存 ID到InstanceId的映射,当Actor位置发生改变时,会通知Location服务器更新映射表,将其最新的位置进行上传。
Actor模型的注意事项
- Actor模型是纯粹的服务端消息通信机制,跟客户端没有关系!
- 客户端发送消息不会知晓其是否为actor消息依然是用session进行的收发消息,只有到了gate服务器才会进行判断消息类型,如果是Actor消息,然后gate服务器才会找到目标Actor的InstanceId,进而转发到指定Map服务器下的Actor中。(ActorLocation消息会多进行一步去Location服务器下获取InstanceId)
- 基于以上的性质,客户端在发送IActor或IActorLocation消息时仍是使用session.send或call,发送到网关服务器下。同理服务端下发的Actor消息只是发给网关服务器的,网关服务器再通过Session转发给客户端,所以在客户端编写处理函数时也只需当处理IMessage一样即可,因为本质上客户端收到的还是网关服务器通过session下发的消息。
- 在多个服务器之间传递IActor消息时,则处理消息必须当作IActor消息处理,需要继承AMActorHandler。
Actor消息的发送和处理问题
- 普通消息处理函数的第一个参数为Session直连的对象,而Actor消息则是一个Actor实体,这个实体就是真正接收到消息的Actor实体。
- 一般Inner服务器传递Actor消息都是直接
ActorMessageSenderComponent.Instance.Call(long actorId,消息);
通过显示指明目标actorId进行消息的传递,接收方即是符合此ActorId的Actor实体,也就是处理函数的第一个参数。 - 而客户端在传递所谓的Actor消息只是向gate服务器利用session发送一条和普通消息无差别的actor消息,真正的Actor消息传递全部都由gate服务器来实现,gate转发需要actorId和消息,actorId的获取利用客户端一开始登录进Map服务器时创建的unit映射关系即可获取,消息则直接转发客户端发送的actor消息即可。
Actor消息的类型
-
IActorRequest消息
-
需要得到回复的请求Actor消息 一般用于InnerMessage中
-
//ResponseType A2M_Reload message M2A_Reload // IActorRequest { int32 RpcId = 90; }
-
Request Response类型消息必须要有RpcId
-
-
IActorResponse消息
-
回复IActorRequest的消息
-
message A2M_Reload // IActorResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; }
-
Response消息必须有Error和Message变量
-
-
IActorMessage消息
-
无需得到回复的Actor消息 可用内部传递或下发至客户端
-
message M2C_TestActorMessage //IActorMessage { string Content = 1; }
-
由于无需请求回复也不和Location服务器交互 无需RpcId等变量
-
-
IActorLocationRequest
-
需要得到回复的请求ActorLocation消息,采用Location机制,适用于经常位置变化的Actor实体(客户端在服务端的Actor映射以及服务端内一些特殊的Actor)
-
//ResponseType M2C_TestActorLocationResponse message C2M_TestActorLocationRequest //IActorLocationRequest { int32 RpcId = 90; string Content = 1; }
-
与Location交互以及请求回复均需要RpcId
-
-
IActorLocationResponse
-
用于回复IActorLocationRequest消息
-
message M2C_TestActorLocationResponse //IActorLocationResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; string Content = 1; }
-
Reponse消息类型的老三样 RpcId Error Message
-
-
IActorLocationMessage
-
无需回复的IActorLocationRequest 消息
-
message C2M_TestActorLocationMessage //IActorLocationMessage { int RpcId = 90; string Content = 1; }
-
需要与Location服务器交互,必须有RpcId字段
-
为什么客户端到map服务端的Actor消息要用ActorLocation而map服务端下发到客户端则直接用Actor消息即可?
首先客户端根map服务端无直接联系,真正和map服务端进行Actor通信的是gate网关服务器,客户端到map服务端的通信本质上是gate到map的Actor通信,一般一个gate会连接多个map服务器,gate到map通信需要确定 游戏对象实体unit的map位置,由于其易变性,需要用到Location服务器所以是ActorLocation消息。而map到客户端本质上是map到gate网关的传递,一般一个map是连接在一个gate上的,所以无需使用Location定位等操作,直接发送Actor消息即可。
Actor消息的传递步骤
服务端之间的Actor消息传递
- 发送方获取目标的ActorId(一般从config中获取)并根据需求定义好要发送的消息类型(6种Actor消息)
- 调用
await ActorMessageSenderComponent.Instance.Call(long actorId,Request消息);
进行Request消息的发送 - 调用
ActorMessageSenderComponent.Instance.Send(long actorId,Message消息);
进行Message消息的发送 - 定义消息的处理函数,针对Request消息继承AMActorRpcHandler或AMActorLocationRpcHandler,其中泛型的第一个参数为Actor实体的类型(例如Unit或Scene),第2,3个参数为request和response的消息的类型。
- 针对Message消息继承AMActorHandler或AMActorLocationHandler,泛型和上面一致
- 处理函数Run的第一个参数为Actor实体类型的对象,即接收方(处理方)的Actor实体类型对象(例如Actor1向Actor2发送一条Actor消息,Actor2中定义的处理函数的第一个参数的对象是Actor2)
客户端和服务端的Actor消息传递
- 获取ZoneScene的Session对象,即获取和网关服务器的连接
- 利用session发送Actor消息到网关服务器
- 网关服务器在接收到消息判断其为Actor消息后会自行进行gate到map服务器的actor消息转发(自动进行无需操作)
- 在服务端定义消息的处理函数,服务端接收到的消息均为gate转发的Actor型消息,所需继承的相关接口和上述456条一致
- 若在Map服务端想下发到客户端,由于map服务端对应网关唯一,无需Location只需发送IActorMessage消息即可发送到网关服务器
- 客户端在接收处理map服务端的actor信息本质上接收的是网关服务器通过session转发到客户端的普通信息,所以客户端在处理时只需实现普通消息AMHandler接口即可。
示例源码
SceneChangeHelper.cs
namespace ET
{
public static class SceneChangeHelper
{
// 场景切换协程
public static async ETTask SceneChangeTo(Scene zoneScene, string sceneName, long sceneInstanceId)
{
try
{
//获取和Gate网关服务器连接的session
Session session = zoneScene.GetComponent<SessionComponent>().Session;
//客户端本质上不知道发送的是Actor消息,仍用session方式发送,到gate处会进行判断处理和转发
var m2c_TestActorLocationRequest = await session.Call(new C2M_TestActorLocationRequest(){ Content = "11111"}) as M2C_TestActorLocationResponse;
Log.Warning(m2c_TestActorLocationRequest.Content);
//发送ActorLocation消息
session.Send(new C2M_TestActorLocationMessage() { Content = "ababab" });
}
catch(Exception e)
{
Log.Error(e.ToString());
}
}
}
}
C2MTestActorLocationRequestHandler.cs
namespace ET
{
public class C2MTestActorLocationRequestHandler : AMActorLocationRpcHandler<Unit, C2M_TestActorLocationRequest, M2C_TestActorLocationResponse>
{
protected async override ETTask Run(Unit unit, C2M_TestActorLocationRequest request, M2C_TestActorLocationResponse response, Action reply)
{
//此处Unit是发送方客户端unit对应映射的服务器下的unit
response.Content = "123456";
reply();
await ETTask.CompletedTask;
}
}
}
C2MTestActorLocationMessageHandler.cs
namespace ET
{
public class C2MTestActorLocationMessageHandler : AMActorLocationHandler<Unit, C2M_TestActorLocationMessage>
{
protected async override ETTask Run(Unit unit, C2M_TestActorLocationMessage message)
{
//这里使用的是Actor消息,无需
M2C_TestActorMessage tmp = new M2C_TestActorMessage() { Content = "M2C Message" };
//这里传递unit是为了利用unit下的gateSessionActorId寻找指定的gate网关服务器
//再利用网关服务器的映射关系转发给对应的客户端消息
MessageHelper.SendToClient(unit,tmp);
await ETTask.CompletedTask;
}
}
}
M2C_TestActorMessageHandler.cs
namespace ET
{
[MessageHandler]
public class M2C_TestActorMessageHandler : AMHandler<M2C_TestActorMessage>
{
protected async override ETTask Run(Session session, M2C_TestActorMessage message)
{
//虽然服务器下发的是actor消息,但actor消息本质还是服务器内部的通讯,网关转发到客户端的时候
//实际上还是发送的普通消息 IMessage,所以这里是session
Log.Debug(message.Content);
await ETTask.CompletedTask;
}
}
}