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. Pydantic异步校验器深:构建高并发验证系统

    title: Pydantic异步校验器深:构建高并发验证系统 date: 2025/3/25 updated: 2025/3/25 author: cmdragon excerpt: Pydanti ...

  2. 本地部署overleaf服务帮助latex论文编写

    是的,overleaf是一个很好的服务,提供了立刻上手就可以编写的latex文章的服务.但是,overleaf会面对latex超时,所以需要付钱的情况,这常出现在编写期刊的论文的情况. 因为时效性,所 ...

  3. dxSpreadSheet的报表demo-关于设计报表模板问题

    学习 dxSpreadSheetReportDesigner过程中发现: dxSpreadSheet通过dxSpreadSheetReportDesigner点击右键出现弹出菜单,自动生成如图的菜单和 ...

  4. study Rust-4【所有权】这个太重要了!

    由于Rust内存垃圾自动回收,那就得搞清楚这个所有权玩意.这个太重要了.因为关系到贯穿于你以后的程序编写. 几个概念: 一.移动 1.咱们一般语言,自己申请内存,自己管理和释放.就是new和free. ...

  5. 一些CF上的补题0504

    知识点模块 1.通过三点计算三角形的面积可以这样写 area=fabs(x1*y2-x2*y1+x2*y3-x3*y2+x3*y1-x1*y3)/2; 2.最小公倍数与最大公约数 x×y=gcd(x, ...

  6. volatile修饰全局变量,可以保证线程并发安全吗?

    今天被人问到volatile能不能保证并发安全? 呵,这能难倒我? 直接上代码: public class ThreadTest { // 使用volatile修饰变量 private static ...

  7. 深度学习实战:从零构建图像分类API(Flask/FastAPI版)

    引言:AI时代的图像分类需求 在智能时代,图像分类技术已渗透到医疗影像分析.自动驾驶.工业质检等各个领域.作为开发者,掌握如何将深度学习模型封装为API服务,是实现技术落地的关键一步.本文将手把手教你 ...

  8. 干货分享!MCP 实现原理,小白也能看懂

    不知道大家有没有发现?对于添加到 MCP 服务市场的成千上万个 MCP 服务(而且这个数字每天还在增加),我们可以不写一行代码,轻松实现调用,但背后的原因究竟是啥呢? MCP 虽然用起来很方便,但搞不 ...

  9. app自动化设计

    一.在pom.xml引入依赖 testng:测试框架用例管理 appium:需要用到appium log4j:日志集成 allure:生成报告 二.po分层 分为基础层,page层,用例层,xml文件 ...

  10. App自动化的元素定位

    一.Appium定位步骤 打开appium,输入本地IP,点击启动服务器 1.点击启动检查器会话 2.配置所需功能,点击启动会话 二.App页面元素 App页面元素分为布局和控件两种 1.布局 Fra ...