Hyperf はどのようにポート 9501/9502 の両方を使用して Websocket サービスに接続し、マルチワーカー コラボレーションを通じてチャット ルーム機能を実装できますか?

Hyperf が 2 つのポートで WebSocket 接続をリッスンできるのはなぜですか?

ソース コードの観点から見ると、複数のサーバーが構成されている場合、実際には 1 つのサーバーのみが起動されます。

注: 以前に使用したコードは、サービスを開始してポートをバインドするものです。以前に swoole 拡張機能のドキュメントを読んだことがありましたが、サービスとリスニング ポートも分離されていることに気づきませんでした。コードは分解し続けることができると思います 分割した場合は、分割を続けると、コードがより柔軟になり、各機能を拡張できます サービスとポートを分割した後、複数のポートを 1 つのサービスにバインドできますサーバー、および各ポートには独立したイベントが存在できます。

/**
 * @param Port[] $servers
 * @return Port[]
 */
protected function sortServers(array $servers): array
{
    $sortServers = [];
    foreach ($servers as $server) {
        switch ($server->getType()) {
            case ServerInterface::SERVER_HTTP:
                $this->enableHttpServer = true;
                if (! $this->enableWebsocketServer) {
                    array_unshift($sortServers, $server);
                } else {
                    $sortServers[] = $server;
                }
                break;
            case ServerInterface::SERVER_WEBSOCKET:
                $this->enableWebsocketServer = true;
                array_unshift($sortServers, $server);
                break;
            default:
                $sortServers[] = $server;
                break;
        }
    }

    return $sortServers;
}

ソースコードから1位のサービス構成をサービスとして作成し、その後監視を追加します

protected function initServers(ServerConfig $config)
{
    $servers = $this->sortServers($config->getServers());

    foreach ($servers as $server) {
        $name = $server->getName();
        $type = $server->getType();
        $host = $server->getHost();
        $port = $server->getPort();
        $sockType = $server->getSockType();
        $callbacks = $server->getCallbacks();

        if (! $this->server instanceof SwooleServer) {
            $this->server = $this->makeServer($type, $host, $port, $config->getMode(), $sockType);
            $callbacks = array_replace($this->defaultCallbacks(), $config->getCallbacks(), $callbacks);
            $this->registerSwooleEvents($this->server, $callbacks, $name);
            $this->server->set(array_replace($config->getSettings(), $server->getSettings()));
            ServerManager::add($name, [$type, current($this->server->ports)]);

            if (class_exists(BeforeMainServerStart::class)) {
                // Trigger BeforeMainServerStart event, this event only trigger once before main server start.
                $this->eventDispatcher->dispatch(new BeforeMainServerStart($this->server, $config->toArray()));
            }
        } else {
            /** @var bool|\Swoole\Server\Port $slaveServer */
            $slaveServer = $this->server->addlistener($host, $port, $sockType);
            if (! $slaveServer) {
                throw new \RuntimeException("Failed to listen server port [{$host}:{$port}]");
            }
            $server->getSettings() && $slaveServer->set(array_replace($config->getSettings(), $server->getSettings()));
            $this->registerSwooleEvents($slaveServer, $callbacks, $name);
            ServerManager::add($name, [$type, $slaveServer]);
        }

        // Trigger beforeStart event.
        if (isset($callbacks[Event::ON_BEFORE_START])) {
            [$class, $method] = $callbacks[Event::ON_BEFORE_START];
            if ($this->container->has($class)) {
                $this->container->get($class)->{$method}();
            }
        }

        if (class_exists(BeforeServerStart::class)) {
            // Trigger BeforeServerStart event.
            $this->eventDispatcher->dispatch(new BeforeServerStart($name));
        }
    }
}

makeServer関数から、サービス内にSERVER_WEBSOCKETがあれば、これがメインサービスとして新しいSwooleWebSocketServerとして起動されます。

protected function makeServer(int $type, string $host, int $port, int $mode, int $sockType): SwooleServer
{
    switch ($type) {
        case ServerInterface::SERVER_HTTP:
            return new SwooleHttpServer($host, $port, $mode, $sockType);
        case ServerInterface::SERVER_WEBSOCKET:
            return new SwooleWebSocketServer($host, $port, $mode, $sockType);
        case ServerInterface::SERVER_BASE:
            return new SwooleServer($host, $port, $mode, $sockType);
    }

    throw new RuntimeException('Server type is invalid.');
}

$this->registerSwooleEvents($this->server, $callbacks, $name); このコードは Websocket のさまざまなイベントを登録するため、メインサーバーには Websocket のさまざまなイベントがあり、http サーバーはポート 9501 にマウントします。イベントはバインドされていますが、ポート 9501 に接続された WebSocket がある場合、サーバーはデフォルトで WebSocket の自動アップグレードを自動的に有効にし、ポート 9501 をリッスンするようにバインドされているメイン サーバーは WebSocketServer であるため、WebSocketServer のデフォルトは onmessage、onopen イベントは次のようになります。使用済み。

