Files
VibeCoding/webrtc_controller_ios/web_rtc_controller_ios/Signaling/SignalingClient.swift
tongtongstudio 6eb2c7321a feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑
- WebSocket改用Bearer令牌认证,移除REGISTER请求
- 设备ID改为服务端下发,支持令牌刷新和强制下线处理
- 新增deviceSecret加密存储和accessToken自动刷新
- 更新设备ID获取方式为出厂SN,添加安全存储依赖
2026-08-01 15:13:12 +08:00

190 lines
6.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <accessToken>`
/// 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 tokenURLSessionWebSocketTask 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()
}
}
}