Laravel 中引入 Swoole Websocket 並實現熱更新 Reload 程式碼

全村的希望發表於2020-04-26

要點:Swoole伺服器若想實現不重啟伺服器更新程式碼,則Swoole伺服器若想實現不重啟伺服器更新程式碼,只有在WorkerStart後才載入的程式碼檔案,才可以實現熱更新。

最終效果:
透過 php websocket.php start/reload/restart/shutdown
控制websocket程式, 透過reload命令實現程式碼熱更新

程式碼檔案:

  1. websocket.php 程式控制檔案
  2. WebsocketHandler.php Websocket事件處理器
  3. App\Services\WebsocketService 業務邏輯處理(非必須)

websocket.php

<?php

require_once __DIR__.'/vendor/autoload.php';


if (php_sapi_name() != 'cli') die('請用cli模式啟動.');

$type = $argv[1];

switch ($type) {
    case 'start':
        echo "websocket server start\n";
        start();
        break;
    case 'reload':
        echo "websocket server reload\n";
        reload();
        break;
    case 'shutdown':
        echo "websocket server shutdown\n";
        shutdown();
        break;
    case 'restart':
        echo "websocket server restart\n";
        restart();
        break;
}

function start()
{
    $dotenv = new Dotenv\Dotenv(__DIR__);
    $dotenv->load();

    $ssl = getenv('WS_SSL');
    $server_sock_type = SWOOLE_SOCK_TCP;
    $server_setting = [
        'max_wait_time' => 60,
        'reload_async' => true
    ];
    if($ssl) {
//        $server_sock_type = SWOOLE_SOCK_TCP | SWOOLE_SSL;
        $server_setting['ssl_cert_file'] = getenv('WS_SSL_CERT_FILE');
        $server_setting['ssl_key_file'] = getenv('WS_SSL_CERT_KEY');
        $server_setting['ssl_verify_peer'] = false;
    }

    $ws = new \Swoole\WebSocket\Server(getenv('WS_ADDRESS'), getenv('WS_PORT'), SWOOLE_PROCESS, $server_sock_type);

    // 配置
    $ws->set($server_setting);

    $ws->on('start', function ($server) {
        swoole_set_process_name("jadeite-websocket");
        echo "主程式PID: {$server->master_pid}\n";
        echo "管理程式PID: {$server->manager_pid}\n";

        $content = <<<EOF
MASTER_PID={$server->master_pid}
MANAGER_PID={$server->manager_pid}

EOF;

        file_put_contents(__DIR__ . '/storage/app/websocket', $content);

    });
    $ws->on('WorkerStart', function (\Swoole\Server $server, int $workerId) {
        var_dump(get_included_files()); //此陣列中的檔案表示程式啟動前就載入了,所以無法reload
        require_once __DIR__ . '/public/index.php';

        register_shutdown_function(function () {
            $error = error_get_last();
            Log::emergency("[Websocket Worker shutdown]:\n".$error);
        });
    });

    require_once 'WebsocketHandler.php';
    $handler = new WebsocketHandler($ws);

    $ws->on('open', function ($ws, $request) use ($handler) {

        $handler->handleOpen($request);
    });

    //監聽WebSocket訊息事件
    $ws->on('message', function ($ws, $frame) use ($handler) {
        $handler->handleMessage($frame);
    });

    //監聽WebSocket連線關閉事件
    $ws->on('close', function ($ws, $fd) use ($handler) {
        $handler->handleClose($fd);
    });

    $ws->start();
}

function reload()
{
    $content = file_get_contents(__DIR__ . '/storage/app/websocket');
    echo "{$content}\n";
    $master_pid = explode('=', explode("\n", $content)[0])[1];
    echo "kill -USR1 {$master_pid}\n";
    exec("kill -USR1 {$master_pid}");
}

function shutdown()
{
    $content = file_get_contents(__DIR__ . '/storage/app/websocket');
    echo "{$content}\n";
    $master_pid = explode('=', explode("\n", $content)[0])[1];
    echo "kill -15 {$master_pid}\n";
    exec("kill -15 {$master_pid}");
}

function restart()
{
    shutdown();
    start();
}

WebsocketHandler.php

<?php
/**
 * Created by PhpStorm.
 * User: liuxiaofeng
 * Date: 2020-03-24
 * Time: 14:24
 */
use App\Services\WebsocketService;

class WebsocketHandler
{
    protected $ws = null;
    protected $service = null;

    public function __construct(\Swoole\WebSocket\Server $ws) {
        $this->ws = $ws;
    }

    public function handleOpen($request) {
        if($this->service === null) {
            $this->service = new WebsocketService($this->ws);
        }

        $this->service->handleOpen($request);
    }

    public function handleMessage($frame) {
        if($this->service === null) {
            $this->service = new WebsocketService($this->ws);
        }

        $this->service->handleMessage($frame);
    }

    public function handleClose($fd) {
        if($this->service === null) {
            $this->service = new WebsocketService($this->ws);
        }

        $this->service->handleClose($fd);
    }




}

App\Services\WebsocketService

<?php
/**
 * Created by PhpStorm.
 * User: liuxiaofeng
 * Date: 2020-03-23
 * Time: 16:07
 */

namespace App\Services;
use Carbon\Carbon;
use Illuminate\Support\Facades\Redis;

class WebsocketService
{
    protected $ws = null;
    protected $redis = null;


    public function __construct(\Swoole\WebSocket\Server $ws) {
        $this->ws = $ws;
        $this->redis = Redis::connection();
    }

    public function handleOpen($request) {
        echo "[handling open]\n";
        echo "client-{$request->fd} is opened.\n";
    }

    public function handleMessage($frame) {
        try{
            if($frame->data === 'heartbeat.') return;
            echo "[handling client-{$frame->fd} message]\n";
            echo "[Data]: {$frame->data}\n";

            $data = json_decode($frame->data, true);
            if(!is_array($data)) {
                return;
            }
            if(isset($data['type'])) {
                $type = $data['type'];
                $data = $data['data'];
                switch ($type) {
                    case 'index':
                        // 進入首頁
                        break;
                }
            }
        } catch (\Throwable $e) {
            \Log::error("[Websocket Error on handling Message]:\n". $e->getMessage());
        }



    }


    public function handleClose($fd) {
        try{
            echo "[handling client-{$fd} close]\n";
        } catch (\Throwable $e) {
            \Log::error("[Websocket Error on handling Close]:\n". $e->getMessage());
        }

    }

    /**
     * 單獨傳送
     * @param $fd
     * @param $data
     */
    public function send($fd, $data) {
        if($this->ws->isEstablished($fd)) {
            $this->ws->push($fd, json_encode($data));
        }
    }
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結
劉曉峰

相關文章