Courier is a library for creating long running connections using MQTT which is the industry standard for IoT Messaging. Long running connection is a persistent connection established between client & server for bi-directional communication. A long running connection is maintained for as long as possible with the help of keepalive packets for instant updates. This also saves battery and data on mobile devices.
Find the detailed documentation here - https://gojek.github.io/courier-iOS/
End-to-end courier example - https://gojek.github.io/courier/docs/Introduction
Setup Courier to subscribe, send, and receive message with bi-directional long running connection between iOS device and broker.
You can run the sample App to connect to any broker that you can configure. Select CourierE2EApp
from the scheme.
Courier supports both Cocoapods and SPM for dependency manager. It is separated into 5 modules:
CourierCore
: Contains public APIs such as protocols and data types for Courier. Other modules have basic dependency on this module. You can use this module if you want to implement the interface in your project without adding Courier implementation in your project.CourierMQTT
: Contains implementation ofCourierClient
andCourierSession
usingMQTT
. This module has dependency toMQTTClientGJ
.MQTTClientGJ
: A forked version of open source library MQTT-Client-Framework. It add several features such as connect and inactivity timeout. It also fixes race condition crashes inMQTTSocketEncoder
andConnack
status 5 not completing the decode beforeMQTTTransportDidClose
got invoked bugs.CourierProtobuf
: Contains implementation ofProtobufMessageAdapter
usingProtofobuf
. It has dependency toSwiftProtobuf
library, this isoptional
and can be used if you are using protobuf for data serialization.CourierMQTTChuck
: Can be ussed to inspects all the outgoing or incoming packets for an underlying MQTT connection. It intercepts all the packets, persisting them and providing a UI for accessing all the MQTT packets sent or received. It also provides multiple other features like search, share, and clear data. UsesSwiftUI
under the hood.
// Podfile
target 'Example-App' do
use_frameworks!
pod 'CourierCore'
pod 'CourierMQTT'
pod 'CourierProtobuf' #optional
pod 'CourierMQTTChuck' #optional
end
Simply add the package dependency to your Package.swift and depend on CourierCore
and CourierMQTT
in the necessary targets:
dependencies: [
.package(url: "https://github.com/gojek/courier-iOS", branch: "main")
]
To connect to MQTT broker you need to implement IConnectionServiceProvider. First you need to implement IConnectionServiceProvider/clientId
to return an unique string to identify your client. This must be unique for each device that connect to broker.
var clientId: String {
UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
}
Next, you need to implement IConnectionServiceProvider/getConnectOptions(completion:)
method. You need to provide ConnectOptions
instance that will be used to make connection to the broker. This method provides an escaping closure, in case you need to retrieve the credential from remote API asynchronously.
func getConnectOptions(completion: @escaping (Result<ConnectOptions, AuthError>) -> Void) {
executeNetworkRequest { (response: ConnectOptions) in
completion(.success(connectOptions))
} failure: { _, _, error in
completion(.failure(error))
}
}
Here are the data that you need to provide in ConnectOptions.
/// IP Host address of the broker
public let host: String
/// Port of the broker
public let port: UInt16
/// Keep Alive interval used to ping the broker over time to maintain the long run connection
public let keepAlive: UInt16
/// Unique Client ID used by broker to identify connected clients
public let clientId: String
/// Username of the client
public let username: String
/// Password of the client used for authentication by the broker
public let password: String
/// Tells broker whether to clear the previous session by the clients
public let isCleanSession: Bool
Next, we need to create instance of CourierClient that uses MQTT as its implementation. Initialize CourierClientFactory
instance and invoke CourierClientFactory/makeMQTTClient(config:)
. We need to pass instance MQTTClientConfig with several parameters that we can customize.
let clientFactory = CourierClientFactory()
let courierClient = clientFactory.makeMQTTClient(
config: MQTTClientConfig(
authService: HiveMQAuthService(),
messageAdapters: [
JSONMessageAdapter(),
ProtobufMessageAdapter()
],
autoReconnectInterval: 1,
maxAutoReconnectInterval: 30
)
)
MQTTClientConfig/messageAdapters
: we need to pass array ofMessageAdapter
. This will be used for serialization when receiving from broker and sending message to the broker.CourierMQTT
provides built in message adapters for JSON(JSONMessageAdapter)
and Plist(PlistMessageAdapter)
format that conforms toCodable
protocol. You can only use one of them because both implements to Codable to avoid any conflict. To use protobuf, please importCourierProtobuf
and passProtobufMessageAdapter
.MQTTClientConfig/authService
: we need to pass our implementation of IConnectionServiceProvider protocol for providing the ConnectOptions to the client.MQTTClientConfig/autoReconnectInterval
The interval used to make reconnection to broker in case of connection lost. This will be multiplied by 2 for each time until it successfully make the connection. The upper limit is based onMQTTClientConfig/maxAutoReconnectInterval
.
To connect to the broker, you simply need to invoke connect
method
courierClient.connect()
To disconnect, you just need to invoke disconnect
method
courierClient.disconnect()
To get the ConnectionState, you can access the CourierSession/connectionState property
courierClient.connectionState
You can also subscribe the ConnectionState
publisher using the CourierSession/connectionStatePublisher
property. The observable API that Courier provide is very similar to Apple Combine
although it is implemented internally using RxSwift
so we can support iOS 12
.
courierClient.connectionStatePublisher
.sink { [weak self] self?.handleConnectionStateEvent($0) }
.store(in: &cancellables)
As MQTT supports QoS 1 and QoS 2 message to ensure deliverability when there is no internet connection and user reconnected back to broker, we also persists those message in local cache. To disconnect and remove all of this cache, you can invoke.
courierClient.destroy()
There are several things that you need to keep in mind when using Courier:
- Courier will always disconnect when the app goes to background as iOS doesn't support long run socket connection in background.
- Courier will always automatically reconnect when the app goes to foreground if there is a topic to subscribe.
- Courier handles reconnection in case of bad/lost internet connection using Reachability framework.
- Courier will persist QoS > 0 messages in case there are no active subscription to Observable/Publisher using configurable TTL.
The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. There are 3 QoS levels in MQTT:
- At most once (0)
- At least once (1)
- Exactly once (2).
When you talk about QoS in MQTT, you need to consider the two sides of message delivery:
- Message delivery form the publishing client to the broker.
- Message delivery from the broker to the subscribing client.
You can read more about the detail of QoS in MQTT from HiveMQ site.
To subscribe to a topic from the broker. we can invoke CourierSession/subscribe(_:)
and pass a tuple containing the topic string and QoS enum.
courierClient.subscribe(("chat/user1", QoS.zero))
You can also subscribe to multiple topics, invoking CourierSession/subscribe(_:)
and pass an array containing tuples of topic string and QoS enum
courierClient.subscribe([
("chat/user1", QoS.zero),
("order/1234", QoS.one),
("order/123456", QoS.two),
])
After you have subscribed to the topic, you need to subscribe to a message publisher passing the associated topic using CourierSession/messagePublisher(topic:)
. This method uses Generic
for serializing the binary data to a type. Make sure you have provided the associated MessageAdapter that can decode the data to the type.
courierClient.messagePublisher(topic: topic)
.sink { [weak self] (note: Note) in
self?.messages.insert(Message(id: UUID().uuidString, name: "Protobuf: \(note.title)", timestamp: Date()), at: 0)
}.store(in: &cancellables)
This method returns AnyPublisher which you can chain with operators such as AnyPublisher/filter(predicate:)
or AnyPublisher/map(transform:)
.
The observable API that Courier provide is very similar to Apple Combine although it is implemented internally using RxSwift so we can support iOS 12, only the filter
and map
operators are supported.
To unsubscribe from a topic. we can invoke CourierSession/unsubscribe(_:)
and pass a topic string.
courierClient.unsubscribe("chat/user1")
You can also subscribe to multiple topics, invoking CourierSession/unsubscribe(_:)
and pass an array containing tuples of topic string and QoS enum
courierClient.unsubscribe([
"chat/user1",
"order/"
])
To send the message to the broker, first make sure you have provided a MessageAdapter
that is able to encode your object to the binary data format. For example, if you have a data struct that you want to send as JSON. Make sure, it conforms to Encodable
protocol and pass JSONMessageAdapter
in MQTTClientConfig
when creating the CourierClient
instance.
You simply need to invoke CourierSession/publishMessage(_:topic:qos:)
passing the topic string and QoS enum. This is a throwing
function which can throw in case it fails to encode to data.
let message = Message(
id: UUID().uuidString,
name: message,
timestamp: Date()
)
try? courierService?.publishMessage(
message,
topic: "chat/1234",
qos: QoS.zero
)
To listen to Courier system events such on CourierEvent/connectionSuccess
, CourierEvent/connectionAttempt
, and many more casess declared in CourierEvent
enum, you can implement the ICourierEventHandler
protocol and implement ICourierEventHandler/onEvent(_:)
method. This method will be invoked for any courier system events.
Finally, make sure to have strong reference to the instance, and invoke CourierEventManager/addEventHandler(_:)
passing the instance.
courierClient.addEventHandler(analytics)
CourierMQTTChuck
is used to inspects all the outgoing or incoming packets for an underlying MQTT connection.
It intercepts all the packets, persisting them and providing a UI for accessing all the MQTT packets sent or received. It also provides multiple other features like search, share, and clear data.
It uses SwiftUI under the hood with minimum iOS deployment version of 15, you can still build this on iOS 11 to access the logger without the view.
Add Dependency to CourierMQTTChuck
and simply declare the logger like so:
import CourierMQTTChuck
let logger = MQTTChuckLogger()
Then Declare the MQTTChuckView
passing the logger
in SwiftUI View.
.sheet(isPresented: $showChuckView, content: {
NavigationView {
MQTTChuckView(logger: logger)
}
})
Read our contribution guide to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Courier iOS library.
All Courier modules except MQTTClientGJ are MIT Licensed. MQTTClientGJ is Eclipse Licensed.