9503 WebSocket サーバーが有効になっている場合、理論的には、WebSocket を使用してポート 9501 に接続すると、接続された 9503 のコールバック イベントになるはずだと推測されます。http リスニング ポートが WebSocket プロトコルを自動的に開かないようにする場合は、open_websocket_protocol=false を設定します。

<?php
return [
// 这里省略了该文件的其它配置
'servers' => [
        [
            'name' => 'http',
            'type' => Server::SERVER_HTTP,
            'host' => '0.0.0.0',
            'port' => 9501,
            'sock_type' => SWOOLE_SOCK_TCP,
            'callbacks' => [
                Event::ON_REQUEST => [Hyperf\HttpServer\Server::class,'onRequest'],
             ],
             'settings' => ['open_websocket_protocol' => false,]
         ],
     ]
 ];

SWOOLE に登場する多くの定数については、スクリプト実行時に自動的に設定されるものであり、実際のコードを実行することで確認できます。

define('SWOOLE_HTTP2_ERROR_COMPRESSION_ERROR', 9);
define('SWOOLE_HTTP2_ERROR_CONNECT_ERROR', 10);
define('SWOOLE_HTTP2_ERROR_ENHANCE_YOUR_CALM', 11);
define('SWOOLE_HTTP2_ERROR_INADEQUATE_SECURITY', 12);
define('SWOOLE_BASE', 1);
define('SWOOLE_PROCESS', 2);
define('SWOOLE_IPC_UNSOCK', 1);
define('SWOOLE_IPC_MSGQUEUE', 2);
define('SWOOLE_IPC_PREEMPTIVE', 3);

以下、ランダムに設定したtest.phpでSWOOLE_BASEを出力すると1が出力されるのですが、これらの定数は実行時に設定されるものだと思っていましたが、この理解は間違っていたようです。

<?php
echo SWOOLE_BASE."\n";
echo "hello\n";

// 输出
1
hello

Hyperf-skeleton によって与えられるデフォルトの構成はプロセス モードです。これは見つからないため、より明確です。PROCESS モードが使用されるため、WebSocket が接続されると、すべての接続が Manager によって制御されます。

SWOOLE_PRECESS および SWOOLE_BASE の 2 つのモード

サーバーの 2 つの動作モードの概要

Swoole\Server コンストラクターの 3 番目のパラメーターでは、 SWOOLE_BASEまたは SWOOLE_PROCESSという 2 つの定数値を入力できます 。これら 2 つのモードの違い、利点、欠点を以下に紹介します。

SWOOLE_PROCESS

SWOOLE_PROCESS モードでは、すべてのクライアント TCP 接続がメイン プロセスと確立されます。内部実装は比較的複雑で、多数のプロセス間通信およびプロセス管理メカニズムが使用されます。非常に複雑なビジネス ロジックを含むシナリオに適しています。Swoole は、完全なプロセス管理とメモリ保護メカニズムを提供します。ビジネスロジックが非常に複雑な場合でも、長時間安定して動作します。

Swoole は、  Reactorスレッドにバッファーの機能を提供し、多数の低速接続やバイト単位の悪意のあるクライアントに対処できます。

プロセスモードの利点:

  • 接続とデータ要求の送信が分離されており、大量のデータを含む接続と少量のデータを含む接続があるため、ワーカー プロセスが不均衡になることはありません。

  • Workerプロセスで致命的なエラーが発生した場合、接続は切断されません。

  • 単一接続の同時実行を実現でき、少数の TCP 接続のみが維持され、リクエストは複数のワーカー プロセスで同時に処理できます。

プロセスモードの欠点:

  • 2 つの IPC のオーバーヘッドがあり、マスター プロセスとワーカー プロセスは unixSocketを使用して通信する必要があります。

  • SWOOLE_PROCESS は PHP ZTS をサポートしていません。この場合、SWOOLE_BASE を使用するか、  single_threadを true に設定することしかできません。

SWOOLE_BASE

SWOOLE_BASE このモードは、従来の非同期ノンブロッキング サーバーです。Nginx や Node.js などのプログラムと完全に一致しています。

worker_numパラメータは BASE モードでも有効であり、複数の Worker プロセスが開始されます。

TCP 接続要求が受信されると、すべてのワーカー プロセスがこの接続をめぐって競合し、最終的にワーカー プロセスがクライアントとの TCP 接続を正常に確立し、この接続によって送受信されるすべてのデータがこのワーカーと直接通信します。メインプロセスの Reactor スレッドによって実行されます。

  • BASE モードでは、マスター プロセスの役割はなく、 マネージャープロセスの役割のみがあります。

  • 各ワーカー プロセスは、  SWOOLE_PROCESSモード でReactorスレッドとワーカー プロセスの役割を同時に引き受けます。

  • Manager プロセスは BASE モードではオプションです。worker_num=1 が設定され、Task 機能と MaxRequest 機能が使用されていない場合、最下層は Manager プロセスを作成せずに別の Worker プロセスを直接作成します。

