Files
shop-platform/src/ws_server.php

335 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// WebSocket服务器启动脚本
// 使用方法php ws_server.php
// 启用错误报告
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require __DIR__ . '/vendor/autoload.php';
// 初始化ThinkPHP应用环境加载配置和环境变量主要用于数据库连接和其他配置
use think\App;
// 创建应用实例传入应用根目录src目录
// @intelephense-ignore-next-line
$app = new App(__DIR__);
// 加载环境变量
if (is_file(__DIR__ . '/.env')) {
$app->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> 的 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();