Swoole 中通過 process 模組實現多程式

Remember發表於2019-12-19

我們都知道,PHP有它自帶的程式控制 pcntl,Swoole 中的 process 提供了更強大的功能,直接擷取了官網的一張圖。

Laravel

下面我們模擬一個TCP伺服器,演示一下基於 process 的多程式服務。

接下來,先來看我們伺服器的程式碼部分.我們設定子程式數為3個,在下面這段程式碼中,主程式啟動之後,會額外啟動3個子程式,負責處理客戶端連線以及請求操作,當子程式退出後,主程式會重新建立新的子程式.如果主程式退出,那麼子程式在處理完當前請求之後也會退出。

:pencil2:Server

<?php

class Server
{
    private $mpid; //主程式id
    private $pids = []; //子程式陣列
    private $socket; //網路套間字

    const Max_PROCESS = 3; //最大建立程式數

    //主程式邏輯
    public function run()
    {
        $process = new \Swoole\Process(function () {

            //獲取主程式id
            $this->mpid = posix_getpid();
            echo time() . " master process pid: {$this->mpid}" . PHP_EOL;
            //建立TCP伺服器並獲取套間字
            $this->socket = stream_socket_server("tcp://127.0.0.1:9505", $errno, $errstr);
            if (!$this->socket) {
                exit("service start error:$errstr --- $errno");
            }

            //啟動子程式
            for ($i = 1; $i <= self::Max_PROCESS; $i++) {
                $this->startWorkerProcess();
            }

            echo "Waiting client connect" . PHP_EOL;

            //主程式等待子程式退出 必須是死迴圈
            while (1) {
                if(count($this->pids)){
                    //回收結束執行的子程式,預設為阻塞,false 表示非阻塞模式,如果失敗返回 false
                    $ret = \Swoole\Process::wait(false);
                    if ($ret) {
                        echo time() . " worker process: {$ret['pid']} exit ,then new process..." . PHP_EOL;
                        //新建立一個子程式
                        $this->startWorkerProcess();
                        //從陣列中刪除這個已不存在的子程式 pid
                        $index=array_search($ret['pid'],$this->pids);
                        unset($this->pids[$index]);
                    }
                }
                sleep(1);  //
            }
        }, false, false);
        //把當前程式升級為守護程式
        \Swoole\Process::daemon();

        $process->start();
    }

    //建立子程式
    public function startWorkerProcess()
    {
        $process = new \Swoole\Process(function (\Swoole\Process $work) {
            $this->acceptClient($work);

        }, false, false);
        $pid = $process->start();
        $this->pids[] = $pid;
    }

    //接收客戶端請求
    public function acceptClient(&$worker)
    {
        //子程式等待客戶端連線,不能退出
        while (1) {
            //接收由 stream_socket_server()建立的的套間字連線
            $conn = stream_socket_accept($this->socket, -1);
            //如果定義了連線回撥的地址,就呼叫
            if ($this->onConnect) {
                call_user_func($this->onConnect, $conn);
            }
            //開始迴圈讀取客戶端資訊

            $recv = ''; //實際接收資料
            $buffer = ''; //緩衝資料

            while (1) {
                //檢查主程式是否正常,如果不正常,退出子程式
                $this->checkmPid($worker);

                //讀取客戶端資訊
                $buffer = fread($conn, 20);

                //如果沒有訊息

                if ($buffer === false || $buffer === '') {
                    //如果設定了了解關閉的回撥函式,那麼執行關閉回撥
                    if ($this->onClose) {
                        call_user_func($this->onClose, $conn);
                    }
                    //等待下一個連線訊息
                    break;
                }

                //訊息結束的位置
                $pos = strpos($buffer, "\n");

                //沒有結束符,說明還沒有讀完
                if (false === $pos) {
                    $recv .= $buffer; //拼接訊息
                } else {              //訊息讀完了,處理訊息
                    $recv .= trim(substr($buffer, 0, $pos + 1));
                    //如果定義了處理訊息回撥的函式,那就直接呼叫回撥
                    if ($this->onMessage) {
                        call_user_func($this->onMessage, $conn, $recv);
                    }

                    //如果接收到quit 表示退出,那麼關閉這個連結,等待下一個客戶端連線

                    if ($recv === 'quit') {
                        echo 'client close' . PHP_EOL;
                        fclose($conn);
                        break;
                    }

                }
                //清空訊息
                $recv = '';
            }
        }
    }

    public function checkmPid(&$worker)
    {
        //說明主程式不存在,子程式此時是殭屍程式,需要退出
        if (!\Swoole\Process::kill($this->mpid, 0)) {
            $worker->exit();
            echo "worker: {$worker->pid} exit" . PHP_EOL;
        }
    }
}

$server = new Server();

//連線回撥
$server->onConnect = function ($conn) {
    echo "onConnect -- accepted " . stream_socket_get_name($conn, true) . PHP_EOL;
};
//接收訊息回撥
$server->onMessage = function ($conn, $msg) {
    echo "message is ------ $msg" . PHP_EOL;

    fwrite($conn, "received: " . $msg . "\n");
};

//關閉回撥
$server->onClose = function ($conn) {
    echo "close---" . stream_socket_get_name($conn, true) . PHP_EOL;
};

$server->run();

開啟服務之後,你可以通過命令檢視程式 pstree -p 主程式id 或者通過命令檢視執行的更多資訊 ps -ef | grep Server.php,可以看到對應程式的 id。

Swoole 中通過 process 模組實現多程式

現在讓我們建立一個簡單的客戶端連線伺服器。

:pencil2:Client

<?php

go(function () {
    $client = new \Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
    // 嘗試與指定 TCP 服務端建立連線(IP和埠號需要與服務端保持一致,超時時間為0.5秒)
    if ($client->connect("127.0.0.1", 9505, 0.5)) {
        // 建立連線後傳送內容
        $client->send("hello world\n");
        // 列印接收到的訊息
        echo $client->recv().PHP_EOL;
        sleep(2);
        // 關閉連線
        $client->close();
    } else {
        echo "connect failed.";
    }
});

建立一個協程客戶端請求,服務端接收資料,服務端響應資料,客戶端接收資料的整個過程。整理客戶端 sleep 兩秒之後才關閉連線。

Laravel

然後我們來很粗糙的手動kill掉其中的一個子程式。父程式又重新建立一個子程式。

Laravel

和預期一樣,主程式又重新建立了一個新的子程式。

:pencil2:參考資料以及程式碼整理

swoole官網

通過 Process 模組在 PHP 中實現多程式

swooleForYou程式碼整理地址

吳親庫裡

相關文章