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
IActorResuest
IActorResponse

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框架学习笔记-服务器(刚哥)

ET5.0服务端架构的更多相关文章

  1. 从服务端架构设计角度,深入理解大型APP架构升级

    随着智能设备普及和移动互联网发展,移动端应用逐渐成为用户新入口,重要性越来越突出.但企业一般是先有PC端应用,再推APP,APP 1.0版的功能大多从现有PC应用平移过来,没有针对移动自身特点考虑AP ...

  2. Swift3.0服务端开发(一) 完整示例概述及Perfect环境搭建与配置(服务端+iOS端)

    本篇博客算是一个开头,接下来会持续更新使用Swift3.0开发服务端相关的博客.当然,我们使用目前使用Swift开发服务端较为成熟的框架Perfect来实现.Perfect框架是加拿大一个创业团队开发 ...

  3. Swift3.0服务端开发(五) 记事本的开发(iOS端+服务端)

    前边以及陆陆续续的介绍了使用Swift3.0开发的服务端应用程序的Perfect框架.本篇博客就做一个阶段性的总结,做一个完整的实例,其实这个实例在<Swift3.0服务端开发(一)>这篇 ...

  4. 创建自己的OAuth2.0服务端(一)

    如果对OAuth2.0有任何的疑问,请先熟悉OAuth2.0基础的文章:http://www.cnblogs.com/alunchen/p/6956016.html 1. 前言 本篇文章时对 客户端的 ...

  5. oauth2.0服务端与客户端搭建

    oauth2.0服务端与客户端搭建 - 推酷 今天搭建了oauth2.0服务端与客户端.把搭建的过程记录一下.具体实现的功能是:client.ruanwenwu.cn的用户能够通过 server.ru ...

  6. 1年内4次架构调整,谈Nice的服务端架构变迁之路

    Nice 本身是一款照片分享社区类型的应用,在分享照片和生活态度的同时可以在照片上贴上如品牌.地点.兴趣等tag. Nice从2013.10月份上线App Store到目前每天2亿PV,服务端架构经过 ...

  7. http服务端架构演进

    摘要 在详解http报文相关文章中我们介绍了http协议是如何工作的,那么构建一个真实的网站还需要引入组件呢?一些常见的名词到底是什么含义呢? 什么叫正向代理,什么叫反向代理 服务代理与负载均衡的差别 ...

  8. Zabbix5.0服务端部署

    Zabbix5.0服务端部署 基础环境配置 [root@localhost ~]# systemctl disable --now firewalld Removed symlink /etc/sys ...

  9. 谈一款MOBA游戏《码神联盟》的服务端架构设计与实现

    一.前言 <码神联盟>是一款为技术人做的开源情怀游戏,每一种编程语言都是一位英雄.客户端和服务端均使用C#开发,客户端使用Unity3D引擎,数据库使用MySQL.这个MOBA类游戏是笔者 ...

  10. 谈一款MOBA类游戏《码神联盟》的服务端架构设计与实现(更新优化思路)

    注:本文仅用于在博客园学习分享,还在随着项目不断更新和完善中,多有不足,暂谢绝各平台或个人的转载和推广,感谢支持. 一.前言 <码神联盟>是一款为技术人做的开源情怀游戏,每一种编程语言都是 ...

随机推荐

  1. 面试题-Spring和Springboot框架

    前言 spring框架部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定的 ...

  2. 【Python】基础操作

    指定解释器的运行环境 有时候我们会遇见报错 SyntaxError: Non-ASCII character '\xe4' in file E:/PycharmProjects/LEDdisplay2 ...

  3. QwQ-32B:用强化学习打造的AI推理黑科技 🚀

    现在就体验 QwQ-32B:https://qwq32.com AI界的新星闪耀登场 小伙伴们,AI领域又出现重大突破啦!Qwen团队最新发布的QwQ-32B模型简直太厉害了!这个只有320亿参数的模 ...

  4. 基于C#的学生社团管理系统(简单基础版)

    前言 该系统为个人独立编写测试,也算自己的孩子吧,虽然基础功能简单但是也为了大家能有个可以借鉴,可以改写的模版使用,我就写个博客让大家参考,但是拒绝搬运售卖. * 正式介绍 该系统基于C#开发,使用V ...

  5. 一文速通Python并行计算:06 Python多线程编程-基于队列进行通信

    一文速通 Python 并行计算:06 Python 多线程编程-基于队列进行通信 摘要: 队列是一种线性数据结构,支持先进先出(FIFO)操作,常用于解耦生产者和消费者.慢速生产-快速消费场景中,队 ...

  6. 使用SymPy求解矩阵微分方程

    引言 在数学.物理.工程等领域,微分方程常常被用来描述系统的变化和动态过程.对于多变量系统或者多方程系统,矩阵微分方程是非常常见的,它可以用来描述如电路.控制系统.振动系统等复杂的动态行为.今天,我们 ...

  7. macOS 和 Windows 操作系统下如何安装和启动 MySQL / Redis 数据库

    你好,我是 Kagol,个人公众号:前端开源星球(欢迎关注我,分享更多前端开源知识). TinyPro 后台管理系统的 NestJS 后端依赖 MySQL 和 Redis 数据库,本文主要带大家安装和 ...

  8. 『Plotly实战指南』--布局基础篇

    在数据分析与可视化领域,一张优秀的图表不仅需要准确呈现数据,更应通过合理的布局提升信息传达效率,增强专业性和可读性. Plotly作为一款强大的Python可视化库,提供了丰富的布局定制功能,帮助我们 ...

  9. MySQL 中的回表是什么?

    MySQL 中的回表 回表是 MySQL 查询优化中的一个概念,指的是在使用非聚簇索引查询时,无法直接从索引中获取所需的所有数据,需要通过非聚簇索引查找到主键值,然后再去聚簇索引中根据主键值获取完整数 ...

  10. 从源码看 QT 的事件系统及自定义事件

    事件是程序内部或外部触发的动作或状态变化的信号.在 Qt 中,所有事件都是 QEvent 派生类的对象,事件由 QObject 派生类的对象接收和处理.每一个事件都有对应的 QEvent 派生类,当事 ...