import Foundation protocol SignalingClientDelegate: AnyObject { func signalingDidConnect() func signalingDidDisconnect() func signaling(didFail error: String) func signaling(didReceive message: SignalMessage) } /// 信令 WebSocket 客户端(URLSessionWebSocketTask 实现)。 /// 连接成功后自动发送 REGISTER(deviceType = CONTROLLER), /// 所有回调均已切换到主线程。 final class SignalingClient: NSObject { weak var delegate: SignalingClientDelegate? private let serverUrl: String private let deviceId: String private var session: URLSession? private var task: URLSessionWebSocketTask? private var manuallyClosed = false /// WebSocket 握手是否已完成(didOpen 后置为 true) private var isOpened = false /// 是否已上报过断开/失败,避免重复回调 private var didNotifyClosure = false init(serverUrl: String, deviceId: String) { self.serverUrl = serverUrl self.deviceId = deviceId super.init() } func connect() { guard let url = URL(string: serverUrl) else { notifyError("无效的服务器地址: \(serverUrl)") return } manuallyClosed = false isOpened = false didNotifyClosure = false let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 15 session = URLSession(configuration: config, delegate: self, delegateQueue: nil) task = session?.webSocketTask(with: url) // 接收循环在 didOpenWithProtocol(握手完成)后再启动, // 避免在 socket 未真正连接时调用 receive 触发 Code 57。 task?.resume() } func disconnect() { manuallyClosed = true isOpened = false task?.cancel(with: .normalClosure, reason: "Disconnecting".data(using: .utf8)) task = nil session?.invalidateAndCancel() session = nil } func send(_ message: SignalMessage) { guard let task else { return } guard let data = try? JSONEncoder().encode(message), let json = String(data: data, encoding: .utf8) else { return } task.send(.string(json)) { error in if let error { NSLog("[Signaling] send error: \(error.localizedDescription)") } } } var isConnected: Bool { task != nil } // MARK: - Private private func registerDevice() { var msg = SignalMessage() msg.type = "REGISTER" msg.fromDeviceId = deviceId msg.deviceType = "CONTROLLER" send(msg) } private func receiveLoop() { task?.receive { [weak self] result in guard let self else { return } switch result { case .success(let wsMessage): if case .string(let text) = wsMessage, let data = text.data(using: .utf8), let message = try? JSONDecoder().decode(SignalMessage.self, from: data) { DispatchQueue.main.async { self.delegate?.signaling(didReceive: message) } } self.receiveLoop() case .failure(let error): self.handleReceiveFailure(error) } } } /// 处理接收失败:区分"正常断开"与"连接失败"。 /// Socket 已断开(Code 57 等)视为断开而非致命错误。 private func handleReceiveFailure(_ error: Error) { guard !manuallyClosed, !didNotifyClosure else { return } didNotifyClosure = true let nsError = error as NSError // NSPOSIXErrorDomain Code 57: Socket is not connected(连接已断开) let isDisconnect = (nsError.domain == NSPOSIXErrorDomain && nsError.code == 57) || (nsError.domain == NSURLErrorDomain && (nsError.code == NSURLErrorNetworkConnectionLost || nsError.code == NSURLErrorCancelled)) DispatchQueue.main.async { if self.isOpened || isDisconnect { // 连接已建立过后再断开,按"断开"处理 self.delegate?.signalingDidDisconnect() } else { // 从未成功建立连接,按"连接失败"处理 self.delegate?.signaling(didFail: error.localizedDescription) } } } private func notifyError(_ text: String) { DispatchQueue.main.async { self.delegate?.signaling(didFail: text) } } } // MARK: - URLSessionWebSocketDelegate extension SignalingClient: URLSessionWebSocketDelegate { func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { isOpened = true // 握手完成后再启动接收循环,避免 socket 未连接时 receive 报错 receiveLoop() registerDevice() DispatchQueue.main.async { self.delegate?.signalingDidConnect() } } func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { isOpened = false guard !manuallyClosed, !didNotifyClosure else { return } didNotifyClosure = true DispatchQueue.main.async { self.delegate?.signalingDidDisconnect() } } }