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í)候需要這樣寫:
\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)去重寫這個(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)的類名寫入。
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)。