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
问题分析与修复方案
核心问题
- 回调参数混淆:
connectToRatchet中onText回调的第二个参数是Phrity Client自身的Connection对象,不是浏览器的连接,导致用spl_object_hash($connection)无法关联到对应的浏览器连接。 - 主循环未处理客户端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
相关产品推荐
相关产品推荐

