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框架學習筆記-服務器(剛哥)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM