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'; // 监听所有网络接口 // 创建WebSocket服务器应用 - 参数顺序:httpHost, port, address $ratchetApp = new RatchetApp($httpHost, $port, $address); // 获取所有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)) { echo "[WebSocket服务器] 从缓存获取插件列表\n"; $current_addons = $cachedAddons; } else { echo "[WebSocket服务器] 从数据库获取插件列表\n"; $addon_model = new Addon(); $addon_data = $addon_model->getAddonList([], 'name,status'); $current_addons = $addon_data['data']; // 将结果存入缓存 $cache->set($cacheKey, $current_addons, $cacheExpire); echo "[WebSocket服务器] 插件列表已缓存(有效期: {$cacheExpire}秒)\n"; } $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']; } } echo "[WebSocket服务器] 从数据库获取的插件列表: " . implode(', ', $current_addon_names) . "\n"; echo "[WebSocket服务器] 已启用的插件: " . implode(', ', $enabled_addons) . "\n"; echo "[WebSocket服务器] 目录中存在的插件: " . implode(', ', $dir_addon_names) . "\n"; // 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)) { echo "[WebSocket服务器] 数据库中存在但目录中不存在的插件: " . implode(', ', $db_only_addons) . "\n"; } if (!empty($dir_only_addons)) { echo "[WebSocket服务器] 目录中存在但数据库中不存在的插件: " . implode(', ', $dir_only_addons) . "\n"; } } catch (\Exception $e) { echo "[WebSocket服务器] 获取插件列表失败: " . $e->getMessage() . "\n"; echo "[WebSocket服务器] 回退到直接扫描目录获取插件列表\n"; // 回退到直接扫描目录 $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)) { echo "[{$addonName}] 插件未启用,跳过WebSocket控制器注册\n"; $disabledAddons[] = $addonName; continue; } // 检查插件目录是否存在 if (!is_dir($addonDir . '/' . $addonName)) { echo "[{$addonName}] 插件目录不存在,跳过WebSocket控制器注册\n"; $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('*'), ''); echo "已注册WebSocket控制器:{$webSocketClass} 到路径 {$path}\n"; $registeredAddons[] = $addonName; } else { echo "[{$addonName}] WebSocket控制器不存在 ({$webSocketClass})\n"; $unregisteredAddons[] = $addonName; } } catch (\Exception $e) { echo "[{$addonName}] 检查WebSocket控制器时出错:{$e->getMessage()}\n"; $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); echo "[默认路径] New connection! ({$conn->resourceId})\n"; $conn->send(json_encode([ 'type' => 'welcome', 'message' => '欢迎连接到默认WebSocket测试路径', 'info' => '此路径仅用于测试,不提供实际功能。请使用/ws/{addonName}连接到具体的addon服务。' ])); } public function onMessage(ConnectionInterface $conn, $msg) { echo "[默认路径] Received message from {$conn->resourceId}: $msg\n"; try { $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) { $conn->send(json_encode(['type' => 'error', 'message' => '解析消息失败: ' . $e->getMessage()])); } } public function onClose(ConnectionInterface $conn) { $this->clients->detach($conn); echo "[默认路径] Connection {$conn->resourceId} has disconnected\n"; } public function onError(ConnectionInterface $conn, \Exception $e) { echo "[默认路径] An error has occurred: {$e->getMessage()}\n"; $conn->close(); } } // 注册默认的/ws路径测试控制器 // 默认测试路径同样不限制 Host $ratchetApp->route('/ws', new DefaultWebSocketController(), array('*'), ''); echo "已注册默认WebSocket测试控制器到路径 /ws\n"; // 缓存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表示永久缓存,直到手动删除 echo "WebSocket服务器已启动,监听地址: ws://{$httpHost}:{$port}\n"; // 显示已注册WebSocket控制器的addon路径 echo "\n已注册WebSocket控制器的addon路径:\n"; if (!empty($registeredAddons)) { foreach ($registeredAddons as $addonName) { echo " - ws://{$httpHost}:{$port}/ws/{$addonName} (已注册)\n"; } } else { echo " - 暂无已注册的WebSocket控制器\n"; } // 显示未注册WebSocket控制器的addon路径 echo "\n未注册WebSocket控制器的addon路径:\n"; if (!empty($unregisteredAddons)) { foreach ($unregisteredAddons as $addonName) { echo " - ws://{$httpHost}:{$port}/ws/{$addonName} (未注册)\n"; } } else { echo " - 所有已启用且目录存在的addon都已注册WebSocket控制器\n"; } // 显示未启用的addon echo "\n未启用的addon:\n"; if (!empty($disabledAddons)) { foreach ($disabledAddons as $addonName) { echo " - {$addonName} (未启用)\n"; } } else { echo " - 所有addon都已启用\n"; } // 显示目录不存在的addon echo "\n目录不存在的addon:\n"; if (!empty($missingDirAddons)) { foreach ($missingDirAddons as $addonName) { echo " - {$addonName} (目录不存在)\n"; } } else { echo " - 所有已启用的addon目录都存在\n"; } echo "\n默认测试路径:\n"; echo " - ws://{$httpHost}:{$port}/ws (默认路径,用于连接测试)\n"; echo "按 Ctrl+C 停止服务器\n"; // 运行服务器 $ratchetApp->run();