iOS MQTT 接入示例(MQTT 物联套件)

前言

求是动端连接抱MQTT,点击按钮利用MQTT给门禁上之配备发送信息;
流淌:门禁设备(Android系统并了MQTT和给硬件信息发送指令的保管)
症结未缓解:
1.门最终开没起来成,硬件是不曾让举报的,门禁设备为未知情;
2.动装备消息发送了,指定的门禁设备是否收到信息,移动端还免知晓;

该文介绍的凡利用阿里之MQTT接抱ios的证实
为于的demo里没有参数说明,看简单说明作为了解;

转: http://jm.taobao.org/2013/10/11/1036/

MQTT协议中文版

MQTT是一个客户端服务端架构的昭示/订阅模式的消息传协议。它的计划性思想是轻飘、开放、简单、规范,易于落实。
这些特征令她对成千上万现象吧还是特别好的选择,特别是对此受限的条件要机器与机具的通信(M2M)以及物联网环境(IoT)
商量传送门:https://mcxiaoke.gitbooks.io/mqtt-cn/content/

上周在线上系统发现了简单只bug,值得记录下搜寻的经过及因。以后如还有查找bug比较有价之更,我吗会见继续享受。

MQTT应用场景

屏幕快照.png

率先独bug的开端,是在线上日志发现一个数打印的不得了——java.lang.ArrayIndexOutOfBoundsException。但是也并未仓库,只出一致执行一行的ArrayIndexOutOfBoundsException。没有仓库,不晓得死是起什么地方弃出来的,也就不能够找到题目之来源于,更称不上缓解。题外,工程师在为此log4j记录错误非常的时段,我视多丁如此用(假设e是格外对象):
log.error(“发生误:”+e);
或者:
log.error(“发生错误:”+e.getMessage());
这样的写法是不对,只记录了充分的音讯,而从未用堆积栈输出到日志,正确的写法是使用error的重载方法:
log.error(“xxx发生错误”,e);
这样才会以日记被完全地出口大堆栈来。如何勾勒好日志是另一个话题,这里不开展。继续我们的找bug经历。刚才提到,我们线上日志一直出现一行错误信息ArrayIndexOutOfBoundsException却从不仓库,是我们从不是地描写日记也?检查代码不是的,这个问题莫过于是跟JDK5引入的一个初特点有关,对于部分频繁抛来之大,JDK为性会做一个优化,在JIT重新编译后会抛出没有仓库的怪。在以server模式的早晚,这个优化是被的,我们的服务器跑在server模式下以jdk版本是6,因此于屡抛出ArrayIndexOutOfBoundsException异常一段时间后优化开始于作用,只抛来无仓库的不可开交信息了。

MQTT 物联套件

介绍 MQTT 协议基本概念,阿里巴巴 MQ 提供的 MQTT 服务的重要原理与 MQTT
协议要的采用场景
地方如下:
https://help.aliyun.com/document\_detail/42419.html?spm=5176.doc47755.6.560.dcabYu

那么怎么解决之题目为?因为这个优化是于JIT重新编译后才从作用,因此等同开始扔来十分的早晚或时有发生堆栈的,所以可以查较旧的日记,寻找出总体堆栈的好信息。但是由于我们的日记太特别,会定期去,我们的服务器也启动了怪丰富时,因此查找日志不是很倚重谱的措施。
另外一个解决办法是少禁用这个优化,强制要求每次都设摒弃来有堆栈的慌。幸好JDK提供了摘项来关闭是优化,配置JVM参数
-XX:-OmitStackTraceInFastThrow
就足以禁止这个优化(注意选择中的减号,加号是启用)。

MQTT iOS 接入示例

介绍如何下 iOS 客户端收发 MQTT 消息地址如下:
https://help.aliyun.com/document\_detail/47755.html?spm=5176.doc44711.6.633.pN8AIa

咱们摸索了大机器,配置了是参数并再次开一下。过了扳平会晤不怕找到问题所在,堆栈类似这样
Caused by: java.lang.ArrayIndexOutOfBoundsException: -1831238
at
sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:436)
at
java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2081)
at
java.util.GregorianCalendar.computeFields(GregorianCalendar.java:1996)
at java.util.Calendar.setTimeInMillis(Calendar.java:1109)
at java.util.Calendar.setTime(Calendar.java:1075)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:876)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:869)
at java.text.DateFormat.format(DateFormat.java:316)
读者必定猜到了,这个问题是由于SimpleDateFormat的误用引起的。SimpleDateFormat的javadoc惨遭来这般句话:
Date formats are not synchronized. It is recommended to create separate
format instances for each thread.
If multiple threads access a format concurrently, it must be
synchronized externally.
可生悲剧的凡及时句话是置身整个doc的末梢对,在我看来,这词话应该置身最前面才对。简单来说即使是SimpleDateFormat不是线程安全的,你还是每次都new一个来用,要么做加锁来共使用。