BASE モードの利点:

  • BASE モードには IPC オーバーヘッドがなく、パフォーマンスが向上します。

  • BASE モードのコードはシンプルでエラーが発生しにくいです

BASE モードの欠点:

  • TCP 接続はワーカー プロセス内で維持されるため、ワーカー プロセスがハングアップすると、このワーカー内のすべての接続が閉じられます。

  • 少数の長い TCP 接続では、すべてのワーカー プロセスを利用できない

  • TCP 接続はワーカーにバインドされており、長時間接続アプリケーションの一部の接続には大量のデータが含まれており、これらの接続が配置されているワーカー プロセスの負荷は非常に高くなります。ただし、一部の接続のデータ量は小さいため、ワーカー プロセスの負荷は非常に低くなり、異なるワーカー プロセスのバランスが取れなくなります。

  • コールバック関数にブロック操作がある場合、サーバーは同期モードに縮退し、TCP バックログ キューが簡単にいっぱいになる可能性があります。

BASE モードに該当するシナリオ:

クライアント接続間の対話が必要ない場合は、BASE モードを使用できます。Memcache、HTTP サーバーなど。

BASE モードの制限:

BASE モードでは、sendと closeを除き 、他のサーバー メソッドはクロスプロセス実行をサポートしません。

Reactor スレッドとワーカー プロセス

リアクタースレッド

  • Reactorスレッドはマスタープロセスで作成されるスレッドです

  • クライアントの TCP 接続の維持、ネットワーク IO の処理、プロトコルの処理、データの送受信を担当します。

  • PHPコードは実行しません

  • TCP クライアントから送信されたデータをバッファリングし、結合し、完全な要求パケットに分割します。

ワーカープロセス

  • Reactor スレッドによって配信されたリクエスト パケットを受け入れ、PHP コールバック関数を実行してデータを処理します。

  • 応答データを生成して Reactor スレッドに送信し、Reactor スレッドが TCP クライアントに送信します。

  • 非同期ノンブロッキング モードまたは同期ブロッキング モードのいずれかになります。

  • ワーカーはマルチプロセス モードで実行されます

両者の関係は、Reactor が nginx、Worker が PHP-FPM であることがわかります。Reactor スレッドはネットワーク リクエストを非同期かつ並行して処理し、処理のために Worker プロセスに転送します。Reactor と Worker 間の 通信はunixSocketを介して行われます。

PHP-FPM アプリケーションでは、多くの場合、タスクが Redis などのキューに非同期的にポストされ、一部の PHP プロセスがバックグラウンドで開始されてこれらのタスクを非同期的に処理します。Swoole が提供する TaskWorker は、タスク配信、キュー、PHP タスク処理プロセス管理を統合した、より完全なソリューションです。非同期タスクの処理は、基礎となる層によって提供される API を通じて非常に簡単に実装できます。さらに、TaskWorker は、タスクの実行完了後に結果を Worker に返すこともできます。

Swoole の Reactor、Worker、TaskWorker を緊密に統合して、より高度な使用方法を提供できます。

より一般的な比喩では、Server が工場であると仮定すると、Reactor は顧客の注文を受け入れる販売になります。Worker は労働者であり、営業マンが注文を受けると、Worker は顧客が望むものを作るために仕事に行きます。TaskWorker は、Worker がいくつかの雑務を行うのを手伝い、Worker が仕事に集中できるようにする管理スタッフとして理解できます。

結論は

  1. SWOOLE_PROCESS モードでは、Websocket 接続オブジェクトはサーバーによって制御されます。リアクターの作成後、接続は管理のためにリアクターに渡されます。リアクターは、処理のためにリクエストをワーカーに割り当てます。ワーカーが処理を完了すると、サーバーはメッセージをキューにプッシュし、特定の Reactor がメッセージを送信するのを待ちます。

  2. hyperf にはプロセス間通信がありますが、なぜ他のプロセスに助けを求める必要があるのか​​、他のプロセスがメッセージを聞いた後にさらにメッセージを送信するのかどうかはまだ不明です。

    1. 開発者は、SWOOLE_PROCESSモードであれば、他のWorkerが送信する仕組みをトリガーしないと述べていますが、SWOOLE_BASEモードのみ、各リンクを各Workerに渡して別々の処理を行う場合、複数のWorkerと連携する必要があり、各ワーカーが競合するため、取得された接続はすべて分離されているため、複数のメッセージが送信される状況は発生しません。

  3. 上記 2 は分散型 WebSocket 構築方式であり、このマルチプロセス方式を利用することで、複数のサーバーが連携して長時間接続サービスを提供し、数千ユーザーのアクセス量を確保することができます。

 

 次のコードは onPipeMessage リスナーからのものです。これは、他のワーカーがそれを受信した後、接続が自分のプロセス上にあると判断して実行することを意味します。

 

 

 

おすすめ

転載: blog.csdn.net/wangsenling/article/details/132403645