Swift+Starscream客户端可连WebSocket服务器但无法接收请求
WebSocket客户端无法接收服务器命令排查
我使用Starscream和Swift开发的客户端能连接WebSocket服务器并发送ping,但服务器显示已发送命令请求,客户端却无法接收,最终服务器等待响应超时。
客户端Swift代码
import Foundation import Starscream import UniformTypeIdentifiers class HelperWebSocket: WebSocketDelegate { static let shared = HelperWebSocket() private var serverWebSocketURL: URL { if let jsonServerURLString = SettingsManager.shared.jsonServerURL, let url = URL(string: jsonServerURLString) { print("HelperWebSocket: Server URL fetched from SettingsManager: \(url)") return url } else { print("HelperWebSocket: Server URL not found in SettingsManager, using default URL.") return URL(string: "wss://*******:65500/")! } } private var socket: WebSocket! { didSet { print("HelperWebSocket: WebSocket delegate set.") socket.delegate = self } } private var heartbeatTimer: Timer? private init() { print("HelperWebSocket: Inside init, about to initialize socket.") socket = WebSocket(request: URLRequest(url: serverWebSocketURL), certPinner: FoundationSecurity(allowSelfSigned: false)) // Disable SSL Certificate Validation //socket.disableSSLCertValidation = true print("HelperWebSocket: Socket initialized, about to connect.") socket.connect() print("HelperWebSocket: Socket connection initiated.") startHeartbeat() print("HelperWebSocket: Heartbeat started.") } // MARK: - WebSocketDelegate methods func didReceive(event: WebSocketEvent, client: WebSocketClient) { print("HelperWebSocket: Received WebSocket event: \(event)") switch event { case .connected(let headers): print("HelperWebSocket: Successfully connected to server with headers: \(headers)") print("HelperWebSocket: Heartbeat will keep the connection alive.") case .disconnected(let reason, let code): print("HelperWebSocket: Disconnected from server. Reason: \(reason), Code: \(code)") print("HelperWebSocket: Stopping heartbeat.") stopHeartbeat() case .pong: print("HelperWebSocket: Received PONG from server. Heartbeat acknowledged.") case .text(let text): print("HelperWebSocket: Received text: \(text)") if let command = try? JSONDecoder().decode(ServerCommand.self, from: Data(text.utf8)) { print("HelperWebSocket: Successfully decoded the command: \(command)") handleCommand(command) } else { print("HelperWebSocket: Failed to decode the incoming command.") } default: print("HelperWebSocket: Unhandled WebSocket event.") break } } // MARK: - Command Handling private func handleCommand(_ command: ServerCommand) { switch command.type { case .listDirectory, .upload, .genericCommand: guard let allowedCommand = command.command else { NSLog("HelperWebSocket: Command not provided.") return } helperController.runCommand(allowedCommand) { (result) in DispatchQueue.main.async { switch result { case .success(let reply): let response = ServerResponse(type: .commandOutput, data: reply.standardOutput?.components(separatedBy: "\n")) self.sendResponse(response) case .failure(let error): NSLog("HelperWebSocket: Failed to handle command: \(error)") self.sendErrorResponse(error: error) } } } case .hello: let helloResponse = ServerResponse(type: .commandOutput, data: ["Hello back from the client"]) sendResponse(helloResponse) } } private func sendErrorResponse(error: Error) { let errorResponse = ServerResponse(error: error) sendResponse(errorResponse) } private func sendResponse(_ response: ServerResponse) { do { let responseData = try JSONEncoder().encode(response) if let jsonString = String(data: responseData, encoding: .utf8) { socket.write(string: jsonString) } } catch { NSLog("HelperWebSocket: Failed to encode and send response: \(error)") } } // MARK: - Heartbeat private func startHeartbeat() { DispatchQueue.main.async { [weak self] in print("HelperWebSocket: Starting heartbeat.") self?.heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 120, repeats: true) { [weak self] _ in self?.sendHeartbeat() } } } private func stopHeartbeat() { print("HelperWebSocket: Stopping heartbeat.") heartbeatTimer?.invalidate() heartbeatTimer = nil } private func sendHeartbeat() { print("HelperWebSocket: Sending heartbeat (PING) to server.") socket.write(ping: Data()) } // MARK: - Data Models private struct ServerCommand: Codable { enum CommandType: String, Codable { case listDirectory case upload case genericCommand case hello } var type: CommandType var path: String? var url: URL? var command: AllowedCommand? } private struct ServerResponse: Codable { enum ResponseType: String, Codable { case commandOutput case error } var type: ResponseType var data: [String]? var errorMessage: String? init(type: ResponseType, data: [String]?) { self.type = type self.data = data self.errorMessage = nil } init(error: Error) { self.type = .error self.errorMessage = "\(error)" self.data = nil } } } // MARK: - URL Extension for MIME Type extension URL { func mimeType() -> String { let pathExtension = self.pathExtension if let type = UTType(filenameExtension: pathExtension) { return type.preferredMIMEType ?? "application/octet-stream" } return "application/octet-stream" } }
服务器Node.js代码
const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const path = require('path'); const https = require('https'); const app = express(); const fs = require('fs'); const credentials = { key: fs.readFileSync(path.join(__dirname, 'private.key'), 'utf8'), ca: fs.readFileSync(path.join(__dirname, 'ca_bundle.crt'), 'utf8'), cert: fs.readFileSync(path.join(__dirname, 'certificate.crt'), 'utf8') }; const server = https.createServer(credentials, app); const wss = new WebSocket.Server({ server }); let clients = {}; wss.on('connection', (ws, req) => { const ip = req.socket.remoteAddress; console.log(`[INFO] Client Connected: ${ip}`); clients[ip] = ws; // Send a hello message to the client ws.send(JSON.stringify({ type: "hello", message: "Hello from the server" })); ws.on('message', (message) => { try { const msgObj = JSON.parse(message); switch (msgObj.type) { case 'command': console.log(`[COMMAND] Received command from ${ip}: ${msgObj.commandName}`); break; case 'error': console.error(`[ERROR] Received error from ${ip}: ${msgObj.errorMessage}`); break; default: console.warn(`[WARN] Unknown message type from ${ip}.`); } } catch (err) { console.error(`[ERROR] Failed to parse message from ${ip}: ${message}`); } }); ws.on('close', () => { console.log(`[INFO] Client Disconnected: ${ip}`); delete clients[ip]; }); ws.on('error', (err) => { console.error(`[ERROR] WebSocket error from ${ip}:`, err); }); ws.on('ping', () => { console.log(`[PING] Received ping from ${ip}`); ws.pong(); // send a 'pong' back in response to the 'ping' }); ws.on('pong', () => { console.log(`[PONG] Received pong from ${ip}`); }); }); app.get('/clients', (req, res) => { console.log('[REQUEST] /clients endpoint accessed.'); res.json({ connectedClients: Object.keys(clients) }); }); app.get('/send-command/:ip', (req, res) => { const { ip } = req.params; const { command } = req.query; console.log(`[INFO] /send-command called. Attempting to send command to ${ip}: ${command}`); const client = clients[ip]; if (!client) { console.error(`[ERROR] Client ${ip} not found in connected list.`); return res.status(400).json({ error: 'Client not found' }); } // Send command as JSON const commandObject = { type: 'command', command: command }; // Create a timeout for the response const responseTimeout = setTimeout(() => { client.removeListener('message', messageHandler); console.error(`[ERROR] Response from ${ip} timed out.`); res.status(500).json({ error: 'Response timed out' }); }, 10000); // Handle the client's response const messageHandler = (message) => { console.log(`[INFO] Inside messageHandler, received message: ${message}`); clearTimeout(responseTimeout); client.removeListener('message', messageHandler); try { const response = JSON.parse(message); if (response.type && response.type === 'error') { console.error(`[ERROR] Client ${ip} responded with an error: ${response.errorMessage}`); return res.status(400).json({ error: response.errorMessage }); } console.log(`[INFO] Received valid response from ${ip}: ${message}`); res.json({ result: response }); } catch (err) { console.error(`[ERROR] Failed to parse response from ${ip}: ${message}`); res.status(500).json({ error: 'Failed to parse response' }); } }; client.on('message', messageHandler); client.send(JSON.stringify(commandObject), (err) => { if (err) { console.log(`[ERROR] Inside client.send callback, an error occurred.`); clearTimeout(responseTimeout); client.removeListener('message', messageHandler); console.error(`[ERROR] Failed to send command to ${ip}:`, err); return res.status(500).json({ error: 'Failed to send command' }); } console.log(`[INFO] Command sent successfully to ${ip}. Waiting for response...`); }); }); const PORT = 65500; server.listen(PORT, () => { console.log(`[INFO] Server is running on https://localhost:${PORT}`); });
服务器日志
[INFO] /send-command called. Attempting to send command to ::ffff:*: listContentsOfFolder /Users [INFO] Command sent successfully to ::ffff:*. Waiting for response... [ERROR] Response from ::ffff:* timed out.
问题原因
- 类型枚举不匹配:服务器发送的命令类型是
"command",但客户端ServerCommand.CommandType中没有对应的枚举值,导致JSON解码失败,客户端无法识别命令。 - 命令字段类型不兼容:客户端
ServerCommand的command字段是AllowedCommand?自定义类型,而服务器发送的是字符串格式的命令,即使类型匹配,也会解码失败。 - 无错误反馈:客户端解码失败时仅打印日志,未向服务器返回错误响应,导致服务器等待超时。
修复方案
客户端调整
- 更新枚举类型:在
ServerCommand.CommandType中添加case command,匹配服务器发送的类型:enum CommandType: String, Codable { case listDirectory case upload case genericCommand case hello case command } - 兼容命令字段:将
ServerCommand的command字段暂时改为String?,后续再转换为AllowedCommand,或为AllowedCommand实现自定义解码逻辑:var command: String? - 添加错误反馈:解码失败时向服务器发送错误响应:
case .text(let text): print("HelperWebSocket: Received text: \(text)") if let command = try? JSONDecoder().decode(ServerCommand.self, from: Data(text.utf8)) { print("HelperWebSocket: Successfully decoded the command: \(command)") handleCommand(command) } else { print("HelperWebSocket: Failed to decode the incoming command.") let decodeError = NSError(domain: "WebSocket", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to decode command: \(text)"]) sendErrorResponse(error: decodeError) }
服务器调整(可选)
如果不想修改客户端,可将服务器发送的类型改为客户端已有的genericCommand:
const commandObject = { type: 'genericCommand', command: command };
内容的提问来源于stack exchange,提问作者Samuel Justice
相关产品推荐
相关产品推荐

