0, 'info' => 1, 'warn' => 2, 'error' => 3]; public function __construct(array $cfg) { $this->level = $cfg['level'] ?? 'info'; $this->maxSize = $cfg['max_size'] ?? 10485760; $this->logFile = $cfg['file'] ?? __DIR__ . '/logs/signaling.log'; $dir = dirname($this->logFile); if (!is_dir($dir)) { mkdir($dir, 0755, true); } } public function debug(string $msg, array $ctx = []): void { $this->log('debug', $msg, $ctx); } public function info(string $msg, array $ctx = []): void { $this->log('info', $msg, $ctx); } public function warn(string $msg, array $ctx = []): void { $this->log('warn', $msg, $ctx); } public function error(string $msg, array $ctx = []): void { $this->log('error', $msg, $ctx); } private function log(string $level, string $msg, array $ctx): void { if (($this->levelMap[$level] ?? 0) < ($this->levelMap[$this->level] ?? 1)) { return; } if (is_file($this->logFile) && filesize($this->logFile) > $this->maxSize) { $bak = $this->logFile . '.1'; if (is_file($bak)) unlink($bak); rename($this->logFile, $bak); } $ts = date('Y-m-d H:i:s'); $ctxStr = $ctx ? ' ' . json_encode($ctx, JSON_UNESCAPED_UNICODE) : ''; $line = "[{$ts}] [{$level}] {$msg}{$ctxStr}\n"; file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX); } } // ─── 设备管理器 ──────────────────────────────────────────── class DeviceManager { private array $devices = []; private array $connections = []; private array $ipCount = []; private SignalLogger $log; private int $maxPerIp; private int $heartbeatTimeout; public function __construct(SignalLogger $log, int $maxPerIp = 10, int $heartbeatTimeout = 90) { $this->log = $log; $this->maxPerIp = $maxPerIp; $this->heartbeatTimeout = $heartbeatTimeout; } public function register(ConnectionInterface $conn, array $data): array { $deviceId = $data['deviceId'] ?? $data['fingerprint'] ?? ''; if (empty($deviceId)) { return ['success' => false, 'error' => 'deviceId is required']; } $ip = $conn->remoteAddress; $this->ipCount[$ip] = ($this->ipCount[$ip] ?? 0) + 1; if ($this->ipCount[$ip] > $this->maxPerIp) { $this->ipCount[$ip]--; return ['success' => false, 'error' => 'max connections per IP exceeded']; } if (isset($this->connections[$deviceId]) && $this->connections[$deviceId] !== $conn) { $oldConn = $this->connections[$deviceId]; try { $oldConn->close(); } catch (\Throwable $e) {} unset($this->devices[$deviceId], $this->connections[$deviceId]); } $this->devices[$deviceId] = [ 'deviceId' => $deviceId, 'alias' => $data['alias'] ?? 'Unknown', 'deviceModel' => $data['deviceModel'] ?? '', 'deviceType' => $data['deviceType'] ?? 'mobile', 'fingerprint' => $data['fingerprint'] ?? $deviceId, 'userId' => $data['userId'] ?? null, 'protocol' => $data['protocol'] ?? 'xianyan-v1', 'ip' => $ip, 'connectedAt' => time(), 'lastSeen' => time(), 'isOnline' => true, ]; $this->connections[$deviceId] = $conn; $conn->deviceId = $deviceId; $this->log->info("Device registered: {$deviceId}", [ 'alias' => $data['alias'] ?? 'Unknown', 'type' => $data['deviceType'] ?? 'mobile', 'ip' => $ip, ]); $onlineDevices = $this->getOnlineDevices($deviceId); return ['success' => true, 'deviceId' => $deviceId, 'onlineDevices' => $onlineDevices]; } public function unregister(ConnectionInterface $conn): void { $deviceId = $conn->deviceId ?? null; if ($deviceId && isset($this->devices[$deviceId])) { $ip = $this->devices[$deviceId]['ip'] ?? ''; if ($ip && isset($this->ipCount[$ip])) { $this->ipCount[$ip]--; if ($this->ipCount[$ip] <= 0) unset($this->ipCount[$ip]); } $this->log->info("Device unregistered: {$deviceId}", [ 'alias' => $this->devices[$deviceId]['alias'] ?? '', ]); unset($this->devices[$deviceId], $this->connections[$deviceId]); } } public function heartbeat(string $deviceId): void { if (isset($this->devices[$deviceId])) { $this->devices[$deviceId]['lastSeen'] = time(); } } public function getOnlineDevices(string $excludeId = ''): array { $now = time(); $result = []; foreach ($this->devices as $id => $dev) { if ($id === $excludeId) continue; if ($now - $dev['lastSeen'] > $this->heartbeatTimeout) { $dev['isOnline'] = false; continue; } $result[] = [ 'deviceId' => $dev['deviceId'], 'alias' => $dev['alias'], 'deviceModel' => $dev['deviceModel'], 'deviceType' => $dev['deviceType'], 'isOnline' => true, 'isPaired' => false, 'protocol' => $dev['protocol'], 'lastSeen' => $dev['lastSeen'] * 1000, ]; } return $result; } public function getConnection(string $deviceId): ?ConnectionInterface { return $this->connections[$deviceId] ?? null; } public function getDevice(string $deviceId): ?array { return $this->devices[$deviceId] ?? null; } public function getDeviceByConn(ConnectionInterface $conn): ?array { $deviceId = $conn->deviceId ?? null; return $deviceId ? ($this->devices[$deviceId] ?? null) : null; } public function cleanupStale(): int { $now = time(); $cleaned = 0; foreach ($this->devices as $id => $dev) { if ($now - $dev['lastSeen'] > $this->heartbeatTimeout) { $conn = $this->connections[$id] ?? null; if ($conn) { try { $conn->close(); } catch (\Throwable $e) {} } unset($this->devices[$id], $this->connections[$id]); $cleaned++; } } return $cleaned; } public function getStats(): array { return [ 'totalDevices' => count($this->devices), 'totalConns' => count($this->connections), 'uniqueIps' => count($this->ipCount), ]; } } // ─── 配对记录管理 ────────────────────────────────────────── class PairingManager { private string $dataFile; private array $records = []; private array $pendingRequests = []; private SignalLogger $log; private int $requestTtl; public function __construct(SignalLogger $log, string $dataFile, int $requestTtl = 300) { $this->log = $log; $this->dataFile = $dataFile; $this->requestTtl = $requestTtl; $this->loadRecords(); } private function loadRecords(): void { $dir = dirname($this->dataFile); if (!is_dir($dir)) mkdir($dir, 0755, true); if (is_file($this->dataFile)) { $data = json_decode(file_get_contents($this->dataFile), true); if (is_array($data)) { $this->records = $data; } } } private function saveRecords(): void { file_put_contents( $this->dataFile, json_encode($this->records, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX ); } public function createRequest(string $fromId, string $toId, string $fingerprint): array { $this->cleanupExpired(); $requestId = bin2hex(random_bytes(16)); $this->pendingRequests[$requestId] = [ 'id' => $requestId, 'fromId' => $fromId, 'toId' => $toId, 'fingerprint' => $fingerprint, 'createdAt' => time(), 'expiresAt' => time() + $this->requestTtl, ]; $this->log->info("Pair request created: {$fromId} -> {$toId}"); return $this->pendingRequests[$requestId]; } public function acceptRequest(string $requestId, string $fromId, string $toId): array { if (!isset($this->pendingRequests[$requestId])) { return ['success' => false, 'error' => 'request not found or expired']; } $req = $this->pendingRequests[$requestId]; unset($this->pendingRequests[$requestId]); $pairId1 = $fromId . ':' . $toId; $pairId2 = $toId . ':' . $fromId; $this->records[$pairId1] = [ 'id' => $pairId1, 'deviceId' => $fromId, 'peerDeviceId' => $toId, 'peerFingerprint' => $req['fingerprint'], 'pairedAt' => time(), 'lastVerifiedAt' => time(), 'isTrusted' => true, ]; $this->records[$pairId2] = [ 'id' => $pairId2, 'deviceId' => $toId, 'peerDeviceId' => $fromId, 'peerFingerprint' => $req['fingerprint'], 'pairedAt' => time(), 'lastVerifiedAt' => time(), 'isTrusted' => true, ]; $this->saveRecords(); $this->log->info("Pair accepted: {$fromId} <-> {$toId}"); return ['success' => true]; } public function rejectRequest(string $requestId): array { if (isset($this->pendingRequests[$requestId])) { unset($this->pendingRequests[$requestId]); $this->log->info("Pair rejected: {$requestId}"); } return ['success' => true]; } public function getPairedDevices(string $deviceId): array { $result = []; foreach ($this->records as $key => $rec) { if ($rec['deviceId'] === $deviceId) { $result[] = $rec; } } return $result; } public function removePairing(string $deviceId, string $peerDeviceId): array { $pairId1 = $deviceId . ':' . $peerDeviceId; $pairId2 = $peerDeviceId . ':' . $deviceId; unset($this->records[$pairId1], $this->records[$pairId2]); $this->saveRecords(); $this->log->info("Pair removed: {$deviceId} <-> {$peerDeviceId}"); return ['success' => true]; } public function isPaired(string $deviceId, string $peerDeviceId): bool { $pairId = $deviceId . ':' . $peerDeviceId; return isset($this->records[$pairId]); } private function cleanupExpired(): void { $now = time(); foreach ($this->pendingRequests as $id => $req) { if ($now > $req['expiresAt']) { unset($this->pendingRequests[$id]); } } } } // ─── 信令服务核心 ────────────────────────────────────────── class SignalingServer implements MessageComponentInterface { private DeviceManager $deviceManager; private PairingManager $pairingManager; private SignalLogger $log; private array $config; private int $startTime; public function __construct(array $config) { $this->config = $config; $this->log = new SignalLogger($config['log'] ?? []); $this->deviceManager = new DeviceManager( $this->log, $config['signaling']['max_devices_per_ip'] ?? 10, $config['signaling']['heartbeat_timeout'] ?? 90 ); $this->pairingManager = new PairingManager( $this->log, $config['pairing']['data_file'] ?? __DIR__ . '/data/pairing_records.json', $config['pairing']['request_ttl'] ?? 300 ); $this->startTime = time(); $this->log->info("SignalingServer initialized"); } public function onOpen(ConnectionInterface $conn): void { $conn->deviceId = null; $this->log->debug("Connection opened: {$conn->remoteAddress}"); } public function onMessage(ConnectionInterface $from, $msg): void { $data = json_decode($msg, true); if (!is_array($data)) { $this->sendError($from, 'invalid JSON'); return; } $type = $data['type'] ?? ''; $this->log->debug("Message received: type={$type}", ['from' => $from->deviceId ?? 'unknown']); switch ($type) { case 'register': $this->handleRegister($from, $data); break; case 'discover': $this->handleDiscover($from); break; case 'discoverMyDevices': $this->handleDiscoverMyDevices($from, $data); break; case 'offer': $this->handleRelay($from, $data, 'offer'); break; case 'answer': $this->handleRelay($from, $data, 'answer'); break; case 'iceCandidate': case 'ice-candidate': $this->handleRelay($from, $data, 'iceCandidate'); break; case 'textMessage': case 'text-message': $this->handleTextMessage($from, $data); break; case 'fileMeta': case 'file-meta': $this->handleRelay($from, $data, 'fileMeta'); break; case 'fileChunk': case 'file-chunk': $this->handleRelay($from, $data, 'fileChunk'); break; case 'fileComplete': case 'file-complete': $this->handleRelay($from, $data, 'fileComplete'); break; case 'progress': $this->handleRelay($from, $data, 'progress'); break; case 'transportNegotiate': case 'transportNegotiate': $this->handleRelay($from, $data, 'transportNegotiate'); break; case 'transportNegotiateResponse': case 'transport-negotiate-response': $this->handleRelay($from, $data, 'transportNegotiateResponse'); break; case 'wsRelay': $this->handleRelay($from, $data, 'wsRelay'); break; case 'pairRequest': case 'pair-request': $this->handlePairRequest($from, $data); break; case 'pairAccept': case 'pair-accept': $this->handlePairAccept($from, $data); break; case 'pairReject': case 'pair-reject': $this->handlePairReject($from, $data); break; case 'heartbeat': $this->handleHeartbeat($from); break; case 'leave': $this->handleLeave($from); break; case 'ping': $this->send($from, ['type' => 'pong', 'ts' => time() * 1000]); break; case 'delivery-ack': $this->handleRelay($from, $data, 'delivery-ack'); break; case 'chunk-ack': $this->handleRelay($from, $data, 'chunk-ack'); break; case 'resume-request': $this->handleRelay($from, $data, 'resume-request'); break; case 'voice-meta': $this->handleRelay($from, $data, 'voice-meta'); break; case 'cloud-cache-notify': $this->handleRelay($from, $data, 'cloud-cache-notify'); break; case 'canvas-stroke': $this->handleRelay($from, $data, 'canvas-stroke'); break; case 'canvas-cursor': $this->handleRelay($from, $data, 'canvas-cursor'); break; case 'canvas-join': $this->handleCanvasJoin($from, $data); break; case 'canvas-leave': $this->handleCanvasLeave($from, $data); break; case 'canvas-snapshot': $this->handleRelay($from, $data, 'canvas-snapshot'); break; case 'screen-share-offer': $this->handleRelay($from, $data, 'screen-share-offer'); break; case 'screen-share-answer': $this->handleRelay($from, $data, 'screen-share-answer'); break; case 'screen-share-stop': $this->handleRelay($from, $data, 'screen-share-stop'); break; case 'remote-input': $this->handleRelay($from, $data, 'remote-input'); break; default: $this->sendError($from, "unknown type: {$type}"); } } public function onClose(ConnectionInterface $conn): void { $deviceId = $conn->deviceId ?? null; if ($deviceId) { $this->broadcastDeviceLeave($deviceId); } $this->deviceManager->unregister($conn); $this->log->info("Connection closed: {$conn->remoteAddress}", ['deviceId' => $deviceId]); } public function onError(ConnectionInterface $conn, \Exception $e): void { $this->log->error("Connection error: " . $e->getMessage(), [ 'deviceId' => $conn->deviceId ?? 'unknown', ]); $conn->close(); } // ─── 消息处理方法 ───────────────────────────────────── private function handleRegister(ConnectionInterface $conn, array $data): void { $payload = $data['data'] ?? $data; $result = $this->deviceManager->register($conn, $payload); if (!$result['success']) { $this->sendError($conn, $result['error']); return; } $this->send($conn, [ 'type' => 'registered', 'data' => [ 'deviceId' => $result['deviceId'], 'onlineDevices' => $result['onlineDevices'], 'protocol' => $payload['protocol'] ?? 'xianyan-v1', 'serverTime' => time() * 1000, ], ]); $this->broadcastDeviceOnline($result['deviceId']); } private function handleDiscover(ConnectionInterface $conn): void { $deviceId = $conn->deviceId ?? null; if (!$deviceId) { $this->sendError($conn, 'not registered'); return; } $devices = $this->deviceManager->getOnlineDevices($deviceId); $pairedDevices = $this->pairingManager->getPairedDevices($deviceId); foreach ($devices as &$dev) { $dev['isPaired'] = $this->pairingManager->isPaired($deviceId, $dev['deviceId']); } unset($dev); $this->send($conn, [ 'type' => 'discover_response', 'data' => [ 'devices' => $devices, 'pairedDevices' => $pairedDevices, ], ]); } private function handleRelay(ConnectionInterface $from, array $data, string $type): void { $fromId = $from->deviceId ?? null; $targetId = $data['targetId'] ?? $data['to'] ?? null; if (!$fromId) { $this->sendError($from, 'not registered'); return; } if (!$targetId) { $this->sendError($from, 'targetId is required'); return; } $targetConn = $this->deviceManager->getConnection($targetId); if (!$targetConn) { $this->sendError($from, "target device not found: {$targetId}"); return; } $this->send($targetConn, [ 'type' => $type, 'from' => $fromId, 'data' => $data['data'] ?? $data['payload'] ?? [], ]); $this->log->debug("Relay {$type}: {$fromId} -> {$targetId}"); } private function handleTextMessage(ConnectionInterface $from, array $data): void { $fromId = $from->deviceId ?? null; $targetId = $data['targetId'] ?? $data['to'] ?? null; if (!$fromId || !$targetId) { $this->sendError($from, 'fromId and targetId required'); return; } $targetConn = $this->deviceManager->getConnection($targetId); if (!$targetConn) { $this->sendError($from, "target device not found: {$targetId}"); return; } $this->send($targetConn, [ 'type' => 'textMessage', 'from' => $fromId, 'data' => [ 'text' => $data['data']['text'] ?? '', 'envelope' => $data['data']['envelope'] ?? null, 'timestamp' => time() * 1000, ], ]); $this->send($from, [ 'type' => 'messageSent', 'data' => ['targetId' => $targetId, 'timestamp' => time() * 1000], ]); $this->log->info("Text message: {$fromId} -> {$targetId}"); } private function handlePairRequest(ConnectionInterface $from, array $data): void { $fromId = $from->deviceId ?? null; $targetId = $data['targetId'] ?? $data['to'] ?? null; $fingerprint = $data['data']['fingerprint'] ?? ''; if (!$fromId || !$targetId) { $this->sendError($from, 'fromId and targetId required'); return; } $request = $this->pairingManager->createRequest($fromId, $targetId, $fingerprint); $targetConn = $this->deviceManager->getConnection($targetId); if ($targetConn) { $fromDevice = $this->deviceManager->getDevice($fromId); $this->send($targetConn, [ 'type' => 'pairRequest', 'from' => $fromId, 'data' => [ 'requestId' => $request['id'], 'fingerprint' => $fingerprint, 'alias' => $fromDevice['alias'] ?? 'Unknown', 'deviceModel' => $fromDevice['deviceModel'] ?? '', 'deviceType' => $fromDevice['deviceType'] ?? 'mobile', 'expiresAt' => $request['expiresAt'] * 1000, ], ]); } $this->send($from, [ 'type' => 'pairRequestSent', 'data' => ['requestId' => $request['id'], 'targetId' => $targetId], ]); } private function handlePairAccept(ConnectionInterface $from, array $data): void { $fromId = $from->deviceId ?? null; $requestId = $data['data']['requestId'] ?? ''; $targetId = $data['targetId'] ?? $data['to'] ?? ''; if (!$fromId || !$requestId) { $this->sendError($from, 'fromId and requestId required'); return; } $result = $this->pairingManager->acceptRequest($requestId, $fromId, $targetId); $targetConn = $this->deviceManager->getConnection($targetId); if ($targetConn) { $this->send($targetConn, [ 'type' => 'pairAccepted', 'from' => $fromId, 'data' => ['requestId' => $requestId], ]); } $this->send($from, [ 'type' => 'pairAcceptConfirmed', 'data' => ['requestId' => $requestId, 'success' => $result['success']], ]); } private function handlePairReject(ConnectionInterface $from, array $data): void { $fromId = $from->deviceId ?? null; $requestId = $data['data']['requestId'] ?? ''; $targetId = $data['targetId'] ?? $data['to'] ?? ''; $this->pairingManager->rejectRequest($requestId); $targetConn = $this->deviceManager->getConnection($targetId); if ($targetConn) { $this->send($targetConn, [ 'type' => 'pairRejected', 'from' => $fromId, 'data' => ['requestId' => $requestId], ]); } $this->send($from, [ 'type' => 'pairRejectConfirmed', 'data' => ['requestId' => $requestId], ]); } private function handleHeartbeat(ConnectionInterface $from): void { $deviceId = $from->deviceId ?? null; if ($deviceId) { $this->deviceManager->heartbeat($deviceId); } $this->send($from, [ 'type' => 'heartbeat_ack', 'data' => ['timestamp' => time() * 1000], ]); } private function handleLeave(ConnectionInterface $from): void { $deviceId = $from->deviceId ?? null; if ($deviceId) { $this->broadcastDeviceLeave($deviceId); } } private function handleDiscoverMyDevices(ConnectionInterface $from, array $data): void { $deviceId = $from->deviceId ?? null; if (!$deviceId) { $this->sendError($from, 'not registered'); return; } $userId = $data['data']['userId'] ?? $data['userId'] ?? null; if (!$userId) { $this->sendError($from, 'userId is required'); return; } $myDevices = []; foreach ($this->deviceManager->getOnlineDevices() as $dev) { $devInfo = $this->deviceManager->getDevice($dev['deviceId']); if ($devInfo && ($devInfo['userId'] ?? null) === $userId) { $myDevices[] = $dev; } } $this->send($from, [ 'type' => 'myDevicesResponse', 'data' => ['devices' => $myDevices], ]); $this->log->info("discoverMyDevices: userId={$userId}, found=" . count($myDevices)); } private function handleCanvasJoin(ConnectionInterface $from, array $data): void { $fromId = $from->deviceId ?? null; $targetId = $data['targetId'] ?? $data['to'] ?? null; $canvasId = $data['data']['canvasId'] ?? $data['canvasId'] ?? ''; if (!$fromId || !$targetId) { $this->sendError($from, 'fromId and targetId required'); return; } $targetConn = $this->deviceManager->getConnection($targetId); if ($targetConn) { $this->send($targetConn, [ 'type' => 'canvas-join', 'from' => $fromId, 'data' => [ 'canvasId' => $canvasId, 'alias' => $this->deviceManager->getDevice($fromId)['alias'] ?? 'Unknown', ], ]); } $this->log->info("canvas-join: {$fromId} -> canvas={$canvasId}"); } private function handleCanvasLeave(ConnectionInterface $from, array $data): void { $fromId = $from->deviceId ?? null; $targetId = $data['targetId'] ?? $data['to'] ?? null; $canvasId = $data['data']['canvasId'] ?? $data['canvasId'] ?? ''; if (!$fromId || !$targetId) { $this->sendError($from, 'fromId and targetId required'); return; } $targetConn = $this->deviceManager->getConnection($targetId); if ($targetConn) { $this->send($targetConn, [ 'type' => 'canvas-leave', 'from' => $fromId, 'data' => ['canvasId' => $canvasId], ]); } $this->log->info("canvas-leave: {$fromId} -> canvas={$canvasId}"); } // ─── 广播方法 ───────────────────────────────────────── private function broadcastDeviceOnline(string $deviceId): void { $device = $this->deviceManager->getDevice($deviceId); if (!$device) return; $msg = json_encode([ 'type' => 'deviceOnline', 'data' => [ 'deviceId' => $deviceId, 'alias' => $device['alias'], 'deviceModel' => $device['deviceModel'], 'deviceType' => $device['deviceType'], 'protocol' => $device['protocol'], ], ], JSON_UNESCAPED_UNICODE); foreach ($this->deviceManager->getOnlineDevices($deviceId) as $dev) { $conn = $this->deviceManager->getConnection($dev['deviceId']); if ($conn) { try { $conn->send($msg); } catch (\Throwable $e) {} } } } private function broadcastDeviceLeave(string $deviceId): void { $device = $this->deviceManager->getDevice($deviceId); if (!$device) return; $msg = json_encode([ 'type' => 'deviceOffline', 'data' => ['deviceId' => $deviceId, 'alias' => $device['alias']], ], JSON_UNESCAPED_UNICODE); foreach ($this->deviceManager->getOnlineDevices() as $dev) { $conn = $this->deviceManager->getConnection($dev['deviceId']); if ($conn) { try { $conn->send($msg); } catch (\Throwable $e) {} } } } // ─── 工具方法 ───────────────────────────────────────── private function send(ConnectionInterface $conn, array $data): void { try { $conn->send(json_encode($data, JSON_UNESCAPED_UNICODE)); } catch (\Throwable $e) { $this->log->error("Send failed: " . $e->getMessage()); } } private function sendError(ConnectionInterface $conn, string $error): void { $this->send($conn, ['type' => 'error', 'data' => ['message' => $error]]); } public function getDeviceManager(): DeviceManager { return $this->deviceManager; } public function getPairingManager(): PairingManager { return $this->pairingManager; } public function getStats(): array { return array_merge($this->deviceManager->getStats(), [ 'uptime' => time() - $this->startTime, 'startTime' => $this->startTime, ]); } } // ─── HTTP健康检查端点 ────────────────────────────────────── class HealthCheckHandler implements \Ratchet\Http\HttpServerInterface { private SignalingServer $signaling; public function __construct(SignalingServer $signaling) { $this->signaling = $signaling; } public function onOpen(ConnectionInterface $conn, \Psr\Http\Message\RequestInterface $request = null): void { $path = $request ? $request->getUri()->getPath() : '/'; switch ($path) { case '/health': $stats = $this->signaling->getStats(); $body = json_encode([ 'status' => 'ok', 'version' => '1.0.0', 'stats' => $stats, 'time' => date('c'), ]); break; case '/stats': $body = json_encode($this->signaling->getStats()); break; default: $body = json_encode(['error' => 'not found']); break; } $response = "HTTP/1.1 200 OK\r\n" . "Content-Type: application/json; charset=utf-8\r\n" . "Access-Control-Allow-Origin: *\r\n" . "Content-Length: " . strlen($body) . "\r\n" . "Connection: close\r\n" . "\r\n" . $body; $conn->send($response); $conn->close(); } public function onMessage(ConnectionInterface $from, $msg): void {} public function onClose(ConnectionInterface $conn): void {} public function onError(ConnectionInterface $conn, \Exception $e): void { $conn->close(); } } // ─── 路由分发 ────────────────────────────────────────────── class RouterHandler implements \Ratchet\Http\HttpServerInterface { private SignalingServer $signaling; private HealthCheckHandler $healthCheck; private WsServer $wsServer; public function __construct(SignalingServer $signaling, WsServer $wsServer) { $this->signaling = $signaling; $this->healthCheck = new HealthCheckHandler($signaling); $this->wsServer = $wsServer; } public function onOpen(ConnectionInterface $conn, \Psr\Http\Message\RequestInterface $request = null): void { $path = $request ? $request->getUri()->getPath() : '/'; if ($path === '/health' || $path === '/stats') { $this->healthCheck->onOpen($conn, $request); } else { $this->wsServer->onOpen($conn, $request); } } public function onMessage(ConnectionInterface $from, $msg): void { $this->wsServer->onMessage($from, $msg); } public function onClose(ConnectionInterface $conn): void { $this->wsServer->onClose($conn); } public function onError(ConnectionInterface $conn, \Exception $e): void { $this->wsServer->onError($conn, $e); } } // ─── 启动服务 ────────────────────────────────────────────── $logger = new SignalLogger($config['log'] ?? []); $signalingServer = new SignalingServer($config); $wsServer = new WsServer($signalingServer); $router = new RouterHandler($signalingServer, $wsServer); $httpServer = new HttpServer($router); $port = $config['signaling']['port'] ?? 9443; $host = $config['signaling']['host'] ?? '0.0.0.0'; $logger->info("Starting XianYan Signaling Server on {$host}:{$port}"); $logger->info("Supported protocols: " . implode(', ', $config['protocol']['supported'] ?? ['xianyan-v1'])); $server = IoServer::factory($httpServer, $port, $host); $server->loop->addPeriodicTimer(30, function () use ($signalingServer, $logger) { $cleaned = $signalingServer->getDeviceManager()->cleanupStale(); if ($cleaned > 0) { $logger->info("Cleaned up {$cleaned} stale connections"); } }); $server->loop->addPeriodicTimer(60, function () use ($signalingServer, $logger) { $stats = $signalingServer->getStats(); $logger->info("Stats: " . json_encode($stats)); }); echo "╔══════════════════════════════════════════════╗\n"; echo "║ 闲言APP 文件传输信令服务 v1.0.0 ║\n"; echo "║ WebSocket: ws://{$host}:{$port} ║\n"; echo "║ Health: http://{$host}:{$port}/health ║\n"; echo "║ Protocols: xianyan-v1, localsend-v2 ║\n"; echo "╚══════════════════════════════════════════════╝\n"; $server->run();