import Foundation protocol SignalingClientDelegate: AnyObject { func signalingDidConnect() func signalingDidDisconnect() func signaling(didFail error: String) func signaling(didReceive message: SignalMessage) /// 令牌失效(关闭码 4001):需刷新令牌后重连。 func signalingTokenExpired() /// 强制下线(关闭码 4003):需停止重连并跳登录。 func signalingForceLogout() } /// 信令 WebSocket 客户端(URLSessionWebSocketTask 实现)。 /// /// 鉴权方式:通过 URLRequest 在握手请求头携带 `Authorization: Bearer `。 /// 不再发送 REGISTER —— 连接由服务端根据令牌身份自动完成,并下发 REGISTER_SUCCESS。 final class SignalingClient: NSObject { weak var delegate: SignalingClientDelegate? private let serverUrl: String private let token: String? private var session: URLSession? private var task: URLSessionWebSocketTask? private var manuallyClosed = false /// WebSocket 握手是否已完成(didOpen 后置为 true) private var isOpened = false /// 是否已上报过断开/失败,避免重复回调 private var didNotifyClosure = false /// 心跳定时器(每 25s 发送 PING) private var heartbeatTimer: Timer? init(serverUrl: String, token: String?) { self.serverUrl = serverUrl self.token = token super.init() } func connect() { guard let url = URL(string: serverUrl) else { notifyError("无效的服务器地址: \(serverUrl)") return } manuallyClosed = false isOpened = false didNotifyClosure = false // 握手携带 Bearer token(URLSessionWebSocketTask 不直接支持自定义头,需用 URLRequest)。 var request = URLRequest(url: url) if let token, !token.isEmpty { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 15 session = URLSession(configuration: config, delegate: self, delegateQueue: nil) task = session?.webSocketTask(with: request) // 接收循环在 didOpenWithProtocol(握手完成)后再启动, // 避免在 socket 未真正连接时调用 receive 触发 Code 57。 task?.resume() } func disconnect() { manuallyClosed = true isOpened = false stopHeartbeat() 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 startHeartbeat() { stopHeartbeat() heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 25, repeats: true) { [weak self] _ in guard let self, let task = self.task else { return } task.send(.string("{\"type\":\"PING\"}")) { error in if let error { NSLog("[Signaling] ping error: \(error.localizedDescription)") } } } } private func stopHeartbeat() { heartbeatTimer?.invalidate() heartbeatTimer = nil } 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) } } } /// 处理接收失败:区分"正常断开"与"连接失败"。 private func handleReceiveFailure(_ error: Error) { guard !manuallyClosed, !didNotifyClosure else { return } didNotifyClosure = true let nsError = error as NSError 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 报错。 // 不再发送 REGISTER,服务端根据 Bearer 令牌自动注册。 receiveLoop() startHeartbeat() DispatchQueue.main.async { self.delegate?.signalingDidConnect() } } func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { isOpened = false stopHeartbeat() guard !manuallyClosed, !didNotifyClosure else { return } didNotifyClosure = true let code = Int(closeCode.rawValue) if code == 4001 { DispatchQueue.main.async { self.delegate?.signalingTokenExpired() } return } if code == 4003 { DispatchQueue.main.async { self.delegate?.signalingForceLogout() } return } DispatchQueue.main.async { self.delegate?.signalingDidDisconnect() } } }