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. github release 下载文件慢、或者失败的解决方法

    Free Download Manager 下载工具可以解决Github 下载缓慢或失败问题. 扫描下方二维码,关注公众号 回复github,即可获取下载链接.

  2. zk源码—1.数据节点与Watcher机制及权限

    大纲 1.ZooKeeper的数据模型.节点类型与应用 (1)数据模型之树形结构 (2)节点类型与特性(持久 + 临时 + 顺序 ) (3)节点的状态结构(各种zxid + 各种version) (4 ...

  3. 『Plotly实战指南』--直方图绘制与应用

    在数据科学的世界里,直方图是一种不可或缺的可视化工具,它以其简洁而直观的方式,揭示了数据的内在规律,为数据分析和决策提供了有力支持. 它能够帮助我们快速识别异常值,并为统计推断提供有力支撑. 无论是对 ...

  4. RSA 加密及一些攻击方式

    本文章转载自个人博客seandictionary.top同步更新可能不及时 原理 随机生成两个素数,p , q 令n = p*q 由欧拉公式计算出φ(n) = (p-1)(q-1) 规定e,使得e满足 ...

  5. symfony框架安全组件(security)防火墙配置

    最近改了下之前用symfony做的项目,然后,突然发现防火墙失效了,简单说就是再未登录状态下居然可以访问配置了ROLE_USER.ROLE_ADMIN这些权限的路由,symfony框架很强大,但是文档 ...

  6. excel 类模块的使用

    类模块代码如下: '类模块Cmds的代码 Option Explicit Public WithEvents testx As msforms.CommandButton '增加点击事件 Privat ...

  7. DDD领域驱动大纲讲义

    DDD领域驱动模型 什么是领域?什么是领域模型? 没有丰富的领域知识能做出复杂的银行业业务软件吗 ? 没门 . 答案永远是否定的 . 那么谁 了解银行业业务 ? 软件架构师吗 ? 不 , 他只是在使用 ...

  8. 蒟蒻 AstralNahida 的码风

    前言 这里是蒟蒻 OIer AstralNahida 在 OI 中的码风的详细介绍. 个人认为码风相当清晰,供给各位参考. 约定 对于一些表示必要性的关键词,从 must 到 mustn't 排序如下 ...

  9. 代码随想录第一天 | Leecode 704 二分查找、27 移除元素、977 有序数组的平方

    前言 今天是我开始刷Leecode的第一天,同时这也是开通博客园第一篇博客.我希望能在每篇博客中记录下我做出每一道题的过程,为此我想先说明一下我的博客内容的结构. 题目描述:首先说明题目的要求以及测试 ...

  10. vue3 基础-插件 plugin

    前几篇我们介绍了 mixin 混入的的方式能实现对代码的复用, 而本篇将要介绍的 plugin 将会更加适合这种通用性功能的代码的复用和扩展. 最常用的场景, 比如轮播图就非常实用 plugin 来实 ...