編寫 PHP 守護(hù)進(jìn)程程序

守護(hù)進(jìn)程(daemon),又稱為常駐后臺進(jìn)程。該進(jìn)程持續(xù)在后臺運行,處理系統(tǒng)業(yè)務(wù)。它沒有控制終端,不與前臺交互。要么手動殺死該進(jìn)程,要么系統(tǒng)關(guān)閉的時候被關(guān)閉。通常在小項目當(dāng)中 PHP 沒有此類需求。都是通過編寫定時腳本來執(zhí)行。

今天,我們以完成異步發(fā)送短信來編寫 PHP 守護(hù)進(jìn)程程序。會講到編寫守護(hù)進(jìn)程程序中會遇到的一些問題。以及這些問題的解決方案。

一、PHP CLI 模式###

PHP CLI 即 命令行模式。這是編寫常駐后臺程序必須掌握的知識點。關(guān)于 PHP CLI 相關(guān)的技術(shù)細(xì)節(jié)??梢圆榭床┲髦皩懙囊黄恼?a target="_blank">《PHP 命令行模式》。

我們主要用了 PHP CLI 模式的運行 PHP 腳本的功能。

如:

$ php test.php

二、實例代碼

為了避免空洞的理論。我們直接上代碼,然后對代碼進(jìn)行抽絲剝繭般分析。再一步一步優(yōu)化代碼,達(dá)到我們要求的守護(hù)進(jìn)程級別。

首先,我們要理解異步發(fā)送短信的需求涉及的流程。

(1)用戶登錄/注冊等需求短信驗證碼的位置。點擊獲取驗證碼。

(2)服務(wù)器收到用戶的發(fā)送短信請求。將手機號碼以及待發(fā)送的短信內(nèi)容放入 Redis 隊列。

(3)后臺進(jìn)程持續(xù)監(jiān)聽 Redis 隊列當(dāng)中是否有待處理的短信發(fā)送。有則發(fā)送。無則持續(xù)監(jiān)聽。

通過這三步,我們清晰知道。這個異步短信發(fā)送的需求會涉及到三個技術(shù)點:

(1)隊列:存儲待發(fā)送短信的數(shù)據(jù)。

(2)把用戶短信發(fā)送請求寫入隊列。

(3)從 Redis 隊列取出數(shù)據(jù)進(jìn)行短信發(fā)送。

假設(shè)我們的 Redis 隊列名稱為:sms_list 。

則寫入隊列的程序如下:

PushQueue.php 腳本代碼如下:

<?php
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);

$sms = [
    'mobile'  => '14800001234',
    'content' => '您的驗證碼為:888888。請及時使用,10 分鐘后失效?!綢T訪談】'
];

$ok = $redis->lPush('sms_list', json_encode($sms, JSON_UNESCAPED_UNICODE));
if ($ok) {
    echo "寫入短信隊列 sms_list 成功\n";
}

SmsConsume.php 后臺消費進(jìn)程代碼如下:

<?php
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);

$queueKey = 'sms_list';     // 短信隊列。
$queueIng = 'sms_list_ing'; // 短處中的隊列。

while (true) {
    $content = $redis->bRPopLPush($queueKey, $queueIng, 60);
    if (!empty($content)) {
        $arrCxt = json_decode($content, true);
        /**
         * 調(diào)用短信發(fā)送接口。
         * 由于是演示代碼,此處直接打印輸出即可。
         * 真實場景請調(diào)用短信發(fā)送的接口。
         */
        echo "mobile:{$arrCxt['mobile']}\n";
        echo "content:{$arrCxt['content']}\n\n";
    } else {
        // 暫停 0.1 秒。
        usleep(100000);
    }
}

啟動生產(chǎn)端/消費端

(1)啟動消費端

$ php SmsConsume.php

啟動完成之后,命令終端會一直等待數(shù)據(jù)寫入 Redis 隊列。接下來,我們運行生產(chǎn)端往 Redis 隊列寫入數(shù)據(jù)。

(2)啟動生產(chǎn)端

我們另起一個命令終端執(zhí)行如下命令:

$ php PushQueue.php

運行成功會輸出如下內(nèi)容:

寫入短信隊列 sms_list 成功

說明,我們已經(jīng)成功向 Redis sms_list 隊列寫入了短信發(fā)送的數(shù)據(jù)。

同時,在我們的消費端命令終端輸出了如下內(nèi)容:

mobile:14800001234
content:您的驗證碼為:888888。請及時使用,10 分鐘后失效?!綢T訪談】

問題與缺點:

(1)Redis 讀取數(shù)據(jù)錯誤

在運行消費端 SmsConsume.php 程序的時候,如果我們的生產(chǎn)端超過 60 秒沒有向隊列寫入數(shù)據(jù)。消費端在空閑 60 秒之后,會提示類似錯誤:

...... Uncaught RedisException: read error on connection ......

錯誤分析:

之所以出現(xiàn)這個錯誤。是因為在我們的 PHP 配置里面默認(rèn)限制了一個 socket 連接在 60 秒內(nèi)沒有任何操作就會斷開。斷開的 socket 連接再去讀取數(shù)據(jù)肯定會報錯。此錯誤依然會出現(xiàn)在 MySQL、Kafka、Memcache 等 socket 連接的系統(tǒng)。

解決方案:

知道了問題所在,剩下的就是更改 PHP 這個默認(rèn)的配置。

default_socket_timeout = 60

雖然,我們可以直接在 php.ini 文件中修改此值。但是,我們不建議這樣做。因為,這個配置不僅會影響 PHP CLI 模式,同時也會影響 PHP CGI 模式(Web 訪問)。所以,我們只推薦在代碼當(dāng)中修改。

我們修改 SmsConsume.php 腳本代碼之后如下:

<?php

// 防止 Socket 連接空閑超時退出報錯。
ini_set('default_socket_timeout', -1);

$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);

$queueKey = 'sms_list';     // 短信隊列。
$queueIng = 'sms_list_ing'; // 短處中的隊列。

while (true) {
    $content = $redis->bRPopLPush($queueKey, $queueIng, 60);
    if (!empty($content)) {
        $arrCxt = json_decode($content, true);
        /**
         * 調(diào)用短信發(fā)送接口。
         * 由于是演示代碼,此處直接打印輸出即可。
         * 真實場景請調(diào)用短信發(fā)送的接口。
         */
        echo "mobile:{$arrCxt['mobile']}\n";
        echo "content:{$arrCxt['content']}\n\n";
    } else {
        // 暫停 0.1 秒。
        usleep(100000);
    }
}

通過這樣修改之后,我們再去運行這個腳本。就會發(fā)現(xiàn)不再出現(xiàn)這個錯誤了。

(2)代碼報錯進(jìn)程退出

因為會發(fā)生類似 Redis 讀取數(shù)據(jù)錯誤或其他 PHP 錯誤。此時,PHP 消費端進(jìn)程就會終止執(zhí)行。如果我們把這個消費端程序設(shè)置為后端運行的守護(hù)進(jìn)程。這顯然是不滿足常駐后臺運行的目的。

所以,我們需要捕獲這些錯誤。然后寫日志或打印到命令行終端。

解決方案:

PHP 提供了 try catch 來解決異常。但是,有時候,PHP 并只是拋出異常,也有可能拋出 Notice、warning 等錯誤。此時,我們最好的做法是把這些錯誤轉(zhuǎn)成異常來處理。

在很多成熟的框架都已經(jīng)將錯誤轉(zhuǎn)成異常來處理了。所以,我們唯一要做的就是使用 try catch 來捕獲異常就行了。

SmsConsume.php 腳本修改之后的代碼如下:

<?php

// 防止 Socket 連接空閑超時退出報錯。
ini_set('default_socket_timeout', -1);

$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);

$queueKey = 'sms_list';     // 短信隊列。
$queueIng = 'sms_list_ing'; // 短處中的隊列。

while (true) {
    try {
        $content = $redis->bRPopLPush($queueKey, $queueIng, 60);
        if (!empty($content)) {
            $arrCxt = json_decode($content, true);
            /**
             * 調(diào)用短信發(fā)送接口。
             * 由于是演示代碼,此處直接打印輸出即可。
             * 真實場景請調(diào)用短信發(fā)送的接口。
             */
            echo "mobile:{$arrCxt['mobile']}\n";
            echo "content:{$arrCxt['content']}\n\n";
        } else {
            // 暫停 0.1 秒。
            usleep(100000);
        }
    } catch (\Exception $e) {
        echo "出錯了!\n";
        echo "ErrorMsg:" . $e->getMessage() . "\n\n";
    } catch (\Throwable $e) {
        echo "出錯了!\n";
        echo "ErrorMsg:" . $e->getMessage() . "\n\n";
    }
}

三、設(shè)置消費端為后臺運行

我們現(xiàn)在程序已經(jīng)寫好了。現(xiàn)在就需要將程序設(shè)置為后臺運行。設(shè)置為后臺運行的方案有很多種。

(1)Linux nohup 命令

關(guān)于該命令如何使用,大家可以通過 Google 搜索得到相當(dāng)全的資料。這里就不用去 Google 搬運了。

(2)Supervisor 管理

這是本博主寒冰推薦的方式。Supervisor 是一款非常優(yōu)秀的進(jìn)程管理工具。關(guān)于如何使用,可以查看我之前寫的一篇文章:CentOS7 安裝和使用 Supervisor 工具 。非常詳盡怎樣使用 Supervisor 這款工具。

四、總結(jié)

本篇文章只是一個精簡版的守護(hù)進(jìn)程程序。核心的點都已經(jīng)涉及到。技術(shù)的細(xì)節(jié)方面還需要結(jié)合實際的業(yè)務(wù)進(jìn)行考量。如果,你在使用本篇文章提到的相關(guān)功能時有任何問題,可以留言或者加群(168159147)咨詢。謝謝!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容