
前言
由于百度云有網(wǎng)頁版的MQTT測試終端,所以前期我們可以用那個(gè)網(wǎng)頁終端作為測試端即可.但是我們畢竟是處在移動互聯(lián)網(wǎng)時(shí)代啊,我們總是用網(wǎng)頁是什么鬼~,所以我們要使用手機(jī)終端來接入MQTT,從而實(shí)現(xiàn)手機(jī)終端控制物聯(lián)網(wǎng)設(shè)備.網(wǎng)上的關(guān)于這方面的資料還是比較多的,我用到的三方是MQTTClient,個(gè)人感覺還是十分的簡單.API方法也就那么多,所以上手很快.
在一切開始之前,我們先來回顧一下整個(gè)項(xiàng)目的代碼邏輯.示意圖如下所示.
首先是上線部分,這里主要是分為兩種情況,一,用戶終端已經(jīng)不管在線與否,硬件上線都必須發(fā)布設(shè)備上線消息,里面包含設(shè)備的ID,設(shè)備的名稱以及設(shè)備的功能管理等;二,當(dāng)前用戶上線,會發(fā)布一個(gè)終端設(shè)備上線信息,這時(shí)候所有的在線硬件設(shè)備都需要發(fā)送一遍設(shè)備信息,用戶終端根據(jù)反饋回來的設(shè)備信息更新UI.
然后就是下線邏輯,用戶終端下線不需要通知硬件,但是當(dāng)硬件設(shè)備下線的時(shí)候,需要去通過遺囑消息通知用戶終端,用戶終端根據(jù)其信息更新UI界面.
硬件發(fā)送溫濕度消息邏輯比較簡單只需要發(fā)送溫濕度消息給終端即可.用戶終端根據(jù)發(fā)送過來的消息更新UI界面.
用戶終端發(fā)送控制硬件消息則和上面的有所不同.因?yàn)楫?dāng)硬件接受到控制消息做出對應(yīng)改變的時(shí)候,終端需要根據(jù)設(shè)備當(dāng)前的狀態(tài)來顯示UI,所以硬件設(shè)備還需要發(fā)送一個(gè)反饋消息給用戶終端,告訴用戶終端當(dāng)前設(shè)備的狀態(tài).用戶終端再根據(jù)這個(gè)狀態(tài)修改對應(yīng)的UI界面即可.

