基於 swoole 的 websocket 服務一:狀態同步

real-qgl發表於2022-06-14

基於 swoole 的 websocket 服務一:狀態同步

基礎流程圖

基礎流程圖

效果展示圖

介面展示

介面展示

終端展示

前端實現程式碼

swoole_websocket.html

<html>
    <head>
        <title>WebSocket</title>
        <style>
            h2, hr, ul, li {
                margin: 0;
                padding: 0;
            }
            hr {
                width: 200px;
                margin-top: 5px;
                margin-bottom: 5px;
            }
            #main li {
                list-style: none;
            }
            #main li span {
                display: inline-block;
                width: 50px;
            }
        </style>
    </head>
    <!-- 初始化載入子節點 -->
    <body onload="initialize();">
        <h2>WebSocket</h2>
        <hr>
        <div>
            <ul id="main">
                <!-- 待渲染子節點列表 -->
            </ul>
        </div>
        <hr>
        <button onclick="connWebsocket()">傳送</button>
    </body>
    <script>

        // 初始化頁面載入函式
        function initialize() {
            for (let i = 0; i < 10; i++) {
                // 獲取主節點
                let ulNode = window.document.getElementById('main')
                // 生成隨機id
                let id = makeRandomNumber(5)
                // 建立子節點
                liNode = window.document.createElement('li')
                liNode.innerHTML = `<span>${id}</span><span></span><span>未開始</span>`
                liNode.firstElementChild.setAttribute('style', 'color:#363636;')
                liNode.lastElementChild.setAttribute('style', 'color:#363636;')
                // 應用子節點
                ulNode.appendChild(liNode)
            }
        }

        // 連線websocket伺服器
        function connWebsocket() {
            // 所有的liNodes置為`進行中`
            let liNodes = window.document.getElementById('main').children
            // 待傳送訊息資料
            let allData = []
            for (let i = 0; i < liNodes.length; i++) {
                let firstNode = liNodes[i].firstElementChild
                let lastNode = liNodes[i].lastElementChild

                // 渲染`進行中`狀態樣式
                firstNode.setAttribute('style', 'color:#FF6347;')
                lastNode.setAttribute('style', 'color:#FF6347;')
                lastNode.textContent = '進行中'

                allData.push({
                    'id': firstNode.textContent,
                    'status': lastNode.textContent,
                })
            }

            // 進行weksocket通訊服務
            let ws = new WebSocket("ws://localhost:9999");

            ws.onopen = function(evt) {
                console.log('connection start')
                // 開啟連線就傳送訊息
                ws.send(JSON.stringify(allData))
            };

            // 已完成數量
            let completeNum = 0
            ws.onmessage = function(evt) {
                // 異常或錯誤處理
                try {
                    var obj = JSON.parse(evt.data);
                } catch (e) {
                    console.error(e)
                    return
                }
                if (!obj.id || !obj.status) {
                    console.error(`property is not undefined.`)
                    return
                }

                // 找到`id`對應節點
                let nodeList = [...liNodes]
                let liNode = nodeList.find((node) => {
                    return node.firstElementChild.textContent == obj.id
                })
                if (!liNode) {
                    console.error(`li node is not found.`)
                    return;
                }

                // 重新渲染介面,並自增已完成數量
                completeNum++
                liNode.lastElementChild.textContent = obj.status
                liNode.lastElementChild.setAttribute('style', 'color:#008B8B;')
                liNode.firstElementChild.setAttribute('style', 'color:#008B8B;')

                // 如果完成數和節點數相等,主動斷開連線
                if (completeNum === liNodes.length) {
                    ws.close()
                }
            };

            ws.onclose = function(evt) {
                console.log("connection close");
            }
        }

        // 製作整型隨機數
        function makeRandomNumber(digit = 6) {
            if (digit < 1 || digit > 10) {
                throw new RangeError('位數不能小於1且不能大於10')
            }
            const min = Math.pow(10, digit - 1)
            const max = Math.pow(10, digit) - 1

            let val = Math.floor(Math.random() * max + 1)
            while (val < min) {
                val = Math.floor(Math.random() * max + 1)
            }

            return val
        }

    </script>
</html>

後端程式碼實現

注意:演示框架是 laravel5.7+ 以上版本

1. 檢視swoole擴充套件版本

> php --ri swoole

swoole

Swoole => enabled
Author => Swoole Team <team@swoole.com>
Version => 4.5.11
Built => Feb 21 2022 14:53:00
coroutine => enabled
kqueue => enabled
rwlock => enabled
pcre => enabled
zlib => 1.2.11
brotli => E16777225/D16777225
async_redis => enabled

Directive => Local Value => Master Value
swoole.enable_coroutine => On => On
swoole.enable_library => On => On
swoole.enable_preemptive_scheduler => Off => Off
swoole.display_errors => On => On
swoole.use_shortname => On => On
swoole.unixsock_buffer_size => 262144 => 262144

注意:以上演示 swoole 擴充套件版本為 4.5+

