Actor模型 Actor介紹 在討論Actor模型之前先要討論下ET的架構,游戲伺服器為了利用多核一般有兩種架構,單線程多進程跟單進程多線程架構。兩種架構本質上其實區別不大,因為游戲邏輯開發都需要用單線程,即使是單進程多線程架構,也要用一定的方法保證單線程開發邏輯。ET採用的是單線程多進程的架構, ...
Actor模型
Actor介紹
在討論Actor模型之前先要討論下ET的架構,游戲伺服器為了利用多核一般有兩種架構,單線程多進程跟單進程多線程架構。兩種架構本質上其實區別不大,因為游戲邏輯開發都需要用單線程,即使是單進程多線程架構,也要用一定的方法保證單線程開發邏輯。ET採用的是單線程多進程的架構,而傳統Actor模型一般是單進程多線程的架構,這點是比較大的區別,不能說誰更好,只能說各有優勢。優劣如下:
- 邏輯需要單線程這點都是一樣的,erlang進程邏輯是單線程的,skynet lua虛擬機也是單線程的。ET中一個進程其實相當於一個erlang進程,一個skynet lua虛擬機。
- 採用單線程多進程不需要自己再寫一套profiler工具,可以利用很多現成的profiler工具,例如查看記憶體,cpu占用直接用top命令,這點erlang跟skynet都需要自己另外搞一套工具。
- 多進程單線程架構還有個好處,單台物理機跟多台物理機是沒有區別的,單進程多線程還需要考慮多台物理機的處理。
- 多進程單線程架構一點缺陷是消息跨進程需要進行序列化反序列化,占用一點資源。另外發送網路消息會有幾毫秒延時。一般這些影響可以忽略。
最開始Actor模型是給單進程多線程架構使用的,這是有原因的,因為多線程架構開發者很容易隨意的訪問共用變數,比方說一個變數a, 線程1能訪問,線程2也能訪問,這樣兩個線程在訪問變數a的時候都需要加鎖,共用變數多了之後鎖到處都是,會變得無法維護,框架肯定不能出現到處是線程共用變數的情況。為了保證多線程架構不出問題,必須提供一種開發模型保證多線程開發簡單又安全。erlang語言的併發機制就是actor模型。erlang虛擬機使用多線程來利用多核。erlang設計了一種機制,它在虛擬機之上設計了自己的進程。最簡單的,每個erlang進程都管理自己的變數,每個erlang進程的邏輯都跑在一個線程上,erlang進程跟進程之間邏輯完全隔離,這樣就不存在兩個線程訪問同一變數的情況了也就不存在多線程競爭的問題。接下來問題又出現了,既然每個erlang進程都有自己的數據,邏輯完全是隔離的,兩個erlang進程之間應該怎麼進行通信呢?這時Actor模型就登場了。erlang設計了一種消息機制:一個進程可以向其它進程發送消息,erlang進程之間通過消息來進行通信,看到這會不會感覺很熟悉?這不就是操作系統進程間通信用的消息隊列嗎?沒錯,其實是類似的。erlang裡面拿到進程的id就能給這個進程發送消息。
如果消息只發給進程其實還是有點不方便。比如拿一個erlang進程做moba戰隊進程,戰鬥進程中有10個玩家,如果使用erlang的actor消息,消息只能發送給戰鬥進程,但是很多時候消息是需要發送給一個玩家的,這時erlang需要根據消息中的玩家Id,把消息再次分發給具體的玩家,這樣其實多繞了一圈。
ET的Actor
ET根據自己架構得特點,沒有完全照搬erlang的Actor模型,而是提供了Entity對象級別的Actor模型。這點跟erlang甚至傳統的Actor機制不一樣。ET中,Actor是Entity對象,Entity掛上一個MailboxComponent組件就是一個Actor了。只需要知道Entity的InstanceId就可以發消息給這個Entity了。其實erlang的Actor模型不過是ET中的一種特例,比如給ET服務端Game.Scene當做一個Actor,這樣就可以變成進程級別的Actor。Actor本質就是一種消息機制,這種消息機制不用關心位置,只需要知道對方的InstanceId(ET)或者進程的Pid(erlang)就能發給對方。
語言 | ET | Erlang | Skynet |
---|---|---|---|
架構 | 單線程多進程 | 單進程多線程 | 單進程多線程 |
Actor | Entity | erlang進程 | lua虛擬機 |
ActorId | Entity.InstanceId | erlang進程Id | 服務地址 |
ET的Actor的使用
普通的Actor,我們可以參照Gate Session。map中一個Unit,Unit身上保存了這個玩家對應的gate session。這樣,map中的消息如果需要發給客戶端,只需要把消息發送給gate session,gate session在收到消息的時候轉發給客戶端即可。map進程發送消息給gate session就是典型的actor模型。它不需要知道gate session的位置,只需要知道它的InstanceId即可。MessageHelper.cs中,通過GateSessionActorId獲取一個ActorMessageSender,然後發送。
// 從Game.Scene上獲取ActorSenderComponent,然後通過InstanceId獲取ActorMessageSender ActorSenderComponent actorSenderComponent = Game.Scene.GetComponent<ActorSenderComponent>(); ActorMessageSender actorMessageSender = actorSenderComponent.Get(unitGateComponent.GateSessionActorId); // send actorMessageSender.Send(message); // rpc var response = actorMessageSender.Call(message);
問題是map中怎麼才能知道gate session的InstanceId呢?這就是你需要想方設法傳過去了,比如ET中,玩家在登錄gate的時候,gate session掛上一個信箱MailBoxComponent,C2G_LoginGateHandler.cs中
session.AddComponent<MailBoxComponent, string>(MailboxType.GateSession);
玩家登錄map進程的時候會把這個gate session的InstanceId帶進map中去,C2G_EnterMapHandler.cs中
M2G_CreateUnit createUnit = (M2G_CreateUnit)await mapSession.Call(new G2M_CreateUnit() { PlayerId = player.Id, GateSessionId = session.InstanceId });
Actor消息的處理
首先,消息到達MailboxComponent,MailboxComponent是有類型的,不同的類型郵箱可以做不同的處理。目前有兩種郵箱類型GateSession跟MessageDispatcher。GateSession郵箱在收到消息的時候會立即轉發給客戶端,MessageDispatcher類型會再次對Actor消息進行分發到具體的Handler處理,預設的MailboxComponent類型是MessageDispatcher。自定義一個郵箱類型也很簡單,繼承IMailboxHandler介面,加上MailboxHandler標簽即可。那麼為什麼需要加這麼個功能呢,在其它的actor模型中是不存在這個特點的,一般是收到消息就進行分發處理了。原因是GateSession的設計,並不需要進行分發處理,因此我在這裡加上了郵箱類型這種設計。MessageDispatcher的處理方式有兩種一種是處理對方Send過來的消息,一種是rpc消息
// 處理Send的消息, 需要繼承AMActorHandler抽象類,抽象類第一個泛型參數是Actor的類型,第二個參數是消息的類型 [ActorMessageHandler(AppType.Map)] public class Actor_TestHandler : AMActorHandler<Unit, Actor_Test> { protected override ETTask Run(Unit unit, Actor_Test message) { Log.Debug(message.Info); } } // 處理Rpc消息, 需要繼承AMActorRpcHandler抽象類,抽象類第一個泛型參數是Actor的類型,第二個參數是消息的類型,第三個參數是返回消息的類型 [ActorMessageHandler(AppType.Map)] public class Actor_TransferHandler : AMActorRpcHandler<Unit, Actor_TransferRequest, Actor_TransferResponse> { protected override async ETTask Run(Unit unit, Actor_TransferRequest message, Action<Actor_TransferResponse> reply) { Actor_TransferResponse response = new Actor_TransferResponse(); try { reply(response); } catch (Exception e) { ReplyError(response, e, reply); } } }
我們需要註意一下,Actor消息有死鎖的可能,比如A call消息給B,B call給C,C call給A。因為MailboxComponent本質上是一個消息隊列,它開啟了一個協程會一個一個消息處理,返回ETTask表示這個消息處理類會阻塞MailboxComponent隊列的其它消息。所以如果出現死鎖,我們就不希望某個消息處理阻塞掉MailboxComponent其它消息的處理,我們可以在消息處理類裡面新開一個協程來處理就行了。例如:
[ActorMessageHandler(AppType.Map)] public class Actor_TestHandler : AMActorHandler<Unit, Actor_Test> { protected override ETTask Run(Unit unit, Actor_Test message) { RunAsync(unit, message).Coroutine(); } public ETVoid RunAsync(Unit unit, Actor_Test message) { Log.Debug(message.Info); } }
相關資料可以谷歌一下Actor死鎖的問題。
ET開源地址地址:egametang/ET: Unity3D Client And C# Server Framework (github.com) qq群:474643097