Fix event loop initialization and handle pipe errors
- Modified startup script to instantiate GameProcessServer separately
before wrapping in WsServer/HttpServer, allowing direct access to
set event loop after IoServer creation
- Implemented setLoop() method to set event loop after construction
- Added error handling for fwrite() to gracefully handle broken pipes
when process closes
- Suppressed fread() warnings with @ operator to avoid noise
- Simplified startOutputReader() since event loop polling is now
handled directly in onOpen()
- Added null checks for event loop before using in timers
- Server now properly starts with continuous 100ms output polling
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
081903563a
commit
52e465177c
|
|
@ -3,6 +3,7 @@ namespace Game\Core;
|
|||
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
/**
|
||||
* 进程转发 WebSocket 服务器
|
||||
|
|
@ -14,12 +15,24 @@ class GameProcessServer implements MessageComponentInterface
|
|||
{
|
||||
protected array $clients = [];
|
||||
protected array $processes = [];
|
||||
protected ?LoopInterface $loop = null;
|
||||
protected array $timers = [];
|
||||
|
||||
public function __construct()
|
||||
public function __construct(?LoopInterface $loop = null)
|
||||
{
|
||||
$this->loop = $loop;
|
||||
echo "[进程服务器] 初始化\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件循环(用于延迟初始化)
|
||||
*/
|
||||
public function setLoop(LoopInterface $loop): void
|
||||
{
|
||||
$this->loop = $loop;
|
||||
echo "[进程服务器] 事件循环已设置\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端连接时
|
||||
*/
|
||||
|
|
@ -37,6 +50,27 @@ class GameProcessServer implements MessageComponentInterface
|
|||
'type' => 'system',
|
||||
'message' => '游戏进程已启动,正在加载...'
|
||||
]);
|
||||
|
||||
// 设置定时器,每100ms读取一次进程输出(实时转发)
|
||||
// 只有在事件循环可用时才设置
|
||||
if ($this->loop) {
|
||||
$connId = $conn->resourceId;
|
||||
$timer = $this->loop->addPeriodicTimer(0.1, function() use ($conn, $connId) {
|
||||
// 检查连接是否仍然有效
|
||||
if (!isset($this->clients[$connId]) || !isset($this->processes[$connId])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取进程输出并发送给客户端
|
||||
$this->readProcessOutput($conn);
|
||||
});
|
||||
|
||||
$this->timers[$connId] = $timer;
|
||||
echo "[定时器] 为连接 {$connId} 启动输出轮询\n";
|
||||
} else {
|
||||
echo "[警告] 事件循环未设置,无法启动输出轮询\n";
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->sendError($conn, '启动游戏失败: ' . $e->getMessage());
|
||||
$conn->close();
|
||||
|
|
@ -103,17 +137,8 @@ class GameProcessServer implements MessageComponentInterface
|
|||
*/
|
||||
private function startOutputReader(string $connId, $stdout, $stderr): void
|
||||
{
|
||||
// 创建后台读取循环
|
||||
// 这里使用一个简单的轮询机制
|
||||
// 实际上应该使用事件循环(已由Ratchet处理)
|
||||
// 我们在收到消息时检查输出
|
||||
|
||||
// 创建管道监控文件
|
||||
$readQueuesFile = sys_get_temp_dir() . '/game_' . $connId . '.pipes';
|
||||
file_put_contents($readQueuesFile, json_encode([
|
||||
'stdout' => (int)$stdout,
|
||||
'stderr' => (int)$stderr,
|
||||
]));
|
||||
// 已由 onOpen() 中的事件循环定时器处理
|
||||
// 不再需要额外的初始化
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -133,10 +158,16 @@ class GameProcessServer implements MessageComponentInterface
|
|||
// 将输入写入进程的STDIN
|
||||
$process = $this->processes[$from->resourceId] ?? null;
|
||||
if ($process && is_resource($process['stdin'])) {
|
||||
fwrite($process['stdin'], $input . "\n");
|
||||
$bytesWritten = @fwrite($process['stdin'], $input . "\n");
|
||||
if ($bytesWritten === false || $bytesWritten === 0) {
|
||||
// 写入失败,可能进程已关闭
|
||||
echo "[警告] 无法写入进程 {$from->resourceId},进程可能已关闭\n";
|
||||
// 不中断连接,等待下一次尝试或超时
|
||||
} else {
|
||||
fflush($process['stdin']);
|
||||
echo "[输入] {$from->resourceId}: {$input}\n";
|
||||
}
|
||||
}
|
||||
} elseif ($type === 'ping') {
|
||||
$this->sendMessage($from, ['type' => 'pong']);
|
||||
}
|
||||
|
|
@ -163,7 +194,7 @@ class GameProcessServer implements MessageComponentInterface
|
|||
|
||||
// 读取stdout
|
||||
while (true) {
|
||||
$chunk = fread($process['stdout'], $bufferSize);
|
||||
$chunk = @fread($process['stdout'], $bufferSize);
|
||||
if ($chunk === '' || $chunk === false) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -172,7 +203,7 @@ class GameProcessServer implements MessageComponentInterface
|
|||
|
||||
// 读取stderr
|
||||
while (true) {
|
||||
$chunk = fread($process['stderr'], $bufferSize);
|
||||
$chunk = @fread($process['stderr'], $bufferSize);
|
||||
if ($chunk === '' || $chunk === false) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -195,6 +226,13 @@ class GameProcessServer implements MessageComponentInterface
|
|||
{
|
||||
$connId = $conn->resourceId;
|
||||
|
||||
// 取消定时器
|
||||
if (isset($this->timers[$connId]) && $this->loop) {
|
||||
$this->loop->cancelTimer($this->timers[$connId]);
|
||||
unset($this->timers[$connId]);
|
||||
echo "[定时器] 取消连接 {$connId} 的输出轮询\n";
|
||||
}
|
||||
|
||||
// 关闭进程
|
||||
$process = $this->processes[$connId] ?? null;
|
||||
if ($process) {
|
||||
|
|
@ -225,6 +263,14 @@ class GameProcessServer implements MessageComponentInterface
|
|||
public function onError(ConnectionInterface $conn, \Exception $e)
|
||||
{
|
||||
echo "[错误] {$conn->resourceId}: {$e->getMessage()}\n";
|
||||
|
||||
// 取消定时器
|
||||
$connId = $conn->resourceId;
|
||||
if (isset($this->timers[$connId]) && $this->loop) {
|
||||
$this->loop->cancelTimer($this->timers[$connId]);
|
||||
unset($this->timers[$connId]);
|
||||
}
|
||||
|
||||
$this->onClose($conn);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ namespace Game\Entities;
|
|||
|
||||
class Partner extends Actor
|
||||
{
|
||||
public $id;
|
||||
// Partner特有的天赋加成(与 Actor 不同)
|
||||
public static array $talentBonus = [
|
||||
'hp' => 20,
|
||||
|
|
|
|||
39
test-websocket.php
Normal file
39
test-websocket.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
/**
|
||||
* WebSocket 服务器测试脚本
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use WebSocket\Client;
|
||||
|
||||
echo "连接到 WebSocket 服务器: ws://localhost:9002\n";
|
||||
|
||||
try {
|
||||
$client = new Client("ws://localhost:9002");
|
||||
echo "[成功] 连接到服务器\n";
|
||||
|
||||
// 接收欢迎消息
|
||||
$message = $client->receive();
|
||||
echo "[收到] " . $message . "\n";
|
||||
|
||||
// 发送测试输入
|
||||
echo "[发送] 测试命令\n";
|
||||
$client->send(json_encode([
|
||||
'type' => 'input',
|
||||
'input' => '1'
|
||||
]));
|
||||
|
||||
// 接收响应
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$message = $client->receive();
|
||||
echo "[收到] " . substr($message, 0, 100) . "...\n";
|
||||
}
|
||||
|
||||
$client->close();
|
||||
echo "[完成] 测试成功\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "[错误] " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
|
@ -17,18 +17,27 @@ use Ratchet\Http\HttpServer;
|
|||
use Ratchet\WebSocket\WsServer;
|
||||
use Game\Core\GameProcessServer;
|
||||
|
||||
// 创建WebSocket服务器
|
||||
$ws = new WsServer(new GameProcessServer());
|
||||
// 分步骤创建应用栈,以便能访问 GameProcessServer 实例
|
||||
echo "[初始化] 创建 GameProcessServer 实例...\n";
|
||||
$gameServer = new GameProcessServer(null);
|
||||
|
||||
// 用HTTP服务器包装
|
||||
$http = new HttpServer($ws);
|
||||
echo "[初始化] 创建 WsServer...\n";
|
||||
$wsServer = new WsServer($gameServer);
|
||||
|
||||
// 创建IO服务器,监听9002端口
|
||||
$server = IoServer::factory(
|
||||
$http,
|
||||
9002,
|
||||
'0.0.0.0'
|
||||
);
|
||||
echo "[初始化] 创建 HttpServer...\n";
|
||||
$httpServer = new HttpServer($wsServer);
|
||||
|
||||
// 创建IO服务器(内部创建事件循环)
|
||||
echo "[初始化] 创建 IO 服务器...\n";
|
||||
$server = IoServer::factory($httpServer, 9002, '0.0.0.0');
|
||||
echo "[初始化] IO 服务器创建成功\n";
|
||||
|
||||
// 现在获取事件循环并设置给 GameProcessServer 实例
|
||||
$loop = $server->loop;
|
||||
echo "[初始化] 获取事件循环成功\n";
|
||||
|
||||
$gameServer->setLoop($loop);
|
||||
echo "[初始化] 为 GameProcessServer 设置了事件循环\n";
|
||||
|
||||
echo <<<'ASCII'
|
||||
╔══════════════════════════════════════════╗
|
||||
|
|
@ -48,6 +57,7 @@ echo <<<'ASCII'
|
|||
💡 特点:
|
||||
✓ 无需修改游戏代码
|
||||
✓ 每个用户独立的游戏进程
|
||||
✓ 实时输出(100ms轮询)
|
||||
✓ 完整的ANSI颜色支持
|
||||
✓ 实时交互
|
||||
|
||||
|
|
@ -55,4 +65,20 @@ echo <<<'ASCII'
|
|||
|
||||
ASCII;
|
||||
|
||||
$server->run();
|
||||
ob_implicit_flush();
|
||||
fflush(STDOUT);
|
||||
|
||||
echo "\n[启动] 准备启动事件循环...\n";
|
||||
fflush(STDOUT);
|
||||
|
||||
try {
|
||||
echo "[启动] 调用 \$loop->run()...\n";
|
||||
fflush(STDOUT);
|
||||
$loop->run();
|
||||
echo "[启动] \$loop->run() 已返回\n";
|
||||
} catch (Exception $e) {
|
||||
echo "\n[错误] 事件循环异常: " . $e->getMessage() . "\n";
|
||||
echo $e->getTraceAsString() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user