iOS接抱 非CocoaPods 安装配备

滑到示例地址之根: 下载demo
拿到路pods下的MQTTClient下之MQTTClient导入工程;
不用以LICENSE文件也导入进程序

发问题之代码就是由于工程师认为SimpleDateFormat的创办代价十分高,然后搞了个map做缓存,所有线程共用是instance做format,同时没有举行一道。悲剧就生了。
这里就是引出自身思念干的老二沾问题,在使一个近似或措施的早晚,最好能详细地看下该类的javadoc,JDK的javadoc是举行的很好的,javadoc除了做说明外,通常还见面受示例,并且会沾来一部分关键问题,如线程安全性和平台移植性。

不是CocoaPods安装,需要将设下面文件改化双料引号””
#import <MQTTClient/MQTTSession.h>
#import <MQTTClient/MQTTSessionLegacy.h>
#import <MQTTClient/MQTTSessionSynchron.h>
#import <MQTTClient/MQTTMessage.h>
#import <MQTTClient/MQTTTransport.h>
#import <MQTTClient/MQTTCFSocketTransport.h>
#import <MQTTClient/MQTTCoreDataPersistence.h>
#import <MQTTClient/MQTTSSLSecurityPolicyTransport.h>

#import "MQTTSession.h"
#import "MQTTSessionLegacy.h"
#import "MQTTSessionSynchron.h"
#import "MQTTMessage.h"
#import "MQTTTransport.h"
#import "MQTTCFSocketTransport.h"
#import "MQTTCoreDataPersistence.h"
#import "MQTTSSLSecurityPolicyTransport.h"

说到底,我拿开只测试,到底以采取SimpleDateFormat怎么开才是太好的点子?假设我们若促成一个formatDate方法以日期格式化成”yyyy-MM-dd”的格式。
先是单办法是每次用还创一个instance,并调用format方法:
public static String formatDate1(Date date) {
SimpleDateFormat format = new SimpleDateFormat(“yyyy-MM-dd”);
return format.format(date);
}

每当急需贯彻之地方导入头文件;
/*
 * MQTTClient: imports
 * MQTTSessionManager.h is optional
 */
#import "MQTTClient.h"
#import "MQTTSessionManager.h"

/*
 * MQTTClient: using your main view controller as the MQTTSessionManagerDelegate
 */

#import <CommonCrypto/CommonHMAC.h>

仲单措施是止创造一个instance,但是当调用方法的下召开同:
private static final SimpleDateFormat formatter = new
SimpleDateFormat(“yyyy-MM-dd”);

添加摄:
MQTTSessionManagerDelegate

/*
 * MQTTClient: keep a strong reference to your MQTTSessionManager here
 */
@property (strong, nonatomic) MQTTSessionManager *manager;

@property (strong, nonatomic) NSDictionary *mqttSettings;
@property (strong, nonatomic) NSString *rootTopic;
@property (strong, nonatomic) NSString *accessKey;
@property (strong, nonatomic) NSString *secretKey;
@property (strong, nonatomic) NSString *groupId;
@property (strong, nonatomic) NSString *clientId;
@property (assign, nonatomic) NSInteger qos;

@property (strong, nonatomic) NSMutableArray *chat;

public static synchronized String formatDate2(Date date) {
return formatter.format(date);
}
其三只法子较特殊,我们啊每个线程都缓存一个instance,存放于ThreadLocal里,使用的当儿打ThreadLocal里取就可了:
private static ThreadLocal<SimpleDateFormat> formatCache = new
ThreadLocal<SimpleDateFormat>() {

初始化客户端连接到host
 MQTT-Client-FrameWork 包提供的客户端类有 MQTTSession 和 MQTTSessionManager,
 建议使用后者维持静态资源,而且已经封装好自动重连等逻辑。
 初始化时需要传入相关的网络参数

参数在阿里供的demo里生一个plist文件,如下:

屏幕快照 2017.png

参数如何安排请查看:
https://help.aliyun.com/document\_detail/29536.html?spm=5176.doc29535.6.551.OrrVHX

@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(“yyyy-MM-dd”);
}

自布局文件导入相关属性,发起连接

自己的需要是光出一个地方以,进到此页面的时光,我才发起连接,
点击按钮给门禁上之java程序发送开门信,连接方式调用如下方法:

-(void)initMQTT{

    NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
    NSURL *mqttPlistUrl = [bundleURL URLByAppendingPathComponent:@"mqtt.plist"];
    self.mqttSettings = [NSDictionary dictionaryWithContentsOfURL:mqttPlistUrl];
    self.rootTopic = self.mqttSettings[@"rootTopic"];
    self.accessKey = self.mqttSettings[@"accessKey"];
    self.secretKey = self.mqttSettings[@"secretKey"];
    self.groupId = self.mqttSettings[@"groupId"];
    self.qos =[self.mqttSettings[@"qos"] integerValue];

    //clientId的生成必须遵循GroupID@@@前缀,且需要保证全局唯一
    /*为了保证全局唯一,ios可以获取CFUUID,每次获取都是不一样的,
       想保证一个设备一样,需要存本地一份;
        但是我这里需要每次使用的时候,每次连击所以为了保证clientid全局唯一,
        我每次都获取一次CFUUID,去掉中间的分隔线" - "代码如下,
    */
    CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);
    NSString* cfuuidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));
    NSString* tempstr = [cfuuidString stringByReplacingOccurrencesOfString:@"-" withString:@""];
    cfuuidString = tempstr;

    self.clientId=[NSString stringWithFormat:@"%@@@@%@%@",self.groupId,@"",cfuuidString];

    self.chat = [[NSMutableArray alloc] init];
    /*
     * MQTTClient: create an instance of MQTTSessionManager once and connect
     * will is set to let the broker indicate to other subscribers if the connection is lost
     */
    if (!self.manager) {
        self.manager = [[MQTTSessionManager alloc] init];
        self.manager.delegate = self;

        self.manager.subscriptions = [NSDictionary dictionaryWithObject:
                                      [NSNumber numberWithLong:self.qos]forKey:
                                      [NSString stringWithFormat:@"%@/#", self.rootTopic]];

        //password的计算方式是,使用secretkey对groupId做hmac签名算法,具体实现参考macSignWithText方法
        NSString *passWord = [[self class] macSignWithText:self.groupId secretKey:self.secretKey];
          /*
          此处从配置文件导入的Host即为MQTT的接入点,该接入点获取方式请参考资源申请章节文档,
           在控制台上申请MQTT实例,每个实例会分配一个接入点域名
          */
        [self.manager connectTo:self.mqttSettings[@"host"]
                           port:[self.mqttSettings[@"port"] intValue]
                            tls:[self.mqttSettings[@"tls"] boolValue]
                      keepalive:60  //心跳间隔不得大于120s
                          clean:true
                           auth:true
                           user:self.accessKey
                           pass:passWord
                           will:false
                      willTopic:nil
                        willMsg:nil
                        willQos:0
                 willRetainFlag:FALSE
                   withClientId:self.clientId];

    } else {
        [self.manager connectToLast];
    }
 }

/*
 userName 和 passWord 的设置

 由于服务端需要对客户端进行鉴权,因此需要传入合法的 userName 和 passWord。
 userName 设置为当前用户的 AccessKey,
 password 则设置为 MQTT 客户端 GroupID 的签名字符串,
 签名计算方式是使用 SecretKey 对 GroupID 做 HmacSHA1 散列加密。
 具体方法请参考 👇 中的 macSignWithText 函数。
 */
+ (NSString *)macSignWithText:(NSString *)text secretKey:(NSString *)secretKey
{
    NSData *saltData = [secretKey dataUsingEncoding:NSUTF8StringEncoding];
    NSData *paramData = [text dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableData* hash = [NSMutableData dataWithLength:CC_SHA1_DIGEST_LENGTH ];
    CCHmac(kCCHmacAlgSHA1, saltData.bytes, saltData.length, paramData.bytes, paramData.length, hash.mutableBytes);
    NSString *base64Hash = [hash base64EncodedStringWithOptions:0];

    return base64Hash;
}

};

connectTo方法里的参数说明:
  • tls:false
    //是否采用tls协议,mosca是支撑tls的,如果用了而装成true
  • clean:false
    //session是否清除,这个要留意,如果是false,代表保持登录,
    若客户端离线了双重登录就可接过及离线消息。注意:QoS为1暨QoS为2,并需订阅和发送一致
  • auth:true //是否采取登录验证,和底的user和pass参数组合使用
  • user:_userName //用户名
  • pass:_passwd //密码
  • willTopic:@”” //下面四单参数用来安装如果客户端好离线发送的音讯,
    眼下参数是哪个topic用来传异常离线消息,这里的好离线消息都靠的是客户端掉线后发送的掉线消息
  • will:@”” //异常离线消息体。自定义的良离线消息,约定好格式就可以了
  • willQos:0 //接收离线消息之级别 0、1、2
  • willRetainFlag:false //只有在也true时,Will Qos和Will
    Retain才会给读取,此时消息体payload中
    倘出现Will Topic和Will Message具体内容,否则,Will QoS和Will
    Retain值会被忽视掉
  • withClientId:nil];
    //客户端id,需要特地指出的是这id需要全局唯一,因为服务端是依据这个来区分不同之客户端的,
    默认情况下一个id登录后,假如有另外的接连为这个id登录,上一个接连会叫踹下线;

