env->load(__DIR__ . '/.env'); } // 根据APP_ENV加载环境特定的.env文件 $appEnv = getenv('APP_ENV') ?: ''; if ($appEnv) { $envFile = __DIR__ . '/.env.' . $appEnv; if (is_file($envFile)) { $app->env->load($envFile); } } // 初始化App实例(必须,否则Cache等组件无法获取配置) // 这会加载配置文件,包括cache.php if (!$app->initialized()) { $app->initialize(); } // 获取ThinkPHP Cache实例(三种方式任选其一) // 方式1: 通过App实例直接访问(推荐,在CLI模式下最可靠) $cache = $app->cache; // 方式2: 通过容器获取 // $cache = $app->make('cache'); // 方式3: 使用Facade(在CLI模式下可能不可用,不推荐) // use think\facade\Cache; // Cache::get('key'); // Cache::set('key', 'value', 3600); // Cache使用示例: // $cache->get('key'); // 获取缓存 // $cache->get('key', 'default'); // 获取缓存,不存在时返回默认值 // $cache->set('key', 'value', 3600); // 设置缓存,有效期3600秒 // $cache->has('key'); // 检查缓存是否存在 // $cache->delete('key'); // 删除缓存 // $cache->clear(); // 清空所有缓存 // $cache->tag('tag_name')->set('key', 'value'); // 使用标签缓存 // $cache->tag('tag_name')->clear(); // 清除标签下的所有缓存 use Ratchet\App as RatchetApp; use Ratchet\ConnectionInterface; use Ratchet\MessageComponentInterface; // 配置WebSocket服务器 // Ratchet\App 的 $httpHost 会被用于路由 Host 匹配(默认要求必须等于 ws:// 的 host) // 注意:Ratchet\App 构造函数内部会把 $httpHost 传给 FlashPolicy::addAllowedAccess(), // 为空会触发 “Invalid domain”。因此这里必须是一个合法域名/IP(用 localhost 兜底)。 // 若希望通过任意 IP/域名访问:不要在构造里用空值;而是在 route(...) 的第4个参数传 '', // 让该路由不限制 Host。 $httpHost = getenv('WS_HTTP_HOST'); $httpHost = ($httpHost === false) ? 'localhost' : trim((string)$httpHost); if ($httpHost === '') { $httpHost = 'localhost'; } $port = getenv('WS_PORT') ?: 8080; // WebSocket服务器端口 $address = '0.0.0.0'; // 监听所有网络接口 // 记录服务器启动信息 ws_echo("[WebSocket服务器] 正在启动..."); ws_echo("[WebSocket服务器] 配置信息: HTTP_HOST={$httpHost}, PORT={$port}, ADDRESS={$address}"); // 创建WebSocket服务器应用 - 参数顺序:httpHost, port, address $ratchetApp = new RatchetApp($httpHost, $port, $address); ws_echo("[WebSocket服务器] 应用创建成功"); // 获取所有addon(与InitAddon.php逻辑保持一致) // 1. 从数据库获取插件列表 use app\model\system\Addon; $addonDir = __DIR__ . '/addon'; $addonNames = []; // 从数据库获取addon列表(使用Cache缓存,避免频繁查询数据库) $cacheKey = 'websocket_addon_list'; $cacheExpire = 300; // 缓存5分钟 try { // 尝试从缓存获取addon列表 $cachedAddons = $cache->get($cacheKey); if ($cachedAddons !== null && !empty($cachedAddons)) { ws_echo("[WebSocket服务器] 从缓存获取插件列表"); $current_addons = $cachedAddons; } else { ws_echo("[WebSocket服务器] 从数据库获取插件列表"); // 尝试获取数据库连接并确保连接有效 try { // 尝试初始化数据库连接 $addon_model = new Addon(); $addon_data = $addon_model->getAddonList([], 'name,status'); $current_addons = $addon_data['data']; // 将结果存入缓存 $cache->set($cacheKey, $current_addons, $cacheExpire); ws_echo("[WebSocket服务器] 插件列表已缓存(有效期: {$cacheExpire}秒)"); } catch (\Exception $dbEx) { ws_echo("[WebSocket服务器] 数据库操作失败: {$dbEx->getMessage()}", 'error'); ws_echo("[WebSocket服务器] 尝试重新初始化数据库连接..."); // 尝试重新初始化应用和数据库连接 try { // 重新初始化应用 $app->initialize(); $cache = $app->cache; // 再次尝试获取插件列表 $addon_model = new Addon(); $addon_data = $addon_model->getAddonList([], 'name,status'); $current_addons = $addon_data['data']; // 将结果存入缓存 $cache->set($cacheKey, $current_addons, $cacheExpire); ws_echo("[WebSocket服务器] 重新连接数据库成功,插件列表已缓存"); } catch (\Exception $retryEx) { ws_echo("[WebSocket服务器] 重新连接数据库失败: {$retryEx->getMessage()}", 'error'); ws_echo("[WebSocket服务器] 回退到直接扫描目录获取插件列表"); // 回退到直接扫描目录 $addonNames = []; if (is_dir($addonDir)) { $handle = opendir($addonDir); while (($file = readdir($handle)) !== false) { if ($file != '.' && $file != '..' && is_dir($addonDir . '/' . $file)) { $addonNames[] = $file; } } closedir($handle); sort($addonNames); } $current_addon_names = $addonNames; $enabled_addons = $addonNames; // 回退模式下默认所有插件都启用 // 跳过后续的数据库操作 throw new \Exception('数据库连接失败,已回退到目录扫描模式'); } } } $db_addon_names = array_column($current_addons, 'name'); // 2. 从addon目录读取插件列表 $dir_addon_names = []; if (is_dir($addonDir)) { $dir_handle = opendir($addonDir); if ($dir_handle) { while (($file = readdir($dir_handle)) !== false) { if ($file != '.' && $file != '..' && is_dir($addonDir . '/' . $file)) { $dir_addon_names[] = $file; } } closedir($dir_handle); } } // 3. 使用数据库中的插件列表进行后续处理 $current_addon_names = $db_addon_names; // 4. 记录已启用的插件 $enabled_addons = []; foreach ($current_addons as $addon) { if (isset($addon['status']) && $addon['status'] == 1) { $enabled_addons[] = $addon['name']; } } ws_echo("[WebSocket服务器] 从数据库获取的插件列表: " . implode(', ', $current_addon_names)); ws_echo("[WebSocket服务器] 已启用的插件: " . implode(', ', $enabled_addons)); ws_echo("[WebSocket服务器] 目录中存在的插件: " . implode(', ', $dir_addon_names)); // 5. 比较数据库和目录插件列表的差异 $db_only_addons = array_diff($db_addon_names, $dir_addon_names); $dir_only_addons = array_diff($dir_addon_names, $db_addon_names); if (!empty($db_only_addons)) { ws_echo("[WebSocket服务器] 数据库中存在但目录中不存在的插件: " . implode(', ', $db_only_addons)); } if (!empty($dir_only_addons)) { ws_echo("[WebSocket服务器] 目录中存在但数据库中不存在的插件: " . implode(', ', $dir_only_addons)); } } catch (\Exception $e) { ws_echo("[WebSocket服务器] 获取插件列表失败: " . $e->getMessage(), 'error'); ws_echo("[WebSocket服务器] 回退到直接扫描目录获取插件列表"); // 回退到直接扫描目录 $addonNames = []; if (is_dir($addonDir)) { $handle = opendir($addonDir); while (($file = readdir($handle)) !== false) { if ($file != '.' && $file != '..' && is_dir($addonDir . '/' . $file)) { $addonNames[] = $file; } } closedir($handle); sort($addonNames); } $current_addon_names = $addonNames; $enabled_addons = $addonNames; // 回退模式下默认所有插件都启用 } // 为每个addon注册WebSocket控制器,并记录注册情况 $registeredAddons = []; $unregisteredAddons = []; $disabledAddons = []; $missingDirAddons = []; foreach ($current_addon_names as $addonName) { // 检查插件是否已启用 if (!in_array($addonName, $enabled_addons)) { ws_echo("[{$addonName}] 插件未启用,跳过WebSocket控制器注册"); $disabledAddons[] = $addonName; continue; } // 检查插件目录是否存在 if (!is_dir($addonDir . '/' . $addonName)) { ws_echo("[{$addonName}] 插件目录不存在,跳过WebSocket控制器注册"); $missingDirAddons[] = $addonName; continue; } $webSocketClass = "addon\\{$addonName}\\api\\controller\\WebSocket"; // 检查WebSocket控制器是否存在 try { if (class_exists($webSocketClass)) { // 注册到/ws/{addonName}路径 $path = '/ws/' . $addonName; // 允许任意 Origin,并且不限制 Host(支持通过任意 IP/域名访问) $ratchetApp->route($path, new $webSocketClass(), array('*'), ''); ws_echo("已注册WebSocket控制器:{$webSocketClass} 到路径 {$path}"); $registeredAddons[] = $addonName; } else { ws_echo("[{$addonName}] WebSocket控制器不存在 ({$webSocketClass})"); $unregisteredAddons[] = $addonName; } } catch (\Exception $e) { ws_echo("[{$addonName}] 检查WebSocket控制器时出错:{$e->getMessage()}", 'error'); $unregisteredAddons[] = $addonName; } } // 实现默认的/ws路径的简单测试控制器 class DefaultWebSocketController implements MessageComponentInterface { protected $clients; public function __construct() { $this->clients = new \SplObjectStorage; } public function onOpen(ConnectionInterface $conn) { $this->clients->attach($conn); ws_echo("[默认路径] New connection! ({$conn->resourceId})"); $conn->send(json_encode([ 'type' => 'welcome', 'message' => '欢迎连接到默认WebSocket测试路径', 'info' => '此路径仅用于测试,不提供实际功能。请使用/ws/{addonName}连接到具体的addon服务。' ])); } public function onMessage(ConnectionInterface $conn, $msg) { ws_echo("[默认路径] Received message from {$conn->resourceId}: $msg"); try { // 检查数据库连接状态 if (function_exists('checkDatabaseConnection')) { checkDatabaseConnection(); } $data = json_decode($msg, true); if (isset($data['action']) && $data['action'] === 'ping') { $conn->send(json_encode(['type' => 'pong'])); } else { $conn->send(json_encode([ 'type' => 'info', 'message' => '收到消息,但默认路径不提供实际功能', 'received' => $data ])); } } catch (\Exception $e) { ws_echo("[默认路径] 解析消息失败: {$e->getMessage()}", 'error'); $conn->send(json_encode(['type' => 'error', 'message' => '解析消息失败: ' . $e->getMessage()])); } } public function onClose(ConnectionInterface $conn) { $this->clients->detach($conn); ws_echo("[默认路径] Connection {$conn->resourceId} has disconnected"); } public function onError(ConnectionInterface $conn, \Exception $e) { ws_echo("[默认路径] An error has occurred: {$e->getMessage()}", 'error'); $conn->close(); } } // 注册默认的/ws路径测试控制器 // 默认测试路径同样不限制 Host $ratchetApp->route('/ws', new DefaultWebSocketController(), array('*'), ''); ws_echo("已注册默认WebSocket测试控制器到路径 /ws"); /** * 检查数据库连接状态并在需要时重新初始化 * @return bool 连接是否有效 */ function checkDatabaseConnection() { global $app, $cache; try { // 检查缓存中的连接状态 $connStatus = $cache->get('db_connection_status'); if ($connStatus === 'error') { ws_echo("[WebSocket服务器] 检测到数据库连接错误,尝试重新初始化..."); $app->initialize(); $cache = $app->cache; } // 测试连接 $addon_model = new \app\model\system\Addon(); $addon_model->getAddonList([], 'name', 1, 1); // 更新连接状态 $cache->set('db_connection_status', 'active', 60); return true; } catch (\Exception $e) { ws_echo("[WebSocket服务器] 数据库连接检查失败: {$e->getMessage()}", 'warning'); // 尝试重新初始化 try { $app->initialize(); $cache = $app->cache; // 再次测试 $addon_model = new \app\model\system\Addon(); $addon_model->getAddonList([], 'name', 1, 1); $cache->set('db_connection_status', 'active', 60); ws_echo("[WebSocket服务器] 数据库连接已重新初始化成功"); return true; } catch (\Exception $retryEx) { ws_echo("[WebSocket服务器] 数据库连接重新初始化失败: {$retryEx->getMessage()}", 'error'); $cache->set('db_connection_status', 'error', 60); return false; } } } // 缓存WebSocket服务器信息(可选,用于其他服务查询) $serverInfoKey = 'websocket_server_info'; $cache->set($serverInfoKey, [ 'host' => $httpHost, 'port' => $port, 'address' => $address, 'started_at' => date('Y-m-d H:i:s'), 'registered_addons' => $registeredAddons ], 0); // 0表示永久缓存,直到手动删除 // 启动时检查数据库连接 ws_echo("[WebSocket服务器] 启动时检查数据库连接..."); checkDatabaseConnection(); ws_echo("[WebSocket服务器] 数据库连接检查完成"); ws_echo("WebSocket服务器已启动,监听地址: ws://{$httpHost}:{$port}"); // 显示已注册WebSocket控制器的addon路径 ws_echo("\n已注册WebSocket控制器的addon路径:"); if (!empty($registeredAddons)) { foreach ($registeredAddons as $addonName) { ws_echo(" - ws://{$httpHost}:{$port}/ws/{$addonName} (已注册)"); } } else { ws_echo(" - 暂无已注册的WebSocket控制器"); } // 显示未注册WebSocket控制器的addon路径 ws_echo("\n未注册WebSocket控制器的addon路径:"); if (!empty($unregisteredAddons)) { foreach ($unregisteredAddons as $addonName) { ws_echo(" - ws://{$httpHost}:{$port}/ws/{$addonName} (未注册)"); } } else { ws_echo(" - 所有已启用且目录存在的addon都已注册WebSocket控制器"); } // 显示未启用的addon ws_echo("\n未启用的addon:"); if (!empty($disabledAddons)) { foreach ($disabledAddons as $addonName) { ws_echo(" - {$addonName} (未启用)"); } } else { ws_echo(" - 所有addon都已启用"); } // 显示目录不存在的addon ws_echo("\n目录不存在的addon:"); if (!empty($missingDirAddons)) { foreach ($missingDirAddons as $addonName) { ws_echo(" - {$addonName} (目录不存在)"); } } else { ws_echo(" - 所有已启用的addon目录都存在"); } // 设置基本的信号处理 if (extension_loaded('pcntl')) { pcntl_signal(SIGINT, function() { ws_echo("[WebSocket服务器] 收到终止信号,正在停止..."); ws_echo("[WebSocket服务器] 已停止"); exit(0); }); } // 运行服务器 ws_echo("[WebSocket服务器] 启动主服务器进程"); ws_echo("\n默认测试路径:"); ws_echo(" - ws://{$httpHost}:{$port}/ws (默认路径,用于连接测试)"); ws_echo("按 Ctrl+C 停止服务器"); ws_info("WebSocket服务器已启动,监听地址: ws://{$httpHost}:{$port}"); $ratchetApp->run();