You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP Phrity代理服务器无法接收Ratchet服务器响应的问题排查

WebSocket代理无法接收Ratchet服务器消息的问题

我实现了一个WebSocket代理服务器,负责接收浏览器连接后创建Phrity Client,作为中间层连接Ratchet服务器——每个浏览器连接对应一个独立的Ratchet客户端,用于双向转发消息。目前浏览器到Ratchet的消息转发正常,但无法接收Ratchet服务器发往浏览器的消息,绕过代理直接连接Ratchet时所有功能完全正常。

相关代码

<?php

namespace WebSocket;

require dirname(__DIR__) . '/php/lib/phrity/vendor/autoload.php';

use WebSocket\Server;
use WebSocket\Connection;
use WebSocket\Client;
use WebSocket\Message\Text;
use WebSocket\Message\Message;
use WebSocket\Exception\Exception;

error_reporting(-1);

function logMessage($message) {
    echo date('[Y-m-d H:i:s] ') . $message . "\n";
}

// Server options
$options = [
    'port' => 8091,
    'ssl' => false,
    'timeout' => 200,
    'framesize' => 4096,
    'connections' => 10,
    'debug' => true,
];

// Ratchet server details
$ratchetHost = '127.0.0.1';
$ratchetPort = 1112;

// Arrays to manage connections
$connections = [];
$ratchetClients = [];

function connectToRatchet($host, $port, $token) {
    $ratchetClient = new Client("ws://$host:$port?token=$token");
    
    // Add standard middlewares
    $ratchetClient->addMiddleware(new Middleware\CloseHandler());
    $ratchetClient->addMiddleware(new Middleware\PingResponder());
    
    $ratchetClient->onText(function (Client $client, Connection $connection, Message $message) use (&$connections) {
        logMessage("Received message from Ratchet server: " . $message->getContent());
        
        $connectionId = spl_object_hash($connection);
        
        if (isset($connections[$connectionId])) {
            $browserConnection = $connections[$connectionId];
            
            if ($browserConnection->isOpen()) {
                logMessage("Forwarding message to browser connection $connectionId");
                $browserConnection->text($message->getContent());
            } else {
                logMessage("Error: Browser connection $connectionId is closed");
                // Optionally close and remove the Ratchet client
                $client->close();
                unset($connections[$connectionId]);
            }
        } else {
            logMessage("Error: Browser connection not found for Ratchet client connection $connectionId");
        }
    });

    try {
        $ratchetClient->connect();
        logMessage("Successfully connected to Ratchet server");
        return $ratchetClient;
    } catch (\Exception $e) {
        logMessage("Failed to connect to Ratchet server: " . $e->getMessage());
        return null;
    }
}

try {
    $server = new Server($options['port'], $options['ssl']);

    // Middleware and configuration
    $server->addMiddleware(new \WebSocket\Middleware\CloseHandler())
           ->addMiddleware(new \WebSocket\Middleware\PingResponder())
           ->setTimeout($options['timeout'])
           ->setFrameSize($options['framesize'])
           ->setMaxConnections($options['connections']);

    logMessage("# Listening on port {$server->getPort()}");

    // Event handlers
    $server->onHandshake(function (Server $server, Connection $connection, $request, $response) use ($ratchetHost, $ratchetPort, &$connections, &$ratchetClients) {
        logMessage("Handshake received");

        // Extract token from URI query parameters
        $uri = $request->getUri();
        $queryString = $uri->getQuery();
        $queryParams = [];
        parse_str($queryString, $queryParams);

        if (isset($queryParams['token'])) {
            $token = $queryParams['token'];
            logMessage("Token found: $token");

            // Create a unique ID for the connection
            $connectionId = spl_object_hash($connection);
            $connections[$connectionId] = $connection;

            // Attempt to establish connection to Ratchet server
            $ratchetClient = connectToRatchet($ratchetHost, $ratchetPort, $token);
            if ($ratchetClient) {
                $ratchetClients[$connectionId] = $ratchetClient;
                $ratchetClient->setTimeout(0); // Set to non-blocking after successful connection
            } else {
                $connection->close();
                return;
            }
        } else {
            logMessage("No token found in request");
            $connection->close();
        }
    })
    ->onText(function (Server $server, Connection $connection, Text $message) use (&$connections, &$ratchetClients) {
        logMessage("Received message from browser");

        $connectionId = spl_object_hash($connection);

        if (isset($ratchetClients[$connectionId])) {
            try {
                logMessage("Forwarding message to Ratchet server");
                $ratchetClients[$connectionId]->text($message->getContent());
            } catch (\Exception $e) {
                logMessage("Error sending message to Ratchet: " . $e->getMessage());
                // Attempt to reconnect
                $token = parse_url($ratchetClients[$connectionId]->getUri(), PHP_URL_QUERY);
                $ratchetClients[$connectionId] = connectToRatchet($ratchetHost, $ratchetPort, $token);
                if (!$ratchetClients[$connectionId]) {
                    $connection->close();
                }
            }
        } else {
            logMessage("Error: Ratchet server not connected for connection ID: $connectionId");
            $connection->close();
        }
    })
    ->onDisconnect(function (Server $server, Connection $connection) use (&$connections, &$ratchetClients) {
        logMessage("Client disconnected");

        $connectionId = spl_object_hash($connection);

        if (isset($ratchetClients[$connectionId])) {
            $ratchetClients[$connectionId]->close();
            unset($ratchetClients[$connectionId]);
        }

        if (isset($connections[$connectionId])) {
            unset($connections[$connectionId]);
        }
    })
    ->start();

} catch (\Throwable $e) {
    logMessage("# ERROR: {$e->getMessage()}");
}

