Actor
提供Actor
模式支持,助力游戲行業(yè)開(kāi)發(fā)。EasySwoole
的Actor
采用自定義Process
作為存儲(chǔ)載體,以協(xié)程作為最小調(diào)度單位,利用協(xié)程Channel
做mail box
,而客戶端與Process
之間的通訊,采用UnixSocket
實(shí)現(xiàn),并且借助TCP
實(shí)現(xiàn)分布式的ActorClient
,超高并發(fā)下也能輕松應(yīng)對(duì)。
工作流程
一般來(lái)說(shuō)有兩種策略用來(lái)在并發(fā)線程中進(jìn)行通信:共享數(shù)據(jù)和消息傳遞。使用共享數(shù)據(jù)方式的并發(fā)編程面臨的最大的一個(gè)問(wèn)題就是數(shù)據(jù)條件競(jìng)爭(zhēng),當(dāng)兩個(gè)實(shí)例需要訪問(wèn)同一個(gè)數(shù)據(jù)時(shí),為了保證數(shù)據(jù)的一致性,通常需要為數(shù)據(jù)加鎖,而Actor模型采用消息傳遞機(jī)制來(lái)避免數(shù)據(jù)競(jìng)爭(zhēng),無(wú)需復(fù)雜的加鎖操作,各個(gè)實(shí)例只需要關(guān)注自身的狀態(tài)以及處理收到的消息。
Actor
是完全面向?qū)ο蟆o(wú)鎖、異步、實(shí)例隔離、分布式的并發(fā)開(kāi)發(fā)模式。Actor
實(shí)例之間互相隔離,Actor
實(shí)例擁有自己獨(dú)立的狀態(tài),各個(gè)Actor
之間不能直接訪問(wèn)對(duì)方的狀態(tài),需要通過(guò)消息投遞機(jī)制來(lái)通知對(duì)方改變狀態(tài)。由于每個(gè)實(shí)例的狀態(tài)是獨(dú)立的,沒(méi)有數(shù)據(jù)被共享,所以不會(huì)發(fā)生數(shù)據(jù)競(jìng)爭(zhēng),從而避免了并發(fā)下的加鎖問(wèn)題。
舉一個(gè)游戲場(chǎng)景的例子,在一個(gè)游戲房間中,有5個(gè)玩家,每個(gè)玩家都是一個(gè)PlayerActor
,擁有自己的屬性,比如角色I(xiàn)D,昵稱,當(dāng)前血量,攻擊力等。游戲房間本身也是一個(gè)RoomActor
,房間也擁有屬性,比如當(dāng)前在線的玩家,當(dāng)前場(chǎng)景的怪物數(shù)量,怪物血量等。此時(shí)玩家A攻擊某個(gè)怪物,則PlayerActor-A
向RoomActor
發(fā)送一個(gè)攻擊怪物的指令,RoomActor
經(jīng)過(guò)計(jì)算,得出玩家A對(duì)怪物的傷害值,并給房間內(nèi)的所有PlayerActor
發(fā)送一個(gè)消息(玩家A攻擊怪物A,造成175點(diǎn)傷害,怪物A剩余血量1200點(diǎn)),類似此過(guò)程,每個(gè)PlayerActor
都可以得知房間內(nèi)發(fā)生了什么事情,但又不會(huì)造成同時(shí)訪問(wèn)怪物A的屬性,導(dǎo)致的共享加鎖問(wèn)題。
安裝
Actor
并沒(méi)有作為內(nèi)置組件,需要先引入包并進(jìn)行基礎(chǔ)配置才能夠使用。
composer require easyswoole/actor
使用
建立一個(gè)Actor
每一種對(duì)象(玩家、房間、甚至是日志服務(wù)也可以作為一種Actor
對(duì)象)都建立一個(gè)Actor
來(lái)進(jìn)行管理,一個(gè)對(duì)象可以擁有多個(gè)實(shí)例(Client)
并且可以互相通過(guò)信箱發(fā)送消息來(lái)處理業(yè)務(wù)。
<?php
namespace App\Player;
use EasySwoole\Actor\AbstractActor;
use EasySwoole\Actor\ActorConfig;
/**
* 玩家Actor
* Class PlayerActor
* @package App\Player
*/
class PlayerActor extends AbstractActor
{
/**
* 配置當(dāng)前的Actor
* @param ActorConfig $actorConfig
*/
public static function configure(ActorConfig $actorConfig)
{
$actorConfig->setActorName('PlayerActor');
$actorConfig->setWorkerNum(3);
}
/**
* Actor首次啟動(dòng)時(shí)
*/
protected function onStart()
{
$actorId = $this->actorId();
echo "Player Actor {$actorId} onStart\n";
}
/**
* Actor收到消息時(shí)
* @param $msg
*/
protected function onMessage($msg)
{
$actorId = $this->actorId();
echo "Player Actor {$actorId} onMessage\n";
}
/**
* Actor即將退出前
* @param $arg
*/
protected function onExit($arg)
{
$actorId = $this->actorId();
echo "Player Actor {$actorId} onExit\n";
}
/**
* Actor發(fā)生異常時(shí)
* @param \Throwable $throwable
*/
protected function onException(\Throwable $throwable)
{
$actorId = $this->actorId();
echo "Player Actor {$actorId} onException\n";
}
}
注冊(cè)Actor服務(wù)
可以使用setListenAddress
和setListenPort
指定本機(jī)對(duì)外監(jiān)聽(tīng)的端口,其他機(jī)器可以通過(guò)該端口向本機(jī)的Actor
發(fā)送消息。
public static function mainServerCreate(EventRegister $register) {
// 注冊(cè)Actor管理器
$server = \EasySwoole\EasySwoole\ServerManager::getInstance()->getSwooleServer();
\EasySwoole\Actor\Actor::getInstance()->register(PlayerActor::class);
\EasySwoole\Actor\Actor::getInstance()->setTempDir(EASYSWOOLE_TEMP_DIR)
->setListenAddress('0.0.0.0')->setListenPort('9900')->attachServer($server);
}
Actor實(shí)例管理
服務(wù)啟動(dòng)后就可以進(jìn)行Actor
的操作,管理本機(jī)的Client
實(shí)例,則不需要給client
傳入$node
參數(shù),默認(rèn)的node
為本機(jī),管理其他機(jī)器時(shí)需要傳入。
// 管理本機(jī)的Actor則不需要聲明節(jié)點(diǎn)
$node = new \EasySwoole\Actor\ActorNode();
$node->setIp('127.0.0.1');
$node->setListenPort(9900);
// 啟動(dòng)一個(gè)Actor并得到ActorId 后續(xù)操作需要依賴ActorId
$actorId = PlayerActor::client($node)->create(['time' => time()]); // 00101000000000000000001
// 給某個(gè)Actor發(fā)消息
PlayerActor::client($node)->send($actorId, ['data' => 'data']);
// 給該類型的全部Actor發(fā)消息
PlayerActor::client($node)->sendAll(['data' => 'data']);
// 退出某個(gè)Actor
PlayerActor::client($node)->exit($actorId, ['arg' => 'arg']);
// 退出全部Actor
PlayerActor::client($node)->exitAll(['arg' => 'arg']);
架構(gòu)解讀
Actor
應(yīng)該叫ActorManager
更確切點(diǎn),它用來(lái)注冊(cè)Actor
啟動(dòng)Proxy
和ActorWorker
進(jìn)程。
當(dāng)你在業(yè)務(wù)邏輯里定義了幾種Actor
,比如RoomActor
、PlayerActor
,需要在SwooleServer
啟動(dòng)時(shí)注冊(cè)它們。
具體就是在EasySwooleEvent.mainServerCreate
方法中添加如下代碼。
$actor = Actor::getInstance();
$actor->register(RoomActor::class);
$actor->register(PlayerActor::class);
$actorConf = Config::getInstance()->getConf('ACTOR_SERVER');
$actor->setMachineId($actorConf['MACHINE_ID'])
->setListenAddress($actorConf['LISTEN_ADDRESS'])
->setListenPort($actorConf['PORT'])
->attachServer($server);
其中ListenAddress
、ListenPort
為Proxy
進(jìn)程的監(jiān)聽(tīng)地址端口,MachineId
為ActorWorker
進(jìn)程的機(jī)器碼。
MachineId
和IP:PORT
對(duì)應(yīng)。
attachServer
將開(kāi)啟相應(yīng)數(shù)量的Proxy
進(jìn)程,以及前邊register
的ActorWorker
進(jìn)程。
工作原理
Proxy
進(jìn)程做消息中轉(zhuǎn),Worker
進(jìn)程做消息分發(fā)推送。來(lái)看個(gè)具體的例子:
游戲中玩家P請(qǐng)求進(jìn)入房間R,抽象成Actor
模型就是PlayerActor
需要往RoomActor
發(fā)送請(qǐng)求加入的命令。
那么這時(shí)候需要這樣寫(xiě):
\EasySwoole\Actor\Test\RoomActor::client($node)->send($roomActorId, [
'user_actor_id' => $userActorId,
'data' => '其他進(jìn)入房間的參數(shù)'
])
其中$roomActorId
和$userActorId
是事先xxActor::client()->create()
出來(lái)的。
上面那段代碼的意思就是往$roomActorId
的RoomActor
實(shí)例推送了一條$userActorId
玩家的UserActor
實(shí)例要加入房間的消息。
參數(shù)$node
用來(lái)尋址Proxy
,它由目標(biāo)Actor
實(shí)例的Worker.MachineId
決定,在本例中就是$roomActorId
被創(chuàng)建在了哪個(gè)MachineId
的WorkerProcess
。
通過(guò)$roomActorId
中的機(jī)器碼找到IP:PORT
,生成$node
。
send
時(shí)會(huì)創(chuàng)建一個(gè)協(xié)程TcpClient
,將消息發(fā)送給Proxy
,然后Proxy
將消息轉(zhuǎn)發(fā)(UnixClient)
至本機(jī)WorkerProcess
,WorkerProcess
收到消息,推送到具體的Actor
實(shí)例。
這樣就完成了從PlayerActor
到RoomActor
的請(qǐng)求通訊,RoomActor
收到請(qǐng)求消息并處理完成后,向PlayerActor
回發(fā)處理結(jié)果,用的是同樣的通訊流程。
如果是單機(jī)部署,可以忽略$node
參數(shù),因?yàn)樗型ㄓ嵍际窃诒緳C(jī)進(jìn)行。
多機(jī)的話,需要自己根據(jù)業(yè)務(wù)來(lái)實(shí)現(xiàn)Actor
如何分布和定位。
主要屬性
machineId 機(jī)器碼
proxyNum 啟動(dòng)幾個(gè)ProxyProcess
listenPort 監(jiān)聽(tīng)port
listenAddress 監(jiān)聽(tīng)ip
AbstractActor
Actor
實(shí)例的基類,所有業(yè)務(wù)中用到的Actor
都將繼承于`AbstractActor。例如游戲場(chǎng)景中的房間,你可以:
class RoomActor extends AbstractActor
工作原理
每個(gè)Actor
實(shí)例都維護(hù)一份獨(dú)立的數(shù)據(jù)和狀態(tài),當(dāng)一個(gè)Actor
實(shí)例通過(guò)client()->create()
后,會(huì)開(kāi)啟協(xié)程循環(huán),接收mailbox pop
的消息,進(jìn)而處理業(yè)務(wù)邏輯,更新自己的數(shù)據(jù)及狀態(tài)。具體實(shí)現(xiàn)就是__run()
這個(gè)方法。
靜態(tài)方法 configure
用來(lái)配置ActorConfig
,只需要在具體的Actor
(如RoomActor
)去重寫(xiě)這個(gè)方法就行。
關(guān)于ActorConfig
具體屬性可以看下邊ActorConfig
部分。
幾個(gè)虛擬方法
以下幾個(gè)虛擬方法需要在Actor
子類中實(shí)現(xiàn),這幾個(gè)方法被用在__run()
中來(lái)完成Actor
的運(yùn)行周期。
onStart() 在協(xié)程開(kāi)啟前執(zhí)行,你可以在此進(jìn)行Actor
初始化的一些操作,比如獲取房間的基礎(chǔ)屬性等。
onMessage() 當(dāng)接收到消息時(shí)執(zhí)行,一個(gè)Actor
實(shí)例的生命周期基本上就是在收消息-處理-發(fā)消息,你需要在這里對(duì)消息進(jìn)行解析處理。
onExit() 當(dāng)接收到退出命令時(shí)執(zhí)行。比如你希望在一個(gè)Actor
實(shí)例退出的時(shí)候,同時(shí)通知某些關(guān)聯(lián)的其他Actor
,可以在此處理。
其它
exit() 用于實(shí)例自己退出操作,會(huì)向自己發(fā)一條退出的命令。
tick()、after() 兩個(gè)定時(shí)器,用于Actor
實(shí)例的定時(shí)任務(wù),比如游戲房間的定時(shí)刷怪(tick)
;掉線后多長(zhǎng)時(shí)間自動(dòng)踢出(after)
。
static client() 用于創(chuàng)建一個(gè)ActorClient
來(lái)進(jìn)行對(duì)應(yīng)Actor
(實(shí)例)的通訊。
ActorClient
Actor
通訊客戶端,調(diào)用xxActor::client()
來(lái)創(chuàng)建一個(gè)ActorClient
進(jìn)行Actor
通訊。
上邊已經(jīng)大概講過(guò)了Actor
的通訊流程,本質(zhì)就是TcpClient->ProxyProcess->UnixClient->ActorWorkerProcess->xxActor
。
看下它實(shí)現(xiàn)了哪些方法:
create() 創(chuàng)建一個(gè)xxActor
實(shí)例,返回actorId
,在之后你可以使用這個(gè)actorId
與此實(shí)例進(jìn)行通訊。
send() 指定actorId
,向其發(fā)送消息。
exit() 通知xxActor
退出指定actorId
的實(shí)例。
sendAll() 向所有的xxActor
實(shí)例發(fā)送消息。
exitAll() 退出所有xxActor
實(shí)例。
exist() 當(dāng)前是否存在指定actorId
的xxActor
實(shí)例。
status() 當(dāng)前ActorWorker
下xxActor
的分布狀態(tài)。
ActorConfig
具體Actor
的配置項(xiàng),比如RoomActor
、PlayerActor
都有自己的配置。
actorName 一般用類名就可以,注意在同一個(gè)服務(wù)中這個(gè)是不能重復(fù)的。
actorClass 在Actor->register()
會(huì)將對(duì)應(yīng)的類名寫(xiě)入。
workerNum 為Actor
開(kāi)啟幾個(gè)進(jìn)程,Actor->attachServer()
時(shí)會(huì)根據(jù)這個(gè)參數(shù)為相應(yīng)Actor
啟動(dòng)WorkerNum
個(gè)Worker
進(jìn)程。
ActorNode
上邊提到過(guò),xxActor::client($node)
,這個(gè)$node
就是ActorNode
對(duì)象,屬性為Ip
和Port
,用于尋址Proxy
。
WorkerConfig
WorkerProcess
的配置項(xiàng),WorkerProcess
啟動(dòng)時(shí)用到。
workerId worker
進(jìn)程Id
,create Actor
的時(shí)候用于生成actorId
machineId worker
進(jìn)程機(jī)器碼,create Actor
的時(shí)候用于生成actorId
trigger 異常觸發(fā)處理接口
WorkerProcess
Actor
的重點(diǎn)在這里,每個(gè)注冊(cè)的Actor
(類)會(huì)啟動(dòng)相應(yīng)數(shù)量的WorkerProcess
。
比如你注冊(cè)了RoomActor
、PlayerActor
,workerNum
都配置的是3,那么系統(tǒng)將啟動(dòng)3個(gè)RoomActor
的Worker
進(jìn)程和3個(gè)PlayerActor
的Worker
進(jìn)程。
每個(gè)WorkerProcess
維護(hù)一個(gè)ActorList
,你通過(guò)client()->create()
的Actor
將分布在不同Worker
進(jìn)程里,由它的ActorList
進(jìn)行管理。
WorkerProcess
通過(guò)協(xié)程接收client
(這個(gè)client
就是Proxy
做轉(zhuǎn)發(fā)時(shí)的UnixClient
)消息,區(qū)分消息類型,然后分發(fā)給對(duì)應(yīng)的Actor
實(shí)例。
請(qǐng)仔細(xì)閱讀下WorkerProcess
的源碼,它繼承于AbstractUnixProcess
。
UnixClient
UnixStream Socket
,自行了解。Proxy
轉(zhuǎn)發(fā)消息給本機(jī)Actor
所使用的Client
。
Protocol
數(shù)據(jù)封包協(xié)議。
ProxyCommand
消息命令對(duì)象,Actor2
將不同類型的消息封裝成格式化的命令,最終傳給WorkerProcess
。
你可以在ActorClient
中了解一下方法和命令的對(duì)應(yīng)關(guān)系,但這個(gè)不需要在業(yè)務(wù)層去更改。
ProxyConfig
消息代理的配置項(xiàng)。
actorList 注冊(cè)的actor
列表。
machineId 機(jī)器碼
tempDir 臨時(shí)目錄
trigger 錯(cuò)誤觸發(fā)處理接口
ProxyProcess
Actor->attachServer()
會(huì)啟動(dòng)proxyNum
個(gè)ProxyProcess
。
用于在Actor
實(shí)例和WorkerProcess
做消息中轉(zhuǎn)。