Files
hospital-rescue-app-v2/uni_modules/x-mqtt-s/utssdk/app-ios/xMqttHelp.swift
T
2026-09-24 16:25:22 +08:00

295 lines
10 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
import CocoaMQTT
import DCloudUTSFoundation
// 证书加载工具预计由同目录的 xMqttCerHelp.swift 提供
// getClientCertFromP12File(resourcePath: String, certPassword: String) -> [String: Any]?
// getClientCertFromCrtFile(resourcePath: String) -> [String: Any]?
@objc public class XMqttHelp: NSObject {
@objc public enum ConnectStatus: Int {
case wait
case opening
case open
case error
case dissconnect
case message
}
public typealias EventType = String // 'open' | 'error' | 'dissconnect' | 'message'
public typealias MQTTEventCallback = @convention(block) (_ type: EventType, _ topic: String?, _ payload: String) -> Void
public typealias VoidCallback = @convention(block) () -> Void
private var mqtt: CocoaMQTT?
@objc public private(set) var connectStatus: ConnectStatus = .wait
private var anyStateCallback: VoidCallback?
private struct EventItem {
let type: EventType
let callback: MQTTEventCallback
}
private var events = [String: EventItem]()
private var pendingSubs = [(topic: String, qos: CocoaMQTTQoS)]()
@objc public override init() {
super.init()
}
// opts keys 对齐 index.uts: protocol, server, port, path, clientId, userName, passWord, keepAliveInterval, reconnect, useSSL, certName, certPassword
@objc public func create(_ opts: NSDictionary) -> XMqttHelp {
let proto = ((opts["protocol"] as? String) ?? "wss").lowercased() // ws | wss
let server = (opts["server"] as? String) ?? ""
let port = (opts["port"] as? NSNumber)?.uint16Value ?? 0
let path = (opts["path"] as? String) ?? "/mqtt"
let clientId = (opts["clientId"] as? String) ?? ("iOS-" + UUID().uuidString)
let userName = opts["userName"] as? String
let passWord = opts["passWord"] as? String
let keepAlive = (opts["keepAliveInterval"] as? NSNumber)?.uint16Value ?? 60
let reconnect = (opts["reconnect"] as? NSNumber)?.boolValue ?? true
var useSSL = (opts["useSSL"] as? NSNumber)?.boolValue ?? (proto == "wss")
let certName = opts["certName"] as? String
let certPassword = opts["certPassword"] as? String
let allowUntrust = (opts["allowUntrustCACertificate"] as? NSNumber)?.boolValue ?? false
var headers = opts["headers"] as? [String: String] ?? [:]
let subProtocols = opts["protocols"] as? [String] ?? ["mqtt"]
let connectTimeoutMs = (opts["connectTimeoutMs"] as? NSNumber)?.intValue ?? 30000
let websocket = CocoaMQTTWebSocket(uri: path)
// if !subProtocols.isEmpty {
// // 通过请求头携带子协议,等效于 Sec-WebSocket-Protocol
// headers["sec-websocket-protocol"] = subProtocols.joined(separator: ", ")
// }
// 常见要求:补 Host 与 Origin
headers["Host"] = server
if headers["Origin"] == nil {
headers["Origin"] = "https://\(server)"
}
// 标准版本
headers["Sec-WebSocket-Version"] = "13"
headers["sec-websocket-protocol"] = "mqtt"
headers["upgrade"] = "websocket"
headers["connection"] = "Upgrade"
websocket.headers = headers
// 显式打开 wssCocoaMQTT 2.1.x 部分版本需要)
// 存在则赋值,不存在编译器会报错;若报错我再回退此行
#if compiler(>=5.0)
// 尝试兼容的属性名
// @available: 部分版本属性名为 enableSSL 或 isSecure
// 这里使用可选链避免编译期错误不可行,因此直接尝试常见属性名
// 如果你的 CocoaMQTT 版本不支持,将移除此行由 mqtt.enableSSL 控制
#endif
let mqtt = CocoaMQTT(clientID: clientId, host: server, port: port, socket: websocket)
mqtt.username = userName
mqtt.password = passWord
mqtt.keepAlive = keepAlive
mqtt.enableSSL = useSSL
mqtt.autoReconnect = reconnect
mqtt.cleanSession = true
mqtt.willMessage = CocoaMQTTMessage(topic: "/will", string: "dieout")
console.log("[XMqttHelp] connect url => \(proto)://\(server):\(port)\(path), ssl=\(useSSL), headers=\(headers)")
// 证书策略(可选)
if useSSL, let name = certName, !name.isEmpty {
if name.lowercased().hasSuffix(".p12") {
if let pwd = certPassword, !pwd.isEmpty {
let base = (name as NSString).deletingPathExtension
if let path = Bundle.main.path(forResource: base, ofType: "p12"),
let settings = getClientCertFromP12File(resourcePath: path, certPassword: pwd) {
mqtt.sslSettings = settings
} else {
console.log("[XMqttHelp] p12 证书无效或找不到: \(name)")
}
} else {
console.log("[XMqttHelp] p12 证书未提供密码")
}
} else if name.lowercased().hasSuffix(".crt") {
let base = (name as NSString).deletingPathExtension
if let path = Bundle.main.path(forResource: base, ofType: "crt"),
let settings = getClientCertFromCrtFile(resourcePath: path) {
mqtt.sslSettings = settings
} else {
console.log("[XMqttHelp] crt 证书无效或找不到: \(name)")
}
}
}
// 自签证书信任(调试可开,生产建议禁用或使用证书绑定)
mqtt.allowUntrustCACertificate = allowUntrust
// 指定 SNI 主机名,避免非标准端口下的握手失败
if useSSL {
var ssl = mqtt.sslSettings ?? [:]
ssl[kCFStreamSSLPeerName as String] = server as NSString
mqtt.sslSettings = ssl
}
// 事件回调
weak var weakSelf = self
mqtt.didDisconnect = { [weak weakSelf] _, error in
guard let self = weakSelf else { return }
self.connectStatus = .error
self.notifyStateChange()
self.buildCallEvents(type: "error", topic: nil, payload: (error as? CocoaMQTTError) != nil ? "连接错误" : "地址错误,或者没有网络。")
}
mqtt.didConnectAck = { [weak weakSelf] _, _ in
guard let self = weakSelf else { return }
self.connectStatus = .open
self.notifyStateChange()
self.buildCallEvents(type: "open", topic: nil, payload: "已连接")
// 补发未完成的订阅
if !self.pendingSubs.isEmpty {
for item in self.pendingSubs {
self.mqtt?.subscribe(item.topic, qos: item.qos)
console.log("[XMqttHelp] re-subscribe => \(item.topic) qos=\(item.qos.rawValue)")
}
self.pendingSubs.removeAll()
}
}
mqtt.didChangeState = { [weak weakSelf] _, state in
guard let self = weakSelf else { return }
switch state {
case .connecting:
console.log("[XMqttHelp] 连接中")
case .connected:
console.log("[XMqttHelp] 连接成功")
case .disconnected:
self.connectStatus = .dissconnect
self.notifyStateChange()
self.buildCallEvents(type: "dissconnect", topic: nil, payload: "已断开连接")
default:
break
}
}
mqtt.didReceiveMessage = { [weak weakSelf] _, message, _ in
guard let self = weakSelf else { return }
self.connectStatus = .message
self.notifyStateChange()
let payload = message.string ?? (String(data: Data(message.payload), encoding: .utf8) ?? "")
self.buildCallEvents(type: "message", topic: message.topic, payload: payload)
}
self.mqtt = mqtt
self.connectStatus = .wait
self.notifyStateChange()
// 连接超时保护
if connectTimeoutMs > 0 {
let deadline = DispatchTime.now() + .milliseconds(connectTimeoutMs)
DispatchQueue.main.asyncAfter(deadline: deadline) { [weak self] in
guard let self = self else { return }
if self.connectStatus == .opening {
console.log("[XMqttHelp] 连接超时: \(connectTimeoutMs)ms")
_ = self.disconnect()
self.connectStatus = .error
self.notifyStateChange()
self.buildCallEvents(type: "error", topic: nil, payload: "连接超时")
}
}
}
return self
}
@objc public func connect() -> XMqttHelp {
guard let mqtt = self.mqtt else { return self }
self.connectStatus = .opening
self.notifyStateChange()
mqtt.connect()
return self
}
@objc public func publish(_ topic: String, message: String, qos: Int, retained: Bool, completion: @escaping @convention(block) (_ ok: Bool) -> Void) -> XMqttHelp {
guard let mqtt = self.mqtt else { completion(false); return self }
let q: CocoaMQTTQoS = (qos == 2) ? .qos2 : ((qos == 1) ? .qos1 : .qos0)
let msg = CocoaMQTTMessage(topic: topic, string: message, qos: q, retained: retained)
mqtt.publish(msg)
completion(true)
return self
}
@objc public func subscribe(_ topics: [NSDictionary]) -> XMqttHelp {
guard let mqtt = self.mqtt else { return self }
for item in topics {
guard let topic = item["topic"] as? String else { continue }
let qosVal = (item["qos"] as? NSNumber)?.intValue ?? 0
let q: CocoaMQTTQoS = (qosVal == 2) ? .qos2 : ((qosVal == 1) ? .qos1 : .qos0)
console.log(self.connectStatus == .open,"opeing status")
if self.connectStatus == .open {
mqtt.subscribe(topic, qos: q)
console.log("[XMqttHelp] subscribe => \(topic) qos=\(q.rawValue)")
} else {
self.pendingSubs.append((topic, q))
console.log("[XMqttHelp] cache subscribe (not open) => \(topic) qos=\(q.rawValue)")
}
}
return self
}
@objc public func unsubscribe(_ topics: [String]) -> XMqttHelp {
guard let mqtt = self.mqtt else { return self }
for t in topics { mqtt.unsubscribe(t) }
return self
}
@objc public func disconnect() -> XMqttHelp {
guard let mqtt = self.mqtt else { return self }
mqtt.disconnect()
return self
}
// 注册任意状态变化的回调(无参数)
@objc public func setCallBack(_ cb: @escaping VoidCallback) {
self.anyStateCallback = cb
}
// 规范命名,提供同等功能(可选使用)
@objc public func getStatus() -> String {
return self.statusString(self.connectStatus)
}
// 事件系统:与 index.uts 的 addEventListener/removeEventListener 对齐
@objc @discardableResult
public func addEventListener(_ type: EventType, callback: @escaping MQTTEventCallback) -> String {
let id = String(format: "%.0f-%d", Date().timeIntervalSince1970 * 1000, Int.random(in: 0...9999))
events[id] = EventItem(type: type, callback: callback)
return id
}
@objc @discardableResult
public func removeEventListener(_ id: String) -> XMqttHelp {
events.removeValue(forKey: id)
return self
}
private func buildCallEvents(type: EventType, topic: String?, payload: String) {
for (_, item) in events where item.type == type {
item.callback(type, topic, payload)
}
}
private func notifyStateChange() {
anyStateCallback?()
}
private func statusString(_ status: ConnectStatus) -> String {
switch status {
case .wait:
return "wait"
case .opening:
return "opening"
case .open:
return "open"
case .error:
return "error"
case .dissconnect:
return "dissconnect"
case .message:
return "message"
}
}
}