又一次分析了代碼邏輯,接下來我們一起看一下MQTTClient提供的API方法以及常用屬性.
MQTTClient的API
- 初始化連接MQTT服務(wù)器操作.
- (void)connectTo:(NSString *)host
port:(NSInteger)port
tls:(BOOL)tls
keepalive:(NSInteger)keepalive
clean:(BOOL)clean
auth:(BOOL)auth
user:(NSString *)user
pass:(NSString *)pass
will:(BOOL)will
willTopic:(NSString *)willTopic
willMsg:(NSData *)willMsg
willQos:(MQTTQosLevel)willQos
willRetainFlag:(BOOL)willRetainFlag
withClientId:(NSString *)clientId
securityPolicy:(MQTTSSLSecurityPolicy *)securityPolicy
certificates:(NSArray *)certificates
protocolLevel:(MQTTProtocolVersion)protocolLevel
connectHandler:(MQTTConnectHandler)connectHandler;
- 主動斷開與服務(wù)器的連接.
- (void)disconnectWithDisconnectHandler:(MQTTDisconnectHandler)disconnectHandler;
- 發(fā)送消息,包括消息主體(data),主題(topic),消息質(zhì)量等級(qos),是否是需要回執(zhí)(retainFlag)等參數(shù).
- (UInt16)sendData:(NSData *)data topic:(NSString *)topic qos:(MQTTQosLevel)qos retain:(BOOL)retainFlag;
- 訂閱主題字典屬性,以訂閱主題名為key值,以消息質(zhì)量等級(Qos)為value值進(jìn)行存儲.
@property (strong, nonatomic) NSDictionary<NSString *, NSNumber *> *subscriptions;
- MQTT當(dāng)前狀態(tài)屬性,只讀屬性.
@property (nonatomic, readonly) MQTTSessionManagerState state;
- MQTT代理屬性
@property (weak, nonatomic) id<MQTTSessionManagerDelegate> delegate;
- MQTT狀態(tài)改變回調(diào)方法,MQTT代理方法之一.通過這個(gè)方法,我們可以監(jiān)聽MQTT的狀態(tài)從而做出對應(yīng)的UI改變.
- (void)sessionManager:(MQTTSessionManager *)sessionManager didChangeState:(MQTTSessionManagerState)newState;
- MQTT接受消息方法,MQTT代理方法之一.通過這個(gè)方法,我們可以監(jiān)聽所有訂閱的主題所接受到的消息.
- (void)handleMessage:(NSData *)data onTopic:(NSString *)topic retained:(BOOL)retained;
上面說了一堆API,接下來,我們就看一下我們?nèi)绾问褂?strong>MQTTClient的API來實(shí)現(xiàn)我們想要的功能吧.
代碼解讀
在講解說明這個(gè)代碼工程之前.我們先來聊聊我都定義了什么主題,所有主題如下所示.
| 主題名稱 | Qos | 功能 | 硬件權(quán)限 | 終端權(quán)限 |
|---|---|---|---|---|
| Client | Qos0 | 設(shè)備信息主題 | 發(fā)布,訂閱 | 發(fā)布,訂閱 |
| Will | Qos0 | 遺囑主題 | 發(fā)布,訂閱 | 發(fā)布,訂閱 |
| Data | Qos0 | 溫濕度數(shù)據(jù)主題 | 發(fā)布 | 訂閱 |
| Order | Qos0 | 用戶指令主題 | 訂閱 | 發(fā)布 |
其中硬件設(shè)備反饋是通過 Client 來進(jìn)行發(fā)布的.還有個(gè)問題就是如果你的身份對某個(gè)主題沒有權(quán)限,你去對該主題進(jìn)行訂閱和發(fā)布消息會導(dǎo)致MQTT的重新連接.
上面看完了主題設(shè)計(jì)部分,我們接下來一起來看一下iOS代碼部分,對于UI部分,我們沒有什么好說的.我們主要看一下MQTT實(shí)現(xiàn)部分.
首先,我們先用cocoapod導(dǎo)入 MQTTClient,如下所示.
pod 'MQTTClient'
整體的代碼部分在Helps目錄下的MQTTManager單例類中.其實(shí)主要MQTT方法有連接方法,斷開方法,重新連接方法,訂閱和取消訂閱主題方法,發(fā)送消息等方法.如下所示.
/**
綁定連接MQTT服務(wù)器
@param username 賬號
@param password 密碼
@param topicArray 主題名稱數(shù)組
@param isSSL 是否是SSL連接
*/
- (void)bindWithUserName:(NSString *)username password:(NSString *)password topicArray:(NSArray <NSString *>*)topicArray isSSL:(BOOL)isSSL;
/**
主動斷開MQTT服務(wù)器
*/
- (void)disconnectService;
/**
重新連接MQTT服務(wù)器
*/
- (void)reloadConectService;
/**
訂閱某個(gè)主題
@param topic 主題名稱
*/
- (void)subscribeTopic:(NSString *)topic;
/**
取消訂閱
@param topic 主題名稱
*/
- (void)unsubscribeTopic:(NSString *)topic;
/**
發(fā)送字符串類型的消息
@param stringMessage 字符串消息
@param topic 主題
*/
- (void)sendMQTTStringMessage:(NSString *)stringMessage topic:(NSString *)topic;
/**
發(fā)送字典類型的消息
@param mapMessage 字典類型消息
@param topic 主題
*/
- (void)sendMQTTMapMessage:(NSDictionary *)mapMessage topic:(NSString *)topic;
然后,我定義了幾個(gè)通知消息名稱,為什么使用通知,主要是可能會有多個(gè)界面都需要MQTT的相關(guān)數(shù)據(jù),所以定了通知來進(jìn)行數(shù)據(jù)的傳遞.通知名稱如下所示.
//收到消息的通知,object攜帶類型為MQTTMessageModel
#define ReceiveMessageNotificationName @"ReceiveMessageNotificationName"
//MQTT狀態(tài)發(fā)生改變的通知,不攜帶object.也可以使用KVO監(jiān)聽單例中mqttState的變化
#define MQTTChangeStateNotificationName @"MQTTChangeStateNotificationName"
//MQTT的可操作指令發(fā)送改變的通知
#define MQTTOrderChangeStateNotificationName @"MQTTOrderChangeStateNotificationName"
//MQTT的f設(shè)備返回指令信息的通知 帶有@{@"clientID":xxx, @"switchID":xxx}信息
#define MQTTOrderResponseStateNotificationName @"MQTTOrderResponseStateNotificationName"
MQTTManager.m 實(shí)現(xiàn)邏輯部分主要說明兩個(gè)方法,一個(gè)是狀態(tài)回調(diào)方法,一個(gè)是數(shù)據(jù)接受回調(diào)方法.
我們一一來看,先來看狀態(tài)回調(diào)方法.狀態(tài)回調(diào)方法中不管是哪種狀態(tài)都會發(fā)出通知消息,進(jìn)行對應(yīng)的UI界面更新操作. 有一種特殊情況需要注意,那就是當(dāng)設(shè)備連接成功之后,我們需要在MQTTClientTopic發(fā)送設(shè)備的信息.包括設(shè)備的類型(0:用戶終端 1:硬件),設(shè)備名稱,設(shè)備ID等消息.這樣當(dāng)在線硬件接受到消息之后就同樣會發(fā)布硬件信息,這樣在用戶終端就會知道那個(gè)設(shè)備是處于在線狀態(tài),便于我們?nèi)ゲ僮骱筒榭?
//狀態(tài)監(jiān)聽代理方法
- (void)sessionManager:(MQTTSessionManager *)sessionManager didChangeState:(MQTTSessionManagerState)newState {
switch (newState) {
case MQTTSessionManagerStateConnected:{
NSLog(@"eventCode -- 連接成功");
NSDictionary *message = @{
@"type":@(3),
@"data":@{
@"clientType":@(0),
@"clientName":@"騷棟的手機(jī)",
@"clientID":self.cliendId
},
};
self.mqttState = MQTTStateDidConnect;
[self sendMQTTMapMessage:message topic:MQTTClientTopic];
break;
}
case MQTTSessionManagerStateConnecting:
NSLog(@"eventCode -- 連接中");
self.mqttState = MQTTStateConnecting;
break;
case MQTTSessionManagerStateClosed:
NSLog(@"eventCode -- 連接被關(guān)閉");
self.mqttState = MQTTStateDisConnect;
break;
case MQTTSessionManagerStateError:
NSLog(@"eventCode -- 連接錯(cuò)誤");
self.mqttState = MQTTStateDisConnect;
break;
case MQTTSessionManagerStateClosing:
NSLog(@"eventCode -- 關(guān)閉中");
self.mqttState = MQTTStateDisConnect;
break;
case MQTTSessionManagerStateStarting:
NSLog(@"eventCode -- 連接開始");
break;
default:
break;
}
[[NSNotificationCenter defaultCenter] postNotificationName:MQTTChangeStateNotificationName object:nil];
}
數(shù)據(jù)接受回調(diào)方法中主要做的操作是根據(jù)接受到的數(shù)據(jù)去更新UI.其中有溫濕度數(shù)據(jù),反饋數(shù)據(jù),遺囑消息,設(shè)備消息等,然后根據(jù)不同的消息發(fā)布不同的通知消息,從而進(jìn)行UI的修改操作.整體代碼如下所示.
//接受到消息的回調(diào)代理方法
- (void)handleMessage:(NSData *)data onTopic:(NSString *)topic retained:(BOOL)retained {
NSDictionary *message = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
int type = [message[@"type"] intValue];
MQTTMessageModel *messageModel = [[MQTTMessageModel alloc] init];
[messageModel setValuesForKeysWithDictionary:message[@"data"]];
switch (type) {
case 0:
//溫濕度數(shù)據(jù)
messageModel.messageType = MQTTMessageTypeData;
break;
case 1:{
//反饋數(shù)據(jù)
messageModel.messageType = MQTTMessageTypeResponse;
for (ClientModel *clientModel in self.clientArray) {
if ([clientModel.clientID isEqualToString:messageModel.clientID]) {
for (SwitchModel *switchModel in clientModel.switchArray) {
if ([switchModel.switchID isEqualToString:messageModel.switchID]) {
switchModel.switchState = messageModel.isOn;
break;
}
}
break;
}
}
[[NSNotificationCenter defaultCenter] postNotificationName:MQTTOrderResponseStateNotificationName object:@{@"clientID":messageModel.clientID,
@"switchID":messageModel.switchID,
@"isOn":messageModel.isOn}];
break;
}
case 2:{
//遺囑離線數(shù)據(jù)
messageModel.messageType = MQTTMessageTypeWill;
//移除所有相關(guān)的指令信息
for (NSInteger i = self.clientArray.count - 1; i >= 0; i--) {
ClientModel *clientModel = self.clientArray[i];
if ([clientModel.clientID isEqualToString:messageModel.clientID]) {
[self.clientArray removeObject:clientModel];
}
}
[[NSNotificationCenter defaultCenter] postNotificationName:MQTTOrderChangeStateNotificationName object:messageModel];
break;
}
case 3:{
//設(shè)備信息
messageModel.messageType = MQTTMessageTypeClient;
ClientModel *clientModel = [[ClientModel alloc] init];
[clientModel setValuesForKeysWithDictionary:message[@"data"]];
NSArray *switchs = message[@"data"][@"switchs"];
for (NSDictionary * switchDic in switchs) {
SwitchModel *switchModel = [[SwitchModel alloc] init];
switchModel.clientID = clientModel.clientID;
[switchModel setValuesForKeysWithDictionary:switchDic];
[clientModel.switchArray addObject:switchModel];
}
if (clientModel.clientEunmType == ClientTypeESP8266) {
BOOL isHaveClient = NO;
//查看數(shù)組中是否有該設(shè)備的信息
for (ClientModel *nowClientModel in self.clientArray) {
if ([clientModel.clientID isEqualToString:nowClientModel.clientID]) {
isHaveClient = YES;
break;
}
}
if (!isHaveClient) {
[self.clientArray addObject:clientModel];
[[NSNotificationCenter defaultCenter] postNotificationName:MQTTOrderChangeStateNotificationName object:messageModel];
}
}
break;
}
}
[[NSNotificationCenter defaultCenter] postNotificationName:ReceiveMessageNotificationName object:messageModel];
}
其他的代碼都比較簡單,連接過程也不過多的敘述了,大家自行修改測試即可.
結(jié)語
好了,用戶iOS終端代碼已經(jīng)完成了,整體來說還是比較簡單.因?yàn)镸QTT協(xié)議主要就是訂閱和發(fā)布.不像其他的即時(shí)通訊協(xié)議有很多的規(guī)定和規(guī)則.故造成API也很簡單.這個(gè)就不過多敘述了.其他的安卓方面的MQTT以及微信小程序的MQTT都比較好實(shí)現(xiàn),這里我不會??,我就不多說了,各位看官自行百度吧.最后放上Demo的傳送門,大家自行修改Macros目錄下的SDPrefixHeader.pch宏定義參數(shù)即可.如果有任何問題,歡迎在評論區(qū)批評指導(dǎo),謝謝大家了.
iOS 終端Github傳送門
