ET5.0服务端架构
1: 整体架构图(图片来源
注意:现在的客户端与服务器的链接只有Realm和Gate。也就是说,客户端在第一次登陆时链接Realm,然后链接Gate,但是不连接Map。Map与Client之间的通讯完全由Gate中转。
2.1、Manager管理服务器-- AppManagerComponent
主要功能:读取配置文件,每隔5秒检测所有的服务器是否健在,如果不健在,则重启该服务器。
2.2、Realm登录服务器【RealmGateAddressComponent】【RealmGateAddressComponentEx】
主要功能:在收到客户端发来的C2R_LoginHandler消息以后,随机挑选一个Gate,让其加入。
2.3、Gate网关服务器,用户长链接的服务器。
【PlayerComponent】
主要功能:保存玩家信息(目前只有账号和UnitId)。
【NetInnerComponent】
主要功能:与Realm和Map服务器通讯。
【GateSessionKeyComponent】
主要功能:保存所有Gate里的玩家的Session的Key
【ActorLocationSenderComponent】
主要功能:向Map内的指定玩家发送消息,如果发送失败,则向Location服务器索要新的地址。
2.4、Location地址服务器
【LocationComponent】
主要功能:保存了所有玩家的地址(Key是玩家的Id,Value是玩家的InstanceId),如果玩家在切换Map的时候,要把这里锁住。
2.5、Map场景服务器。
【NetInnerComponent】
与Gate通信。注意,Map并不与玩家直接通讯,全都由Gate转发。
【ActorMessageSenderComponent】
与Gate通讯。这里可以获得ActorId,而ActorId是找到对应Map的关键信息:IdGenerater.AppId。
对于开房间的游戏来说,一个Map服务器可能会有很多个房间。
3、消息--重点
3.1:ET中的消息类,是基于Google的Protobuf机制来生成的。分别存放于三个文件中:
InnerMessage.Proto
OuterMessage.Proto
HotfixMessage.Proto
每个消息,也可以有三种类型:
IRequest,此类消息,是发送请求,与IResponse配对,实现一个Rpc调用过程
IResponse,此类消息,是接受请求,与IRequest配对,实现一个Rpc返回过程
IMessage,就是一个单向传输的消息。
IRequest/IResponse消息对,让使用者可以把发送和接受写在一个函数之中(这个函数本身必须是一个协程),这样使用者在写代码的时候,思路比较连贯,代码容易看懂,这就是【RPC(远程过程调用)】。
3.2:Protobuf生成的消息代码
消息定义 | 消息ID | |
Inner | InnerMessage.cs | InnerOpcode.cs |
Outer | OuterMessage.cs | OuterOpcode.cs |
Hotfix | HotfixMessage | HotfixOpcode.cs |
InnerMessage:
InnerMessage因为可能会在一个进程内部互传消息,所以,他们的基类都是自己定义的。 在InnerMessage.cs: [Message(InnerOpcode.M2M_TrasferUnitResponse)] public partial class M2M_TrasferUnitResponse: IResponse { public int RpcId { get; set; } public int Error { get; set; } public string Message { get; set; } public long InstanceId { get; set; } }
OuterMessage:
OuterMessage的基类有两个,一个是Google.Protobuf.IMessage,另一个是自己定义的IMessage。
一个在OuterMessage.cs:
[Message(OuterOpcode.C2G2M_TestActorRequest)] public partial class C2G2M_TestActorRequest : IActorLocationRequest {} [Message(OuterOpcode.M2G2C_TestActorResponse)] public partial class M2G2C_TestActorResponse : IActorLocationResponse {} [Message(OuterOpcode.C2M_TestRequest)] public partial class C2M_TestRequest : IActorLocationRequest {}
HotfixMessage:
HotfixMessage的基类有两个,一个是Google.Protobuf.IMessage,另一个是自己定义的IMessage。
一个在HotfixMessage.cs:
/////////////////////////////////////////////////////////////// // 切换装备 /////////////////////////////////////////////////////////////// [Message(HotfixOpcode.C2M_ChangeEquipRequest)] public partial class C2M_ChangeEquipRequest : IClientRequest {} [Message(HotfixOpcode.M2C_ChangeEquipResponse)] public partial class M2C_ChangeEquipResponse : IClientResponse {} [Message(HotfixOpcode.M2C_UnitChangeEquip)] public partial class M2C_UnitChangeEquip : IClientMessage {}
3.3: 自定义消息
为什么三类消息有的基类是一个,而有的基类则是两个。这是因为,Outer和Hotfix都可能是要通过外网来传递消息的,但是Inner的消息仅需要通过内网,最多只是不同进程来传递消息。
但是就算是Inner也可能存在跨进程或者跨不同的物理服务器来传递消息的可能的,所以,应该如何处理呢?其实原因很简单,那就是网络层其实传递什么样的消息都是可以的,是不是Googgle的Protobuf都可以。只不过自己定义的消息,可能就享受不到Protobuf的一些优点了。比如,对于那些取值为0的消息,Protobuf实际上是不传送的,这样会大幅度减少传输的数据量。
自定义缺省字段:
IRequest需要RpcId字段,用来查询对应的Rpc消息对儿。
IResponse需要RpcId,Error, Message,主要用于返回成功或者失败,还有错误消息。
IMessage没有缺省字段。
namespace ETModel { public interface IMessage { } public interface IRequest: IMessage { int RpcId { get; set; } } public interface IResponse : IMessage { int Error { get; set; } string Message { get; set; } int RpcId { get; set; } } public class ErrorResponse : IResponse { public int Error { get; set; } public string Message { get; set; } public int RpcId { get; set; } } }
4、消息通信
4.1:直接通信
直接通信的消息,只需要:在定义Proto消息的时候,在*.proto文件中,在消息类定义的后面增加注释:
// IRequest
// IResponse
// IMessage
此类消息就是最简单的消息,附加了RpcId等自定义字段。
4.2:Actor通信
通过Actor来通讯,需要在定义Proto消息的时候,在*.proto文件,在类定义的后面增加注释:
// IActorRequest
// IActorResponse
// IActorMessage
此类消息,除了RpcId意外,又增加了一个缺省字段:ActorId。
为什么要使用Actor模型来通讯,ET的原版文档里说明,可以参考:【5.4Actor模型】。
4.3:ActorLocation通信
通过ActorLocation来通讯,需要在定义Proto消息的时候,在*.proto文件,在类定义的后面增加注释:
// IActorLocationRequest
// IActorLocationResponse
// IActorLocationMessage
此类消息,同IActorRequest/IActorResponse/IActorMessage消息。只是在执行的时候有更多的逻辑。
ActorLocation又有什么用,可以参考ET的原版文档:【5.5Actor Location】。
4.4:消息处理
消息被接收到以后,首先判断【消息句柄类型】,使用【消息分发函数】,在【消息集合】里找到对应的进行消息分发,然后传入【消息处理句柄】中处理。
直接消息 | Actor | ActorLocation | |
消息 |
IMessage IRequest IResponse |
IActorMessage |
IActorLocationMessage IActorLocationRequest IActorLocationResponse |
消息句柄类型 |
MessageHandlerAttribute |
ActorMessageHandlerAttribute |
|
消息分发函数 |
IMessageDispatcher MessageDispatcherComponent InnerMessageDispatcher OuterMessageDispatcher |
||
消息集合 |
MessageDispatcherComponent |
ActorMessageDispatcherComponent |
|
消息处理句柄 |
IMHandler AMHandler AMRpcHandler |
IMActorHandler AMActorHandler AMActorRpcHandler |
AMActorLocationHandler AMActorLocationRpcHandler |
不同消息及其对应特性
- 不需要返回结果的消息 IMessage
- 需要返回结果的消息 IRequest
- 用于回复的消息 IResponse
- 不需要返回结果的Actor消息 IActorMessage,IActorLocationMessage
- 需要返回结果的Actor消息 IActorRequest IActorLocationRequest
- 用于回复的Actor消息 IActorResponse IActorLocationResponse
4.5:消息句柄
消息句柄的类型,就是告诉程序,发送给哪个服务器的消息,由哪个消息处理函数来处理。
继承关系:BaseAttribute->MessageHandlerAttribute->ActorMessageHandlerAttribute
[MessageHandler(AppType.AllServer)]//消息句柄类型,指定了消息句柄的类型以后,这个消息就会被分发到指定的服务器,此服务器就会收到这个消息。
public class C2R_PingHandler : AMRpcHandler<C2R_Ping, R2C_Ping> //消息处理句柄 { protected override async ETTask Run(Session session, C2R_Ping request, R2C_Ping response, Action reply) { Log.Info("--收到ping--,返回pong信息--"); reply(); await ETTask.CompletedTask; } }
[ActorMessageHandler(AppType.Map)] public class C2G2M_PingHandler : AMActorLocationRpcHandler<Unit, C2G2M_Ping, M2G2C_Ping> { }
4.5.1、消息处理句柄
最基础的消息处理句柄是IMHandler,向上一层是AMHandler,再往上根据不同的消息类型有不同的继承类。
下面是具体的消息处理句柄的定义了,要注意以下几个关键点:
IMessage:
[MessageHandler(AppType.Benchmark)] public class G2C_TestHandler: AMHandler<G2C_Test> { public static int count = 0; protected override async ETTask Run(Session session, G2C_Test message) { 要通过定义MessageHandler,来表明这是一个普通的消息。在Proto中对应的是,要在消息声明的注释里写明: // IMessage message G2C_Test //IMessage { } AMHandler: 这不是一个Rpc消息,所以只需要继承AMHandler即可。 Run(): Run函数的参数:Sessoin, 解包后的消息类。
IRequest/IResponse:
[MessageHandler(AppType.Gate)] public class C2G_EnterMapHandler : AMRpcHandler<C2G_EnterMap, G2C_EnterMap> { protected override async ETTask Run(Session session, C2G_EnterMap request, G2C_EnterMap response, Action reply) { 要通过定义MessageHandler,来表明这是一个普通的消息。在Proto中对应的是,要在消息声明的注释里写明: // IRequest或IResponse。 message C2G_EnterMap //IRequest { int32 RpcId = 90; int32 msg = 1; } AMRpcHandler: 如果是一个Rpc消息,则要继承AMRpcHandler。 Run(): Run函数的参数:Session,解析后的消息类。包括Request消息和Response消息。
IActorMessage:
[ActorMessageHandler(AppType.Map)] public class Actor_GamerReady_NttHandler : AMActorHandler<Gamer, Actor_GamerReady_Ntt> { protected override void Run(Gamer gamer, Actor_GamerReady_Ntt message) { 定义[ActorMessageHandler(AppType.Map)],表示这个是Actor消息处理。在Proto中 message Actor_GamerReady_Ntt // IActorMessage { int32 RpcId = 90; int64 ActorId = 94; int64 UserID = 1; } AMActorHandler:普通Actor信息需要继承该类。 Run(): 参数-Gamer实体类,表示一个玩家。解析后的数据
IActorRequest/IActorResponse:
//玩家出牌 [ActorMessageHandler(AppType.Map)] public class Actor_GamerPlayCard_ReqHandler : AMActorRpcHandler<Gamer, Actor_GamerPlayCard_Req, Actor_GamerPlayCard_Ack> { protected override async Task Run(Gamer gamer, Actor_GamerPlayCard_Req message, Action<Actor_GamerPlayCard_Ack> reply) { 定义 ActorMessageHandler,表示这个是Actor的Rpc消息,有返回值。 message Actor_GamerPlayCard_Req // IActorRequest { int32 RpcId = 90; int64 ActorId = 91; repeated ETModel.Card Cards = 1; } AMActorRpcHandler:Rpc的Actor消息需要继承该类。 Run():参数是Game玩家实体,解析后的数据,需要返回的数据
IActorLocationMessage:
[ActorMessageHandler(AppType.Map)] public class Frame_ClickMapHandler : AMActorLocationHandler<Unit, Frame_ClickMap> { protected override async ETTask Run(Unit unit, Frame_ClickMap message) { 要通过定义ActorMessageHandler,来表明这是一个Actor (Location)消息。在Proto中对应的是,要在消息声明的注释里写明:// IActorMessage或IActorLocationMessage message Frame_ClickMap // IActorLocationMessage { int32 RpcId = 90; int64 ActorId = 93; int64 Id = 94; float X = 1; float Y = 2; float Z = 3; } AMActorLocationHandler:这不是一个Rpc消息,所以需要AMActorLocationHandler这个即可。 Run():函数的第一个参数是:Unit。后面是解包后的消息类。
IActorLocationRequest/IActorLocationResponse:
[ActorMessageHandler(AppType.Map)] public class C2M_ChangeMapHandler : AMActorLocationRpcHandler<Unit, C2G2M_ChangeMapRequest, M2G2C_ChangeMapResponse> { protected override async ETTask Run(Unit unit, C2G2M_ChangeMapRequest request, M2G2C_ChangeMapResponse response, Action reply) { 要通过定义ActorMessageHandler,来表明这是一个Actor Rpc消息。在Proto中对应的是,要在消息声明的注释里写明:// IActorLocationRequet或IActorLocationResponse message Actor_TransferRequest // IActorLocationRequest { int32 RpcId = 90; int64 ActorId = 93; int32 MapIndex = 1; } AMActorLocationRpcHandler:这是一个Rpc消息,所以需要AMActorLocationRpcHandler作为基类。 Run():函数的第一个参数是:Unit。后面是解包后的消息类,包括发送消息和返回消息。
MailBox:不太明白原理,挂载该组件后,就可以发送Actor消息。
后面会有单独一章简介该组件。参考:
消息 |
IClientRequest/IClientResponse/IClientMessage |
消息句柄类型 |
MailboxHandlerAttribute |
消息集合 |
MailboxDispatcherComponent |
消息句柄处理 |
IMailboxHandler |
4.5.2:Session
Rpc工作流程:通过Call函数,调用Send(Request),同时开启ETTaskCompletionSource协程等待消息返回。消息返回以后,通过Reply()再次调用Send(Response),返回消息。
1) Channel 网络层,保存着:与对方通信的网络通道。
2) RemoteAddress 网络层,保存着:对方通讯的远端地址。
3) Stream 网络层,保存着:尚未解包的原始消息内容。
4) OnRead() 当本通道接收到网络消息以后,这个函数被调用。这里会调用Run()函数来解包。
5) Run() 使用Network.MessagePacker来对原始消息解包。
6) requestCallback 内部函数指针。保存
7) Call() 发送Request消息,且注册一个协程,当协程执行完毕以后,调用Replay()函数反向发送Response消息。
8) Send() 发送消息。
9) Reply() 返回消息。
4.5.3:InnerMessageDispatcher
public class InnerMessageDispatcher: IMessageDispatcher { public void Dispatch(Session session, ushort opcode, object message) { // 收到actor消息,放入actor队列 switch (message) { case IActorRequest iActorRequest: { Entity entity = (Entity)Game.EventSystem.Get(iActorRequest.ActorId); if (entity == null) { Log.Warning($"not found actor: {message}"); ActorResponse response = new ActorResponse { Error = ErrorCode.ERR_NotFoundActor, RpcId = iActorRequest.RpcId }; session.Reply(response); return; }
这时候可以看到ActorId的用处了。程序通过IActorRequest里的ActorId,在EventSystem里找到了对应的Unit单位。这个单位就是发送这条消息的单位。
找到单位的时候,在调用【消息处理句柄】的时候,就可以直接把Unit通过参数传递给消息响应函数。
4.5.4:OuterMessageDispatcher
public async ETVoid DispatchAsync(Session session, ushort opcode, object message) { // 根据消息接口判断是不是Actor消息,不同的接口做不同的处理 switch (message) { case IActorLocationRequest actorLocationRequest: // gate session收到actor rpc消息,先向actor 发送rpc请求,再将请求结果返回客户端 { long unitId = session.GetComponent<SessionPlayerComponent>().Player.UnitId; ActorLocationSender actorLocationSender = Game.Scene.GetComponent<ActorLocationSenderComponent>().Get(unitId); int rpcId = actorLocationRequest.RpcId; // 这里要保存客户端的rpcId long instanceId = session.InstanceId; IResponse response = await actorLocationSender.Call(actorLocationRequest); response.RpcId = rpcId; // session可能已经断开了,所以这里需要判断 if (session.InstanceId == instanceId) { session.Reply(response); } break; }
5:消息集合
MessageDispatcherComponent
ActorMessageDispatcherComponent
这里存放着所有本服务器应该响应的消息集合。收到消息以后,要从这里寻找对应的消息。
Game.Scene.GetComponent<MessageDispatcherComponent>().Handle(session, new MessageInfo(opcode, message)); 。。。。。。 public static void Handle(this MessageDispatcherComponent self, Session session, MessageInfo messageInfo) { List<IMHandler> actions; if (!self.Handlers.TryGetValue(messageInfo.Opcode, out actions)) { Log.Error($"消息没有处理: {messageInfo.Opcode} {JsonHelper.ToJson(messageInfo.Message)}"); return; } foreach (IMHandler ev in actions) { try { ev.Handle(session, messageInfo.Message); } catch (Exception e) { Log.Error(e); } } }
参考:https://www.lfzxb.top/et-master-message/
ET框架学习笔记-服务器(刚哥)