运行日志

[2024-08-13 13:49:20] # Listening on port 8091
[2024-08-13 13:49:22] Handshake received
[2024-08-13 13:49:22] Token found: 1003j59399cguvngtresptj4c4
[2024-08-13 13:49:22] Successfully connected to Ratchet server
[2024-08-13 13:49:24] Handshake received
[2024-08-13 13:49:24] Token found: v9ikq2e70dlt3ab9jajk9936s7
[2024-08-13 13:49:24] Successfully connected to Ratchet server
[2024-08-13 13:49:26] Received message from browser
[2024-08-13 13:49:26] Forwarding message to Ratchet server
[2024-08-13 13:49:38] Handshake received
[2024-08-13 13:49:38] Token found: 1003j59399cguvngtresptj4c4

问题分析与修复方案

核心问题

  1. 回调参数混淆:connectToRatchet中onText回调的第二个参数是Phrity Client自身的Connection对象,不是浏览器的连接,导致用spl_object_hash($connection)无法关联到对应的浏览器连接。
  2. 主循环未处理客户端IO:代理服务器的主事件循环(Ratchet Server的start())没有主动轮询Phrity Client的连接状态,即使设置了非阻塞模式,也不会触发消息接收逻辑。

修复步骤

1. 修正回调参数关联

修改connectToRatchet函数,直接传入浏览器连接的ID,代替混淆的Connection对象:

function connectToRatchet($host, $port, $token, $connectionId) {
    $ratchetClient = new Client("ws://$host:$port?token=$token");
    
    $ratchetClient->addMiddleware(new \WebSocket\Middleware\CloseHandler());
    $ratchetClient->addMiddleware(new \WebSocket\Middleware\PingResponder());
    
    $ratchetClient->onText(function (Client $client, \WebSocket\Connection $clientConn, Message $message) use (&$connections, $connectionId) {
        logMessage("Received message from Ratchet server: " . $message->getContent());
        
        if (isset($connections[$connectionId])) {
            $browserConnection = $connections[$connectionId];
            
            if ($browserConnection->isOpen()) {
                logMessage("Forwarding message to browser connection $connectionId");
                $browserConnection->text($message->getContent());
            } else {
                logMessage("Error: Browser connection $connectionId is closed");
                $client->close();
                unset($connections[$connectionId]);
            }
        } else {
            logMessage("Error: Browser connection not found for Ratchet client with connection ID $connectionId");
        }
    });

    try {
        $ratchetClient->connect();
        logMessage("Successfully connected to Ratchet server");
        return $ratchetClient;
    } catch (\Exception $e) {
        logMessage("Failed to connect to Ratchet server: " . $e->getMessage());
        return null;
    }
}

同时修改onHandshake中的调用:

$ratchetClient = connectToRatchet($ratchetHost, $ratchetPort, $token, $connectionId);

2. 主循环添加客户端消息轮询

利用ReactPHP事件循环,定期触发所有Phrity Client的receive()方法,处理Ratchet发来的消息:
在$server->start()之前添加以下代码:

// 获取主事件循环
$loop = \React\EventLoop\Loop::get();

// 每10ms轮询一次客户端,处理消息
$loop->addPeriodicTimer(0.01, function () use (&$ratchetClients) {
    foreach ($ratchetClients as $key => $client) {
        if (!$client->isConnected()) {
            unset($ratchetClients[$key]);
            continue;
        }
        try {
            $client->receive();
        } catch (\Exception $e) {
            logMessage("Error receiving from Ratchet client: " . $e->getMessage());
            $client->close();
            unset($ratchetClients[$key]);
        }
    }
});

3. 修正命名空间问题

原代码中Middleware实例化缺少完整命名空间,需统一改为\WebSocket\Middleware\XXX,避免类找不到的错误。

内容的提问来源于stack exchange,提问作者King

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 19:52:01