守護(hù)進(jìn)程(daemon),又稱為常駐后臺(tái)進(jìn)程依疼。該進(jìn)程持續(xù)在后臺(tái)運(yùn)行痰腮,處理系統(tǒng)業(yè)務(wù)。它沒有控制終端律罢,不與前臺(tái)交互诽嘉。要么手動(dòng)殺死該進(jìn)程,要么系統(tǒng)關(guān)閉的時(shí)候被關(guān)閉弟翘。通常在小項(xiàng)目當(dāng)中 PHP 沒有此類需求虫腋。都是通過(guò)編寫定時(shí)腳本來(lái)執(zhí)行。
今天稀余,我們以完成異步發(fā)送短信來(lái)編寫 PHP 守護(hù)進(jìn)程程序悦冀。會(huì)講到編寫守護(hù)進(jìn)程程序中會(huì)遇到的一些問(wèn)題。以及這些問(wèn)題的解決方案睛琳。
一盒蟆、PHP CLI 模式###
PHP CLI 即 命令行模式踏烙。這是編寫常駐后臺(tái)程序必須掌握的知識(shí)點(diǎn)。關(guān)于 PHP CLI 相關(guān)的技術(shù)細(xì)節(jié)历等√殖停可以查看博主之前寫的一篇文章《PHP 命令行模式》。
我們主要用了 PHP CLI 模式的運(yùn)行 PHP 腳本的功能寒屯。
如:
$ php test.php
二荐捻、實(shí)例代碼
為了避免空洞的理論。我們直接上代碼寡夹,然后對(duì)代碼進(jìn)行抽絲剝繭般分析处面。再一步一步優(yōu)化代碼,達(dá)到我們要求的守護(hù)進(jìn)程級(jí)別菩掏。
首先魂角,我們要理解異步發(fā)送短信的需求涉及的流程。
(1)用戶登錄/注冊(cè)等需求短信驗(yàn)證碼的位置智绸。點(diǎn)擊獲取驗(yàn)證碼野揪。
(2)服務(wù)器收到用戶的發(fā)送短信請(qǐng)求。將手機(jī)號(hào)碼以及待發(fā)送的短信內(nèi)容放入 Redis 隊(duì)列瞧栗。
(3)后臺(tái)進(jìn)程持續(xù)監(jiān)聽 Redis 隊(duì)列當(dāng)中是否有待處理的短信發(fā)送囱挑。有則發(fā)送。無(wú)則持續(xù)監(jiān)聽沼溜。
通過(guò)這三步平挑,我們清晰知道。這個(gè)異步短信發(fā)送的需求會(huì)涉及到三個(gè)技術(shù)點(diǎn):
(1)隊(duì)列:存儲(chǔ)待發(fā)送短信的數(shù)據(jù)系草。
(2)把用戶短信發(fā)送請(qǐng)求寫入隊(duì)列通熄。
(3)從 Redis 隊(duì)列取出數(shù)據(jù)進(jìn)行短信發(fā)送。
假設(shè)我們的 Redis 隊(duì)列名稱為:sms_list
找都。
則寫入隊(duì)列的程序如下:
PushQueue.php
腳本代碼如下:
<?php
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);
$sms = [
'mobile' => '14800001234',
'content' => '您的驗(yàn)證碼為:888888唇辨。請(qǐng)及時(shí)使用,10 分鐘后失效能耻∩兔叮【IT訪談】'
];
$ok = $redis->lPush('sms_list', json_encode($sms, JSON_UNESCAPED_UNICODE));
if ($ok) {
echo "寫入短信隊(duì)列 sms_list 成功\n";
}
SmsConsume.php
后臺(tái)消費(fèi)進(jìn)程代碼如下:
<?php
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);
$queueKey = 'sms_list'; // 短信隊(duì)列。
$queueIng = 'sms_list_ing'; // 短處中的隊(duì)列晓猛。
while (true) {
$content = $redis->bRPopLPush($queueKey, $queueIng, 60);
if (!empty($content)) {
$arrCxt = json_decode($content, true);
/**
* 調(diào)用短信發(fā)送接口饿幅。
* 由于是演示代碼,此處直接打印輸出即可戒职。
* 真實(shí)場(chǎng)景請(qǐng)調(diào)用短信發(fā)送的接口栗恩。
*/
echo "mobile:{$arrCxt['mobile']}\n";
echo "content:{$arrCxt['content']}\n\n";
} else {
// 暫停 0.1 秒。
usleep(100000);
}
}
啟動(dòng)生產(chǎn)端/消費(fèi)端
(1)啟動(dòng)消費(fèi)端
$ php SmsConsume.php
啟動(dòng)完成之后洪燥,命令終端會(huì)一直等待數(shù)據(jù)寫入 Redis 隊(duì)列磕秤。接下來(lái)乳乌,我們運(yùn)行生產(chǎn)端往 Redis 隊(duì)列寫入數(shù)據(jù)。
(2)啟動(dòng)生產(chǎn)端
我們另起一個(gè)命令終端執(zhí)行如下命令:
$ php PushQueue.php
運(yùn)行成功會(huì)輸出如下內(nèi)容:
寫入短信隊(duì)列 sms_list 成功
說(shuō)明市咆,我們已經(jīng)成功向 Redis sms_list 隊(duì)列寫入了短信發(fā)送的數(shù)據(jù)汉操。
同時(shí),在我們的消費(fèi)端命令終端輸出了如下內(nèi)容:
mobile:14800001234
content:您的驗(yàn)證碼為:888888蒙兰。請(qǐng)及時(shí)使用磷瘤,10 分鐘后失效●海【IT訪談】
問(wèn)題與缺點(diǎn):
(1)Redis 讀取數(shù)據(jù)錯(cuò)誤
在運(yùn)行消費(fèi)端 SmsConsume.php
程序的時(shí)候,如果我們的生產(chǎn)端超過(guò) 60 秒沒有向隊(duì)列寫入數(shù)據(jù)梭伐。消費(fèi)端在空閑 60 秒之后痹雅,會(huì)提示類似錯(cuò)誤:
...... Uncaught RedisException: read error on connection ......
錯(cuò)誤分析:
之所以出現(xiàn)這個(gè)錯(cuò)誤。是因?yàn)樵谖覀兊?PHP 配置里面默認(rèn)限制了一個(gè) socket 連接在 60 秒內(nèi)沒有任何操作就會(huì)斷開糊识。斷開的 socket 連接再去讀取數(shù)據(jù)肯定會(huì)報(bào)錯(cuò)绩社。此錯(cuò)誤依然會(huì)出現(xiàn)在 MySQL、Kafka赂苗、Memcache 等 socket 連接的系統(tǒng)愉耙。
解決方案:
知道了問(wèn)題所在,剩下的就是更改 PHP 這個(gè)默認(rèn)的配置拌滋。
default_socket_timeout = 60
雖然朴沿,我們可以直接在 php.ini 文件中修改此值。但是败砂,我們不建議這樣做赌渣。因?yàn)椋@個(gè)配置不僅會(huì)影響 PHP CLI 模式昌犹,同時(shí)也會(huì)影響 PHP CGI 模式(Web 訪問(wèn))坚芜。所以,我們只推薦在代碼當(dāng)中修改斜姥。
我們修改 SmsConsume.php
腳本代碼之后如下:
<?php
// 防止 Socket 連接空閑超時(shí)退出報(bào)錯(cuò)鸿竖。
ini_set('default_socket_timeout', -1);
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);
$queueKey = 'sms_list'; // 短信隊(duì)列。
$queueIng = 'sms_list_ing'; // 短處中的隊(duì)列铸敏。
while (true) {
$content = $redis->bRPopLPush($queueKey, $queueIng, 60);
if (!empty($content)) {
$arrCxt = json_decode($content, true);
/**
* 調(diào)用短信發(fā)送接口缚忧。
* 由于是演示代碼,此處直接打印輸出即可杈笔。
* 真實(shí)場(chǎng)景請(qǐng)調(diào)用短信發(fā)送的接口搔谴。
*/
echo "mobile:{$arrCxt['mobile']}\n";
echo "content:{$arrCxt['content']}\n\n";
} else {
// 暫停 0.1 秒。
usleep(100000);
}
}
通過(guò)這樣修改之后桩撮,我們?cè)偃ミ\(yùn)行這個(gè)腳本敦第。就會(huì)發(fā)現(xiàn)不再出現(xiàn)這個(gè)錯(cuò)誤了峰弹。
(2)代碼報(bào)錯(cuò)進(jìn)程退出
因?yàn)闀?huì)發(fā)生類似 Redis 讀取數(shù)據(jù)錯(cuò)誤或其他 PHP 錯(cuò)誤。此時(shí)芜果,PHP 消費(fèi)端進(jìn)程就會(huì)終止執(zhí)行鞠呈。如果我們把這個(gè)消費(fèi)端程序設(shè)置為后端運(yùn)行的守護(hù)進(jìn)程。這顯然是不滿足常駐后臺(tái)運(yùn)行的目的右钾。
所以蚁吝,我們需要捕獲這些錯(cuò)誤。然后寫日志或打印到命令行終端舀射。
解決方案:
PHP 提供了 try catch 來(lái)解決異常窘茁。但是,有時(shí)候脆烟,PHP 并只是拋出異常山林,也有可能拋出 Notice、warning 等錯(cuò)誤邢羔。此時(shí)驼抹,我們最好的做法是把這些錯(cuò)誤轉(zhuǎn)成異常來(lái)處理。
在很多成熟的框架都已經(jīng)將錯(cuò)誤轉(zhuǎn)成異常來(lái)處理了拜鹤。所以框冀,我們唯一要做的就是使用 try catch 來(lái)捕獲異常就行了。
SmsConsume.php
腳本修改之后的代碼如下:
<?php
// 防止 Socket 連接空閑超時(shí)退出報(bào)錯(cuò)敏簿。
ini_set('default_socket_timeout', -1);
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);
$queueKey = 'sms_list'; // 短信隊(duì)列明也。
$queueIng = 'sms_list_ing'; // 短處中的隊(duì)列。
while (true) {
try {
$content = $redis->bRPopLPush($queueKey, $queueIng, 60);
if (!empty($content)) {
$arrCxt = json_decode($content, true);
/**
* 調(diào)用短信發(fā)送接口惯裕。
* 由于是演示代碼诡右,此處直接打印輸出即可。
* 真實(shí)場(chǎng)景請(qǐng)調(diào)用短信發(fā)送的接口轻猖。
*/
echo "mobile:{$arrCxt['mobile']}\n";
echo "content:{$arrCxt['content']}\n\n";
} else {
// 暫停 0.1 秒帆吻。
usleep(100000);
}
} catch (\Exception $e) {
echo "出錯(cuò)了!\n";
echo "ErrorMsg:" . $e->getMessage() . "\n\n";
} catch (\Throwable $e) {
echo "出錯(cuò)了!\n";
echo "ErrorMsg:" . $e->getMessage() . "\n\n";
}
}
三、設(shè)置消費(fèi)端為后臺(tái)運(yùn)行
我們現(xiàn)在程序已經(jīng)寫好了×撸現(xiàn)在就需要將程序設(shè)置為后臺(tái)運(yùn)行猜煮。設(shè)置為后臺(tái)運(yùn)行的方案有很多種。
(1)Linux nohup 命令
關(guān)于該命令如何使用败许,大家可以通過(guò) Google 搜索得到相當(dāng)全的資料王带。這里就不用去 Google 搬運(yùn)了。
(2)Supervisor 管理
這是本博主寒冰推薦的方式市殷。Supervisor 是一款非常優(yōu)秀的進(jìn)程管理工具愕撰。關(guān)于如何使用,可以查看我之前寫的一篇文章:CentOS7 安裝和使用 Supervisor 工具 。非常詳盡怎樣使用 Supervisor 這款工具搞挣。
四带迟、總結(jié)
本篇文章只是一個(gè)精簡(jiǎn)版的守護(hù)進(jìn)程程序。核心的點(diǎn)都已經(jīng)涉及到囱桨。技術(shù)的細(xì)節(jié)方面還需要結(jié)合實(shí)際的業(yè)務(wù)進(jìn)行考量仓犬。如果,你在使用本篇文章提到的相關(guān)功能時(shí)有任何問(wèn)題舍肠,可以留言或者加群(168159147)咨詢搀继。謝謝!