Files
shop-platform/src/ws_server.php
ZF sun ef708e6b40 feat(WebSocket): 添加数据库连接检查和文件预览功能
- 在DefaultWebSocketController中添加数据库连接检查功能
- 实现文件预览和下载功能及相关API接口
- 更新测试页面支持文件预览和下载操作
- 移除旧的数据库维护子进程机制,改为函数检查
- 在构建请求数据时添加文件字段支持
2026-01-26 08:40:07 +08:00

535 lines
19 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';
// 引入应用公共文件包含log_write函数
require __DIR__ . '/app/common.php';
/**
* WebSocket服务器日志封装函数
* 简化log_write函数的调用自动添加WebSocket服务器前缀
*
* @param string $message 日志内容
* @param string $level 日志级别info, warning, error
* @param int $callerDep 调用栈深度默认1
* @return void
*/
function ws_log(string $message, string $level = 'info', int $callerDep = 1): void
{
log_write($message, $level, 'websocket-server.log', $callerDep + 1);
}
/**
* WebSocket服务器错误日志封装函数
* 简化错误日志的记录自动添加WebSocket服务器前缀
*
* @param string $message 错误信息
* @param int $callerDep 调用栈深度默认1
* @return void
*/
function ws_error(string $message, int $callerDep = 1): void
{
log_write($message, 'error', 'websocket-server.log', $callerDep + 1);
}
/**
* WebSocket服务器警告日志封装函数
* 简化警告日志的记录自动添加WebSocket服务器前缀
*
* @param string $message 警告信息
* @param int $callerDep 调用栈深度默认1
* @return void
*/
function ws_warning(string $message, int $callerDep = 1): void
{
log_write($message, 'warning', 'websocket-server.log', $callerDep + 1);
}
/**
* WebSocket服务器信息日志封装函数
* 简化信息日志的记录自动添加WebSocket服务器前缀
*
* @param string $message 信息内容
* @param int $callerDep 调用栈深度默认1
* @return void
*/
function ws_info(string $message, int $callerDep = 1): void
{
log_write($message, 'info', 'websocket-server.log', $callerDep + 1);
}
/**
* WebSocket服务器输出封装函数
* 同时处理日志记录和控制台输出,使代码更简洁
*
* @param string $message 输出内容
* @param string $level 日志级别info, warning, error
* @param int $callerDep 调用栈深度默认1
* @return void
*/
function ws_echo(string $message, string $level = 'info', int $callerDep = 1): void
{
// 调用相应的日志函数
switch ($level) {
case 'error':
ws_error($message, $callerDep + 1);
break;
case 'warning':
ws_warning($message, $callerDep + 1);
break;
default:
ws_info($message, $callerDep + 1);
break;
}
// 输出到控制台
echo $message . "\n";
}
// 初始化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'; // 监听所有网络接口
// 记录服务器启动信息
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();