ET5.0服务端架构

 

1: 整体架构图(图片来源

 

 

注意:现在的客户端与服务器的链接只有Realm和Gate。也就是说,客户端在第一次登陆时链接Realm,然后链接Gate,但是不连接MapMapClient之间的通讯完全由Gate中转。

 

2.1、Manager管理服务器-- AppManagerComponent

主要功能:读取配置文件,每隔5秒检测所有的服务器是否健在,如果不健在,则重启该服务器。

 

2.2、Realm登录服务器【RealmGateAddressComponent】【RealmGateAddressComponentEx】

主要功能:在收到客户端发来的C2R_LoginHandler消息以后,随机挑选一个Gate,让其加入。

 

2.3、Gate网关服务器,用户长链接的服务器。

【PlayerComponent】

主要功能:保存玩家信息(目前只有账号和UnitId)。

NetInnerComponent】

主要功能:与Realm和Map服务器通讯。

【GateSessionKeyComponent】

主要功能:保存所有Gate里的玩家的SessionKey

【ActorLocationSenderComponent】

主要功能:向Map内的指定玩家发送消息,如果发送失败,则向Location服务器索要新的地址。

 

2.4、Location地址服务器

【LocationComponent】

主要功能:保存了所有玩家的地址(Key是玩家的IdValue是玩家的InstanceId),如果玩家在切换Map的时候,要把这里锁住。

 

2.5、Map场景服务器。

【NetInnerComponent】

Gate通信。注意,Map并不与玩家直接通讯,全都由Gate转发。

【ActorMessageSenderComponent】

Gate通讯。这里可以获得ActorId,而ActorId是找到对应Map的关键信息:IdGenerater.AppId。

对于开房间的游戏来说,一个Map服务器可能会有很多个房间。

 

3、消息--重点

3.1:ET中的消息类,是基于GoogleProtobuf机制来生成的。分别存放于三个文件中:

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也可能存在跨进程或者跨不同的物理服务器来传递消息的可能的,所以,应该如何处理呢?其实原因很简单,那就是网络层其实传递什么样的消息都是可以的,是不是GooggleProtobuf都可以。只不过自己定义的消息,可能就享受不到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框架学习笔记-服务器(刚哥)

 

posted @ 2021-03-05 15:22  Joy_CShow  阅读(1831)  评论(0编辑  收藏  举报