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框架学习笔记-服务器(刚哥)
ET5.0服务端架构的更多相关文章
- 从服务端架构设计角度,深入理解大型APP架构升级
随着智能设备普及和移动互联网发展,移动端应用逐渐成为用户新入口,重要性越来越突出.但企业一般是先有PC端应用,再推APP,APP 1.0版的功能大多从现有PC应用平移过来,没有针对移动自身特点考虑AP ...
- Swift3.0服务端开发(一) 完整示例概述及Perfect环境搭建与配置(服务端+iOS端)
本篇博客算是一个开头,接下来会持续更新使用Swift3.0开发服务端相关的博客.当然,我们使用目前使用Swift开发服务端较为成熟的框架Perfect来实现.Perfect框架是加拿大一个创业团队开发 ...
- Swift3.0服务端开发(五) 记事本的开发(iOS端+服务端)
前边以及陆陆续续的介绍了使用Swift3.0开发的服务端应用程序的Perfect框架.本篇博客就做一个阶段性的总结,做一个完整的实例,其实这个实例在<Swift3.0服务端开发(一)>这篇 ...
- 创建自己的OAuth2.0服务端(一)
如果对OAuth2.0有任何的疑问,请先熟悉OAuth2.0基础的文章:http://www.cnblogs.com/alunchen/p/6956016.html 1. 前言 本篇文章时对 客户端的 ...
- oauth2.0服务端与客户端搭建
oauth2.0服务端与客户端搭建 - 推酷 今天搭建了oauth2.0服务端与客户端.把搭建的过程记录一下.具体实现的功能是:client.ruanwenwu.cn的用户能够通过 server.ru ...
- 1年内4次架构调整,谈Nice的服务端架构变迁之路
Nice 本身是一款照片分享社区类型的应用,在分享照片和生活态度的同时可以在照片上贴上如品牌.地点.兴趣等tag. Nice从2013.10月份上线App Store到目前每天2亿PV,服务端架构经过 ...
- http服务端架构演进
摘要 在详解http报文相关文章中我们介绍了http协议是如何工作的,那么构建一个真实的网站还需要引入组件呢?一些常见的名词到底是什么含义呢? 什么叫正向代理,什么叫反向代理 服务代理与负载均衡的差别 ...
- Zabbix5.0服务端部署
Zabbix5.0服务端部署 基础环境配置 [root@localhost ~]# systemctl disable --now firewalld Removed symlink /etc/sys ...
- 谈一款MOBA游戏《码神联盟》的服务端架构设计与实现
一.前言 <码神联盟>是一款为技术人做的开源情怀游戏,每一种编程语言都是一位英雄.客户端和服务端均使用C#开发,客户端使用Unity3D引擎,数据库使用MySQL.这个MOBA类游戏是笔者 ...
- 谈一款MOBA类游戏《码神联盟》的服务端架构设计与实现(更新优化思路)
注:本文仅用于在博客园学习分享,还在随着项目不断更新和完善中,多有不足,暂谢绝各平台或个人的转载和推广,感谢支持. 一.前言 <码神联盟>是一款为技术人做的开源情怀游戏,每一种编程语言都是 ...
随机推荐
- 面试题-Spring和Springboot框架
前言 spring框架部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定的 ...
- 【Python】基础操作
指定解释器的运行环境 有时候我们会遇见报错 SyntaxError: Non-ASCII character '\xe4' in file E:/PycharmProjects/LEDdisplay2 ...
- QwQ-32B:用强化学习打造的AI推理黑科技 🚀
现在就体验 QwQ-32B:https://qwq32.com AI界的新星闪耀登场 小伙伴们,AI领域又出现重大突破啦!Qwen团队最新发布的QwQ-32B模型简直太厉害了!这个只有320亿参数的模 ...
- 基于C#的学生社团管理系统(简单基础版)
前言 该系统为个人独立编写测试,也算自己的孩子吧,虽然基础功能简单但是也为了大家能有个可以借鉴,可以改写的模版使用,我就写个博客让大家参考,但是拒绝搬运售卖. * 正式介绍 该系统基于C#开发,使用V ...
- 一文速通Python并行计算:06 Python多线程编程-基于队列进行通信
一文速通 Python 并行计算:06 Python 多线程编程-基于队列进行通信 摘要: 队列是一种线性数据结构,支持先进先出(FIFO)操作,常用于解耦生产者和消费者.慢速生产-快速消费场景中,队 ...
- 使用SymPy求解矩阵微分方程
引言 在数学.物理.工程等领域,微分方程常常被用来描述系统的变化和动态过程.对于多变量系统或者多方程系统,矩阵微分方程是非常常见的,它可以用来描述如电路.控制系统.振动系统等复杂的动态行为.今天,我们 ...
- macOS 和 Windows 操作系统下如何安装和启动 MySQL / Redis 数据库
你好,我是 Kagol,个人公众号:前端开源星球(欢迎关注我,分享更多前端开源知识). TinyPro 后台管理系统的 NestJS 后端依赖 MySQL 和 Redis 数据库,本文主要带大家安装和 ...
- 『Plotly实战指南』--布局基础篇
在数据分析与可视化领域,一张优秀的图表不仅需要准确呈现数据,更应通过合理的布局提升信息传达效率,增强专业性和可读性. Plotly作为一款强大的Python可视化库,提供了丰富的布局定制功能,帮助我们 ...
- MySQL 中的回表是什么?
MySQL 中的回表 回表是 MySQL 查询优化中的一个概念,指的是在使用非聚簇索引查询时,无法直接从索引中获取所需的所有数据,需要通过非聚簇索引查找到主键值,然后再去聚簇索引中根据主键值获取完整数 ...
- 从源码看 QT 的事件系统及自定义事件
事件是程序内部或外部触发的动作或状态变化的信号.在 Qt 中,所有事件都是 QEvent 派生类的对象,事件由 QObject 派生类的对象接收和处理.每一个事件都有对应的 QEvent 派生类,当事 ...