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 协议基本概念,阿里巴巴(阿里巴巴(Alibaba)) MQ 提供的 MQTT 服务的主要性原理以及 MQTT
协议主要的利用场景
地址如下:
https://help.aliyun.com/document\_detail/42419.html?spm=5176.doc47755.6.560.dcabYu

那就是说怎么解决那么些问题呢?因为那个优化是在JIT重新编译后才起效果,因而一起先抛出极度的时候照旧有堆栈的,所以可以查阅较旧的日志,寻找有全部堆栈的足够音信。可是由于我们的日记太大,会定期删除,大家的服务器也启动了很长日子,因而查找日志不是很可靠的章程。
另一个解决办法是暂时禁用这些优化,强制必要每便都要抛出有堆栈的丰硕。幸好JDK提供了选项来关闭那个优化,配置JVM参数
-XX:-OmitStackTraceIn法斯特(Fast)Throw
就足以禁止那么些优化(注意选拔中的减号,加号是启用)。

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的误用引起的。SimpleDateFormatjavadoc中有这么句话:
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时,威·尔(W·ill) Qos和威尔Retain才会被读取,此时信息体payload中
    要出新Will Topic和威尔 Message具体内容,否则,威·尔(W·ill) QoS和WillRetain值会被忽视掉
  • 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:-OmitStackTraceIn法斯特(Fast)Throw
(3)使用类或者措施的时候,最好能详细阅读下javadoc,很多题材都能找到答案
(4)使用SimpleDateFormat的时候要留心线程安全性,要么每一遍new,要么做一道,两者的性质不尽相同,但是这些距离很难成为你的习性瓶颈。