public static String formatDate3(Date date) {
SimpleDateFormat format = formatCache.get();
return format.format(date);
}
下一场我们测试下三单主意并发调用生之性并召开一个比较,并作100独线程循环调用1000万糟糕,记录耗时。我们安了JVM参数:
-Xmx512m -XX:CompileThreshold=10000
设置堆最要命也512M,设置当一个方式让调用1万次于的时刻即便被JIT编译。测试的结果如下:

出殡信息(当点击按钮的时光,发送信息方法如下:)

  [self.manager sendData:[self.scanDic.mj_JSONString dataUsingEncoding:NSUTF8StringEncoding]
                     topic:[NSString stringWithFormat:@"%@",
                            self.rootTopic]//此处设置多级子topic
                       qos:self.qos
                    retain:FALSE];

 

出殡方注意topic的设置

以下为安卓代码中的注解示例:
ios的demo中没这个证
消息发送至某主题Topic,所有订阅者Topic的配备都能接受此信息。
本MQTT的披露订阅规范,Topic也得以是不胜枚举Topic。
此地安装了发送至二级topic如下:
sampleClient.publish(topic+”/notice/”, message);
不过发送P2P信,二级Topic必须是“p2p”,三级topic是目标的ClientID
这里安装的三级topic需要是接收方的ClientID如下:
string p2pTopic =topic+”/p2p/”+consumerClientId;

 

qos:消息的传输方式

QoS说明如下:

  • 0 代表“至多一致软”,消息宣布了依靠底层 TCP/IP
    网络。会产生信息丢失或重复。
    随即等同级别可用于如下情况,环境传感器数据,丢失一不好读记录无所谓,因为抢继还见面有第二不良发送。
  • 1 代表“至少一不善”,确保信息到达,但消息又或会见时有发生。
  • 2
    代表“只出相同不成”,确保信息到达一次于。这同一级别可用以如下情况,在计费系统中,消息还或少会招不科学的结果。
  • 备考:由于服务端采用Mosca实现,Mosca目前光支持及QoS 1
  • 只要发送的凡临时的信,例如:给有topic所有在线的设施发送一修消息,丢失的话语也不在乎,0即便足以了
    (客户端登录的当儿如果指明支持的QoS级别,同时发送信息之时光吗要是指明这漫漫消息支持的QoS级别)
  • 若是用客户端保证会接纳信息,需要指定QoS为1,如果以要参加客户端不在线也如力所能及吸纳及信息,
    那么客户端登录的早晚如果指定session的行之有效,接收离线消息需要指定服务端要保存客户端的session状态。

 

接收发送信息之回调

/*
 * MQTTSessionManagerDelegate
 */
- (void)handleMessage:(NSData *)data onTopic:(NSString *)topic retained:(BOOL)retained {
    /*
     * MQTTClient: process received message
     */
    NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    [self.chat insertObject:[NSString stringWithFormat:@"RecvMsg from Topic: %@\nBody: %@", topic, dataString] atIndex:0];    
}

 

 

第1次测试

第2次测试

第3次测试

 

formatDate1

50545

49365

53532

 

formatDate2

10895

10761

10673

 

formatDate3

10386

9919

9527

(单位:毫秒)

自从结果来拘禁,方法1极端缓慢,方法3极度抢,但是就是无限缓慢的道1也得以达到每秒钟200 20万次等的调用量,很少来系统能够达这量级。这个点很麻烦成为您系统的瓶颈所在。从我的角度出发,我会建议您用艺术1或方法2,如果你追那么一些性能提升的话语,可以设想用智3,也尽管是为此ThreadLocal做缓存。

总结下本文找bug经历想发挥的几接触想法:
(1)正确地打印错误日志
(2)在server模式下,最好且设置-XX:-OmitStackTraceInFastThrow
(3)使用类或者措施的当儿,最好能详细看下javadoc,很多题材还能找到答案
(4)使用SimpleDateFormat的下如果留意线程安全性,要么每次new,要么做并,两者的特性有距离,但是这个出入十分不便成为你的性能瓶颈。