2. 建立自定義websocket服務類

app/Handlers/Websockets/CoServer.php

<?php

namespace App\Handlers\Websockets;

use Swoole\Coroutine;

class CoServer
{
    public function __construct()
    {
        // 開啟一鍵協程化
        \Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
    }

    public function start()
    {
        // 非同步風格websocket伺服器
        $ws = new \Swoole\WebSocket\Server('127.0.0.1', 9999);

        $ws->on('Open', function ($ws, $request) {
            $this->log('連線成功', $request->fd);
        });

        $ws->on('Message', function ($ws, $frame) {

            $fid = $frame->fd ?? 0;
            $data = json_decode($frame->data, true);
            if (!$data || !is_array($data)) {
                $this->log('訊息格式錯誤!', $fid);
                return;
            }
            $this->log('伺服器已接收訊息.', $fid);

            // 協程間通訊,類似 go 的 sync.WaitGroup
            $wg = new \Swoole\Coroutine\WaitGroup();

            // 建立請求遠端協程任務
            $wg->add();
            Coroutine::create(function () use($wg, $data, $fid) {
                $this->log('已傳送訊息到遠端.', $fid);

                // 協程 http 客戶端
                $client = new \Swoole\Coroutine\Http\Client('127.0.0.1', 9101);
                $client->post('/remote/message', [
                    'data' => $data,
                    'fid' => $fid,
                ]);
                $client->close();
                $wg->done();
            });

            // 建立監聽訊息協程任務
            $wg->add();
            Coroutine::create(function () use ($wg, $ws, $fid) {
                $this->log('開始監聽訊息.', $fid);

                // 協程 redis 客戶端
                $redis = new \Swoole\Coroutine\Redis();
                $redis->connect('127.0.0.1', 6379);
                if ($redis->subscribe(['ws:fid:'.$fid])) {
                    while ($msg = $redis->recv()) {
                        list($type, $name, $cont) = $msg;
                        if ($type == 'message' && $name == 'ws:fid:'.$fid) {
                            $ws->push($fid, $cont);
                            $this->log('訊息已回覆', $fid);
                        }
                    }
                }
                $redis->close();
                $wg->done();
            });

            $wg->wait();
            // 處理完主動斷開連線
            $ws->close($fid);
        });

        $ws->on('close', function ($server, $fid) {
            echo "client {$fid} closed\n";
        });

        $ws->start();
    }

    protected function log($msg, $fd = 0)
    {
        if ($fd) {
            echo sprintf('[%s]: %d -> %s'.PHP_EOL, date('H:i:s'), $fd, $msg);
        } else {
            echo sprintf('[%s]: %s'.PHP_EOL, date('H:i:s'), $msg);
        }
    }
}

3. 模擬遠端請求介面方法

app/Http/Controllers/RemoteController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

class RemoteController extends Controller
{
      // 對應路由:/remote/message POST
    public function message(Request $request)
    {
        $data = $request->input('data');
        $fid = $request->input('fid');

        Log::info('請求引數: '.json_encode($request->all()));

        while (true) {
            if (empty($data)) {
                break;
            }

            usleep(500000);

            foreach ($data as $key => $val) {
                if (random_int(0, 100) > 80) {
                    $val['status'] = '已完成';
                    Log::info('獲取訊息中...');
                    Redis::connection()->publish('ws:fid:'.$fid, json_encode($val));
                    unset($data[$key]);
                }
            }
        }

        return response()->json([
            'status' => 0,
            'msg'    => 'success',
            'data'   => null,
        ]);
    }
}

4. 建立命令執行websocket服務

app/Console/Commands/WebsocketServer.php

<?php

namespace App\Console\Commands;

use App\Handlers\Websockets\CoServer;
use Illuminate\Console\Command;
use Ratchet\Http\HttpServer;
use Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer;

class WebsocketServer extends Command
{
    protected $signature = 'ws:start {server}';

    protected $description = 'Command description';

    const SERVER_HTTP = 'http';
    const SERVER_CO = 'co';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $allowServers = [self::SERVER_HTTP, self::SERVER_CO];
        $inputServer = $this->argument('server');
        if (!in_array($inputServer, $allowServers)) {
            $this->error('伺服器型別錯誤');
            return;
        }

        switch ($inputServer) {
            case self::SERVER_HTTP:
                $this->handleHttp();
                break;
            case self::SERVER_CO:
                $this->handleCo();
                break;
            default:
                $this->error('伺服器型別不存在');
        }

    }

    protected function handleHttp()
    {
//        $server = IoServer::factory(
//            new HttpServer(
//                new WsServer(
//                    new \App\Handlers\Websockets\HttpServer()
//                )
//            )
//            , 9999);
//        $server->run();
    }

    protected function handleCo()
    {
        $server = new CoServer();
        $server->start();
    }
}

5. 執行websocket服務

> php artisan ws:start co

後續擴充套件

  1. 許可權校驗

  2. 超時機制

  3. 資料同步

  4. 配置引數

    ……

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章