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类游戏《码神联盟》的服务端架构设计与实现(更新优化思路)
注:本文仅用于在博客园学习分享,还在随着项目不断更新和完善中,多有不足,暂谢绝各平台或个人的转载和推广,感谢支持. 一.前言 <码神联盟>是一款为技术人做的开源情怀游戏,每一种编程语言都是 ...
随机推荐
- 视觉SLAM第四讲李群与李代数习题
视觉SLAM第四讲李群与李代数习题 一.验证\(SO(3).SE(3).SIM(3)\)关于乘法成群 首先引入一下群的定义. 群 (Group) 是一种集合加上一种运算的代数结构.我们把集合记作 \( ...
- javascript 判断浏览器
navigator.userAgent 通常我们可以通过navigator.userAgent只读属性来获取浏览器的一些信息,算是原生方法吧. jquery -jquery1.9 版本可以通过$.br ...
- 探秘Transformer系列之(21)--- MoE
探秘Transformer系列之(21)--- MoE 目录 探秘Transformer系列之(21)--- MoE 0x00 概要 0x01 前置知识 1.1 MoE出现的原因 1.1.1 神经网络 ...
- CH390使用注意事项
关于CH390使用注意事项 CH390替换DM90xx硬件注意事项 1.CH390L替换DM9000 AVDD33的对地电容建议1uF贴近芯片放置,42脚为主电源AVDD33需10uF并联0.1uF. ...
- Spring Cloud Config分布式配置中心
一.Spring Cloud Config分布式配置中心作用:可以通过修改在git仓库中的配置文件实现其它所有微服务的配置文件的修改 二.结构图
- Linux运维基础(二)网络常见问题
问题:网卡地址配置不正确 1.网卡地址和虚拟主机的网卡地址不统一 2.网关和DNS的信息不正确 解决方法:如何重新配置网卡地址信息 步骤一:在命令行中使用"nmtui"命令,回车 ...
- (转)python批量提取PDF第一页输出为图片
一:步骤 1.使用input输入路径 2.生成图片存户路径同存放路径 3.生成图片为PNG格式 4.支持自定义截取页数,建议为第一页 二:安装扩展类 pip install PyMuPDF 三:示例代 ...
- 爬虫项目之爬取4K高清壁纸
爬虫项目之爬取4K高清壁纸 目标网址:4K壁纸高清图片_电脑桌面手机全面屏壁纸4K超清_高清壁纸4K全屏 - 壁纸汇 使用技术Selenium+Requests 下面是目标网页 思路:由于此网页是通过 ...
- ESP32C3语音AI对话代码分析
ESP32C3语音AI对话代码分析 代码:基于立创实战派C3例程删改(LCD屏幕显示,触摸和LVGL)和分析 硬件:立创实战派C3 立创官方例程教程链接:第16章 桌面天气助手 | 立创开发板技术文档 ...
- 深度解析Maven版本仲裁机制:核心规则与原理
结论先行 Maven的版本仲裁机制本质是通过 依赖路径 和 声明顺序 的优先级规则,自动解决多版本依赖冲突.其核心规则为: 最短路径优先:依赖树中路径最短的版本生效. 相同路径则先声明优先:路径长度相 ...