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