- 在DefaultWebSocketController中添加数据库连接检查功能 - 实现文件预览和下载功能及相关API接口 - 更新测试页面支持文件预览和下载操作 - 移除旧的数据库维护子进程机制,改为函数检查 - 在构建请求数据时添加文件字段支持
535 lines
19 KiB
PHP
535 lines
19 KiB
PHP
<?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();
|