// // CocoaMQTT.swift // CocoaMQTT // // Created by Feng Lee on 14/8/3. // Copyright (c) 2015 emqx.io. All rights reserved. // import Foundation import MqttCocoaAsyncSocket /** * Conn Ack */ @objc public enum CocoaMQTTConnAck: UInt8, CustomStringConvertible { case accept = 0 case unacceptableProtocolVersion case identifierRejected case serverUnavailable case badUsernameOrPassword case notAuthorized case reserved public init(byte: UInt8) { switch byte { case CocoaMQTTConnAck.accept.rawValue.. Void) @objc optional func mqttUrlSession(_ mqtt: CocoaMQTT, didReceiveTrust trust: SecTrust, didReceiveChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) /// @objc optional func mqtt(_ mqtt: CocoaMQTT, didPublishComplete id: UInt16) /// @objc optional func mqtt(_ mqtt: CocoaMQTT, didStateChangeTo state: CocoaMQTTConnState) /// Called when auto-reconnect schedules a reconnect attempt after an unexpected disconnect. @objc optional func mqtt(_ mqtt: CocoaMQTT, didScheduleReconnect attemptCount: UInt, after interval: UInt16) } /// set mqtt version to 3.1.1 public func setMqtt3Version() { if let storage = CocoaMQTTStorage() { storage.setMQTTVersion("3.1.1") } } /** * Blueprint of the MQTT Client */ protocol CocoaMQTTClient { /* Basic Properties */ var host: String { get set } var port: UInt16 { get set } var clientID: String { get } var username: String? {get set} var password: String? {get set} var cleanSession: Bool {get set} var keepAlive: UInt16 {get set} var willMessage: CocoaMQTTMessage? {get set} /* Basic Properties */ /* CONNNEC/DISCONNECT */ func connect() -> Bool func connect(timeout: TimeInterval) -> Bool func disconnect() func ping() /* CONNNEC/DISCONNECT */ /* PUBLISH/SUBSCRIBE */ func subscribe(_ topic: String, qos: CocoaMQTTQoS) func subscribe(_ topics: [(String, CocoaMQTTQoS)]) func unsubscribe(_ topic: String) func unsubscribe(_ topics: [String]) func publish(_ topic: String, withString string: String, qos: CocoaMQTTQoS, retained: Bool) -> Int func publish(_ message: CocoaMQTTMessage) -> Int /* PUBLISH/SUBSCRIBE */ } /// MQTT Client /// /// - Note: MGCDAsyncSocket need delegate to extend NSObject public class CocoaMQTT: NSObject, CocoaMQTTClient { public weak var delegate: CocoaMQTTDelegate? private var version = "3.1.1" public var host = "localhost" public var port: UInt16 = 1883 public var clientID: String public var username: String? public var password: String? /// Clean Session flag. Default is true /// /// - TODO: What's behavior each Clean Session flags??? public var cleanSession = true /// Setup a **Last Will Message** to client before connecting to broker public var willMessage: CocoaMQTTMessage? /// Enable backgounding socket if running on iOS platform. Default is true /// /// - Note: public var backgroundOnSocket: Bool { get { return (self.socket as? CocoaMQTTSocket)?.backgroundOnSocket ?? true } set { (self.socket as? CocoaMQTTSocket)?.backgroundOnSocket = newValue } } /// Delegate Executed queue. Default is `DispatchQueue.main` /// /// The delegate/closure callback function will be committed asynchronously to it public var delegateQueue = DispatchQueue.main public var connState = CocoaMQTTConnState.disconnected { didSet { __delegate_queue { self.delegate?.mqtt?(self, didStateChangeTo: self.connState) self.didChangeState(self, self.connState) } } } // deliver private var deliver = CocoaMQTTDeliver() /// Re-deliver the un-acked messages public var deliverTimeout: Double { get { return deliver.retryTimeInterval } set { deliver.retryTimeInterval = newValue } } /// Message queue size. default 1000 /// /// The new publishing messages of Qos1/Qos2 will be drop, if the queue is full public var messageQueueSize: UInt { get { return deliver.mqueueSize } set { deliver.mqueueSize = newValue } } /// In-flight window size. default 10 public var inflightWindowSize: UInt { get { return deliver.inflightWindowSize } set { deliver.inflightWindowSize = newValue } } /// Keep alive time interval public var keepAlive: UInt16 = 60 private var aliveTimer: CocoaMQTTTimer? /// Enable auto-reconnect mechanism public var autoReconnect = false /// Reconnect time interval /// /// - note: This value will be increased with `autoReconnectTimeInterval *= 2` /// if reconnect failed public var autoReconnectTimeInterval: UInt16 = 1 // starts from 1 second /// Maximum auto reconnect time interval /// /// The timer starts from `autoReconnectTimeInterval` second and grows exponentially until this value /// After that, it uses this value for subsequent requests. public var maxAutoReconnectTimeInterval: UInt16 = 128 // 128 seconds /// Auto-reconnect backoff interval in seconds for the current reconnect cycle. /// /// This value is advanced for the next reconnect attempt while auto-reconnect is active, /// and resets to `0` when auto-reconnect is inactive. public private(set) var reconnectTimeInterval: UInt16 = 0 /// Number of reconnect attempts scheduled in the current auto-reconnect cycle. /// /// The value resets to `0` after a successful connection or expected disconnect. public private(set) var reconnectAttemptCount: UInt = 0 private var autoReconnTimer: CocoaMQTTTimer? private var is_internal_disconnected = false /// Console log level public var logLevel: CocoaMQTTLoggerLevel { get { return CocoaMQTTLogger.logger.minLevel } set { CocoaMQTTLogger.logger.minLevel = newValue } } /// Enable SSL connection public var enableSSL: Bool { get { return self.socket.enableSSL } set { socket.enableSSL = newValue } } /// public var sslSettings: [String: NSObject]? { get { return (self.socket as? CocoaMQTTSocket)?.sslSettings ?? nil } set { (self.socket as? CocoaMQTTSocket)?.sslSettings = newValue } } /// Allow self-signed ca certificate. /// /// Default is false public var allowUntrustCACertificate: Bool { get { return (self.socket as? CocoaMQTTSocket)?.allowUntrustCACertificate ?? false } set { (self.socket as? CocoaMQTTSocket)?.allowUntrustCACertificate = newValue } } /// The subscribed topics in current communication /// /// Keeping this dictionary-typed preserves the public API while the backing store remains thread-safe. public var subscriptions: [String: CocoaMQTTQoS] { get { subscriptionsStorage.snapshot() } set { subscriptionsStorage.replace(with: newValue) } } private var subscriptionsStorage = ThreadSafeDictionary(label: "subscriptions") fileprivate var subscriptionsWaitingAck = ThreadSafeDictionary(label: "subscriptionsWaitingAck") fileprivate var unsubscriptionsWaitingAck = ThreadSafeDictionary(label: "unsubscriptionsWaitingAck") /// Sending messages fileprivate var sendingMessages: [UInt16: CocoaMQTTMessage] = [:] /// message id counter private var _msgid: UInt16 = 0 fileprivate var socket: CocoaMQTTSocketProtocol fileprivate var reader: CocoaMQTTReader? // Closures public var didConnectAck: (CocoaMQTT, CocoaMQTTConnAck) -> Void = { _, _ in } public var didPublishMessage: (CocoaMQTT, CocoaMQTTMessage, UInt16) -> Void = { _, _, _ in } public var didPublishAck: (CocoaMQTT, UInt16) -> Void = { _, _ in } public var didReceiveMessage: (CocoaMQTT, CocoaMQTTMessage, UInt16) -> Void = { _, _, _ in } public var didSubscribeTopics: (CocoaMQTT, NSDictionary, [String]) -> Void = { _, _, _ in } public var didUnsubscribeTopics: (CocoaMQTT, [String]) -> Void = { _, _ in } public var didPing: (CocoaMQTT) -> Void = { _ in } public var didReceivePong: (CocoaMQTT) -> Void = { _ in } public var didDisconnect: (CocoaMQTT, Error?) -> Void = { _, _ in } public var didReceiveTrust: (CocoaMQTT, SecTrust, @escaping (Bool) -> Swift.Void) -> Void = { _, _, _ in } public var didCompletePublish: (CocoaMQTT, UInt16) -> Void = { _, _ in } public var didChangeState: (CocoaMQTT, CocoaMQTTConnState) -> Void = { _, _ in } public var didScheduleReconnect: (CocoaMQTT, UInt, UInt16) -> Void = { _, _, _ in } /// Initial client object /// /// - Parameters: /// - clientID: Client Identifier /// - host: The MQTT broker host domain or IP address. Default is "localhost" /// - port: The MQTT service port of host. Default is 1883 public init(clientID: String, host: String = "localhost", port: UInt16 = 1883, socket: CocoaMQTTSocketProtocol = CocoaMQTTSocket()) { self.clientID = clientID self.host = host self.port = port self.socket = socket super.init() deliver.delegate = self if let storage = CocoaMQTTStorage() { storage.setMQTTVersion("3.1.1") } else { printWarning("Localstorage initial failed for key: \(clientID)") } } deinit { aliveTimer?.suspend() autoReconnTimer?.suspend() socket.setDelegate(nil, delegateQueue: nil) socket.disconnect() } fileprivate func send(_ frame: Frame, tag: Int = 0) { printDebug("SEND: \(frame)") let data = frame.bytes(version: version) socket.write(Data(bytes: data, count: data.count), withTimeout: 5, tag: tag) } fileprivate func sendConnectFrame() { var connect = FrameConnect(clientID: clientID) connect.keepAlive = keepAlive connect.username = username connect.password = password connect.willMsg = willMessage connect.cleansess = cleanSession send(connect) reader!.start() } fileprivate func nextMessageID() -> UInt16 { if _msgid == UInt16.max { _msgid = 0 } _msgid += 1 return _msgid } fileprivate func puback(_ type: FrameType, msgid: UInt16) { switch type { case .puback: send(FramePubAck(msgid: msgid)) case .pubrec: send(FramePubRec(msgid: msgid)) case .pubcomp: send(FramePubComp(msgid: msgid)) default: return } } /// Connect to MQTT broker /// /// - Returns: /// - Bool: It indicates whether successfully calling socket connect function. /// Not yet established correct MQTT session public func connect() -> Bool { return connect(timeout: -1) } /// Connect to MQTT broker /// - Parameters: /// - timeout: Connect timeout /// - Returns: /// - Bool: It indicates whether successfully calling socket connect function. /// Not yet established correct MQTT session public func connect(timeout: TimeInterval) -> Bool { socket.setDelegate(self, delegateQueue: delegateQueue) reader = CocoaMQTTReader(socket: socket, delegate: self) do { if timeout > 0 { try socket.connect(toHost: self.host, onPort: self.port, withTimeout: timeout) } else { try socket.connect(toHost: self.host, onPort: self.port) } delegateQueue.async { [weak self] in guard let self = self else { return } self.connState = .connecting } return true } catch let error as NSError { printError("socket connect error: \(error.description)") return false } } /// Send a DISCONNECT packet to the broker then close the connection /// /// - Note: Only can be called from outside. /// This closes the connection expectedly, so auto-reconnect will not run. public func disconnect() { expected_disconnect() } /// Disconnect unexpectedly. /// This keeps auto-reconnect behavior enabled. func internal_disconnect() { is_internal_disconnected = false socket.disconnect() } private func expected_disconnect() { is_internal_disconnected = true send(FrameDisconnect(), tag: -0xE0) socket.disconnect() } /// Send a PING request to broker public func ping() { printDebug("ping") send(FramePingReq(), tag: -0xC0) __delegate_queue { self.delegate?.mqttDidPing(self) self.didPing(self) } } /// Publish a message to broker /// /// - Parameters: /// - topic: Topic Name. It can not contain '#', '+' wildcards /// - string: Payload string /// - qos: Qos. Default is Qos1 /// - retained: Retained flag. Mark this message is a retained message. default is false /// - Returns: /// - 0 will be returned, if the message's qos is qos0 /// - 1-65535 will be returned, if the messages's qos is qos1/qos2 /// - -1 will be returned, if the messages queue is full @discardableResult public func publish(_ topic: String, withString string: String, qos: CocoaMQTTQoS = .qos1, retained: Bool = false) -> Int { let message = CocoaMQTTMessage(topic: topic, string: string, qos: qos, retained: retained) return publish(message) } /// Publish a message to broker /// /// - Parameters: /// - message: Message @discardableResult public func publish(_ message: CocoaMQTTMessage) -> Int { let msgid: UInt16 if message.qos == .qos0 { msgid = 0 } else { msgid = nextMessageID() } var frame = FramePublish(topic: message.topic, payload: message.payload, qos: message.qos, msgid: msgid) frame.retained = message.retained delegateQueue.async { self.sendingMessages[msgid] = message } // Push frame to deliver message queue guard deliver.add(frame) else { delegateQueue.async { self.sendingMessages.removeValue(forKey: msgid) } return -1 } return Int(msgid) } /// Subscribe a `/` /// /// - Parameters: /// - topic: Topic Name or Topic Filter /// - qos: Qos. Default is qos1 public func subscribe(_ topic: String, qos: CocoaMQTTQoS = .qos1) { return subscribe([(topic, qos)]) } /// Subscribe a lists of topics /// /// - Parameters: /// - topics: A list of tuples presented by `(/, Qos)` public func subscribe(_ topics: [(String, CocoaMQTTQoS)]) { let msgid = nextMessageID() let frame = FrameSubscribe(msgid: msgid, topics: topics) send(frame, tag: Int(msgid)) subscriptionsWaitingAck[msgid] = topics } /// Unsubscribe a Topic /// /// - Parameters: /// - topic: A Topic Name or Topic Filter public func unsubscribe(_ topic: String) { return unsubscribe([topic]) } /// Unsubscribe a list of topics /// /// - Parameters: /// - topics: A list of `/` public func unsubscribe(_ topics: [String]) { let msgid = nextMessageID() let frame = FrameUnsubscribe(msgid: msgid, topics: topics) unsubscriptionsWaitingAck[msgid] = topics send(frame, tag: Int(msgid)) } } // MARK: CocoaMQTTDeliverProtocol extension CocoaMQTT: CocoaMQTTDeliverProtocol { func deliver(_ deliver: CocoaMQTTDeliver, wantToSend frame: Frame) { if let publish = frame as? FramePublish { let msgid = publish.msgid var message: CocoaMQTTMessage? if let sendingMessage = sendingMessages[msgid] { message = sendingMessage // printError("Want send \(frame), but not found in CocoaMQTT cache") } else { message = CocoaMQTTMessage(topic: publish.topic, payload: publish.payload()) } send(publish, tag: Int(msgid)) if let message = message { self.delegate?.mqtt(self, didPublishMessage: message, id: msgid) self.didPublishMessage(self, message, msgid) } } else if let pubrel = frame as? FramePubRel { // -- Send PUBREL send(pubrel, tag: Int(pubrel.msgid)) } } } extension CocoaMQTT { func __delegate_queue(_ fun: @escaping () -> Void) { delegateQueue.async { [weak self] in guard self != nil else { return } fun() } } private func prepareAutoReconnectAttempt() { if reconnectTimeInterval == 0 { reconnectTimeInterval = min(autoReconnectTimeInterval, maxAutoReconnectTimeInterval) } reconnectAttemptCount += 1 } private func updateAutoReconnectIntervalForNextAttempt() { let doubledInterval = UInt32(reconnectTimeInterval) * 2 reconnectTimeInterval = UInt16(min(doubledInterval, UInt32(maxAutoReconnectTimeInterval))) } private func resetAutoReconnectState() { reconnectTimeInterval = 0 reconnectAttemptCount = 0 autoReconnTimer = nil } private func notifyAutoReconnectScheduled() { delegate?.mqtt?(self, didScheduleReconnect: reconnectAttemptCount, after: reconnectTimeInterval) didScheduleReconnect(self, reconnectAttemptCount, reconnectTimeInterval) } } // MARK: - CocoaMQTTSocketDelegate extension CocoaMQTT: CocoaMQTTSocketDelegate { public func socketConnected(_ socket: CocoaMQTTSocketProtocol) { sendConnectFrame() } public func socket(_ socket: CocoaMQTTSocketProtocol, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Swift.Void) { printDebug("Call the SSL/TLS manually validating function") delegate?.mqtt?(self, didReceive: trust, completionHandler: completionHandler) didReceiveTrust(self, trust, completionHandler) } public func socketUrlSession(_ socket: CocoaMQTTSocketProtocol, didReceiveTrust trust: SecTrust, didReceiveChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { printDebug("Call the SSL/TLS manually validating function - socketUrlSession") delegate?.mqttUrlSession?(self, didReceiveTrust: trust, didReceiveChallenge: challenge, completionHandler: completionHandler) } // ? public func socketDidSecure(_ sock: MGCDAsyncSocket) { printDebug("Socket has successfully completed SSL/TLS negotiation") sendConnectFrame() } public func socket(_ socket: CocoaMQTTSocketProtocol, didWriteDataWithTag tag: Int) { // XXX: How to print writed bytes?? } public func socket(_ socket: CocoaMQTTSocketProtocol, didRead data: Data, withTag tag: Int) { let etag = CocoaMQTTReadTag(rawValue: tag)! var bytes = [UInt8]([0]) switch etag { case CocoaMQTTReadTag.header: data.copyBytes(to: &bytes, count: 1) reader!.headerReady(bytes[0]) case CocoaMQTTReadTag.length: data.copyBytes(to: &bytes, count: 1) reader!.lengthReady(bytes[0]) case CocoaMQTTReadTag.payload: reader!.payloadReady(data) } } public func socketDidDisconnect(_ socket: CocoaMQTTSocketProtocol, withError err: Error?) { // Clean up socket.setDelegate(nil, delegateQueue: nil) if is_internal_disconnected || !autoReconnect { resetAutoReconnectState() } connState = .disconnected delegate?.mqttDidDisconnect(self, withError: err) didDisconnect(self, err) guard !is_internal_disconnected else { is_internal_disconnected = false return } guard autoReconnect else { resetAutoReconnectState() return } prepareAutoReconnectAttempt() // Start reconnector once socket error occurred printInfo("Try reconnect to server after \(reconnectTimeInterval)s") notifyAutoReconnectScheduled() autoReconnTimer = CocoaMQTTTimer.after(Double(reconnectTimeInterval), name: "autoReconnTimer", { [weak self] in guard let self = self else { return } self.updateAutoReconnectIntervalForNextAttempt() _ = self.connect() }) } } // MARK: - CocoaMQTTReaderDelegate extension CocoaMQTT: CocoaMQTTReaderDelegate { func didReceive(_ reader: CocoaMQTTReader, connack: FrameConnAck) { printDebug("RECV: \(connack)") if connack.returnCode == .accept { // Disable auto-reconnect resetAutoReconnectState() is_internal_disconnected = false // Start keepalive timer let interval = Double(keepAlive <= 0 ? 60: keepAlive) aliveTimer = CocoaMQTTTimer.every(interval, name: "aliveTimer") { [weak self] in guard let self = self else { return } self.delegateQueue.async { guard self.connState == .connected else { self.aliveTimer = nil return } self.ping() } } // recover session if enable if cleanSession { deliver.cleanAll() } else { if let storage = CocoaMQTTStorage(by: clientID) { deliver.recoverSessionBy(storage) } else { printWarning("Localstorage initial failed for key: \(clientID)") } } connState = .connected } else { connState = .disconnected expected_disconnect() } delegate?.mqtt(self, didConnectAck: connack.returnCode ?? CocoaMQTTConnAck.serverUnavailable) didConnectAck(self, connack.returnCode ?? CocoaMQTTConnAck.serverUnavailable) } func didReceive(_ reader: CocoaMQTTReader, publish: FramePublish) { printDebug("RECV: \(publish)") let message = CocoaMQTTMessage(topic: publish.topic, payload: publish.payload(), qos: publish.qos, retained: publish.retained) message.duplicated = publish.dup printInfo("Received message: \(message)") delegate?.mqtt(self, didReceiveMessage: message, id: publish.msgid) didReceiveMessage(self, message, publish.msgid) if message.qos == .qos1 { puback(FrameType.puback, msgid: publish.msgid) } else if message.qos == .qos2 { puback(FrameType.pubrec, msgid: publish.msgid) } } func didReceive(_ reader: CocoaMQTTReader, puback: FramePubAck) { printDebug("RECV: \(puback)") deliver.ack(by: puback) delegate?.mqtt(self, didPublishAck: puback.msgid) didPublishAck(self, puback.msgid) } func didReceive(_ reader: CocoaMQTTReader, pubrec: FramePubRec) { printDebug("RECV: \(pubrec)") deliver.ack(by: pubrec) } func didReceive(_ reader: CocoaMQTTReader, pubrel: FramePubRel) { printDebug("RECV: \(pubrel)") puback(FrameType.pubcomp, msgid: pubrel.msgid) } func didReceive(_ reader: CocoaMQTTReader, pubcomp: FramePubComp) { printDebug("RECV: \(pubcomp)") deliver.ack(by: pubcomp) delegate?.mqtt?(self, didPublishComplete: pubcomp.msgid) didCompletePublish(self, pubcomp.msgid) } func didReceive(_ reader: CocoaMQTTReader, suback: FrameSubAck) { printDebug("RECV: \(suback)") guard let topicsAndQos = subscriptionsWaitingAck.removeValue(forKey: suback.msgid) else { printWarning("UNEXPECT SUBACK Received: \(suback)") return } guard topicsAndQos.count == suback.grantedQos.count else { printWarning("UNEXPECT SUBACK Recivied: \(suback)") return } let success: NSMutableDictionary = NSMutableDictionary() var failed = [String]() for (idx, (topic, _)) in topicsAndQos.enumerated() { if suback.grantedQos[idx] != .FAILURE { subscriptionsStorage[topic] = suback.grantedQos[idx] success[topic] = suback.grantedQos[idx].rawValue } else { failed.append(topic) } } delegate?.mqtt(self, didSubscribeTopics: success, failed: failed) didSubscribeTopics(self, success, failed) } func didReceive(_ reader: CocoaMQTTReader, unsuback: FrameUnsubAck) { printDebug("RECV: \(unsuback)") guard let topics = unsubscriptionsWaitingAck.removeValue(forKey: unsuback.msgid) else { printWarning("UNEXPECT UNSUBACK Received: \(unsuback.msgid)") return } // Remove local subscription for t in topics { subscriptionsStorage.removeValue(forKey: t) } delegate?.mqtt(self, didUnsubscribeTopics: topics) didUnsubscribeTopics(self, topics) } func didReceive(_ reader: CocoaMQTTReader, pingresp: FramePingResp) { printDebug("RECV: \(pingresp)") delegate?.mqttDidReceivePong(self) didReceivePong(self) } func didReceive(_ reader: CocoaMQTTReader, disconnect: FrameDisconnect) { printWarning("Received DISCONNECT in MQTT 3.1.1 mode, closing socket") internal_disconnect() } func didReceive(_ reader: CocoaMQTTReader, auth: FrameAuth) { printWarning("Received AUTH in MQTT 3.1.1 mode, closing socket") internal_disconnect() } }