EMQTT
介绍
-
EMQ是基于 Erlang/OTP 平台开发的开源物联网 MQTT 消息服务器。
-
Erlang/OTP 是出色的软实时、低延时、分布式的语言平台。
-
MQTT 是轻量的、发布订阅模式的物联网消息协议。
-
EMQ 项目设计目标是承载移动终端或物联网终端海量 MQTT 连接,并实现在海量物联网设备间快速低延时消息路由:
1、稳定承载大规模的 MQTT 客户端连接,单服务器节点支持50万到100万连接。
2、分布式节点集群,快速低延时的消息路由,单集群支持1000万规模的路由。
3、消息服务器内扩展,支持定制多种认证方式、高效存储消息到后端数据库。
4、完整物联网协议支持,MQTT、MQTT-SN、CoAP、WebSocket 或私有协议支持。
Broker
Broker是什么?broker是代理,发布者发布消息到broker中,订阅都能过broker订阅消息,broker起到一个桥梁的作用,类似于tomcat、nginx
发布订阅
发布者既可以发布也可以订阅,同样的,订订阅者也是可以订阅和发布,这就实现了双方的通信,不难发现这在编程中很常见,是一个异步模式,这也是被用来做推送的原因,举个例子,我们打电话的时候,当对方不接听我们电话也就打不通,必须双方都在才可以通话,这是同步请求/回答,而mqtt则不需要对话的响应,这类似于发邮件,我们发邮件后,对方不一定就立即看到,等他有空的时候打开邮箱才看到,这就是异步发布/订阅的场景。
主题
MQTT 发布者与订阅者之间通过”主题”(Topic) 进行消息路由,MQTT 主题(Topic) 支持’+’, ‘#’的通配符,’+’通配一个层级,’#’通配多个层级(必须在末尾)。
- MQTT 消息发布者只能向特定’名称主题’(不支持通配符)发布消息。
- 订阅者通过订阅’过滤主题’(支持通配符)来匹配消息。
EMQ 消息服务器概念上更像一台网络路由器(Router)或交换机(Switch),而不是传统的企业级消息服务器(MQ)。相比网络路由器按 IP 地址或 MPLS 标签路由报文,EMQ 按主题树(Topic Trie)发布订阅模式在集群节点间路由 MQTT 消息。
订阅、主题、会话
一、订阅(Subscription)
订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。
二、会话(Session)
每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。
三、主题名(Topic Name)
连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。
四、主题筛选器(Topic Filter)
一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。
五、负载(Payload)
消息订阅者所具体接收的内容
服务质量
为了满足不同的场景,MQTT支持三种不同级别的服务质量(Quality of Service,QoS)为不同场景提供消息可靠性:
- 级别0:尽力而为。消息发送者会想尽办法发送消息,但是遇到意外并不会重试。
- 级别1:至少一次。消息接收者如果没有知会或者知会本身丢失,消息发送者会再次发送以保证消息接收者至少会收到一次,当然可能造成重复消息。
- 级别2:恰好一次。保证这种语义肯待会减少并发或者增加延时,不过丢失或者重复消息是不可接受的时候,级别2是最合适的。
安装 启动
RPM安装:rpm -ivh emqttd-centos7-v2.1.2-1.el7.centos.x86_64.rpm
Erlang/OTP R19 依赖 lksctp-tools 库:yum install lksctp-tools
DEB安装:sudo dpkg -i emqttd-ubuntu16.04_v2.0_amd64.deb
Erlang/OTP R19 依赖 lksctp-tools 库:apt-get install lksctp-tools
通用程序包:unzip emqttd-centos7-v2.0.zip
- 控制台调试模式启动 ./emqttd console
- 守护进程模式启动 ./emqttd start
- 查看运行状态 ./emqttd_ctl status
- 停止服务器 ./emqttd stop
emqttd消息服务器启动后,会默认加载Dashboard插件,启动Web管理控制台。用户可通过Web控制台, 查看服务器运行状态、统计数据、客户端(Client)、会话(Session)、主题(Topic)、订阅(Subscription)。
控制台地址: http://127.0.0.1:18083,默认用户: admin,密码:public
插件
扩展插件通过 ‘bin/emqttd_ctl’ 管理命令行,或 Dashboard 控制台加载启用。
- emq_plugin_template 插件模版与演示代码
- emq_retainer Retain 消息存储插件
- emq_modules Presence, Subscription 扩展模块插件
- emq_dashboard Web 管理控制台,默认加载
- emq_auth_clientid ClientId、密码认证插件
- emq_auth_username 用户名、密码认证插件
- emq_auth_ldap LDAP 认证插件
- emq_auth_http HTTP 认证插件
- emq_auth_mysql MySQL 认证插件
- emq_auth_pgsql PostgreSQL 认证插件
- emq_auth_redis Redis 认证插件
- emq_auth_mongo MongoDB 认证插件
- emq_sn MQTT-SN 协议插件
- emq_coap CoAP 协议插件
- emq_stomp Stomp 协议插件
- emq_recon Recon 优化调测插件
- emq_reloader 热升级插件(开发调试)
端口
EMQ 默认开启的 MQTT 服务 TCP 端口:
- 1883 MQTT 协议端口
- 8883 MQTT/SSL 端口
- 8080 HTTP API 端口
- 8083 MQTT/WebSocket 端口
- 8084 MQTT/WebSocket/SSL 端口
- 18083 Dashboard 管理控制台端口
EMQ 节点集群使用的 TCP 端口:
- 4369 集群节点发现端口
- 6369 集群节点控制通道
配置文件 环境变量
/var/log/emqttd 日志文件目录
/var/lib/emqttd/ 数据文件目录
etc/emq.conf EMQ 2.0 消息服务器配置文件
etc/acl.conf EMQ 2.0 默认ACL规则配置文件
etc/plugins/*.conf EMQ 2.0 各类插件配置文件
启动时通过环境变量设置EMQ节点名称、安全Cookie以及TCP端口号:
- EMQ_NODE_NAME Erlang 节点名称,例如: emq@127.0.0.1
- EMQ_NODE_COOKIE Erlang 分布式节点通信 Cookie
- EMQ_MAX_PORTS Erlang 虚拟机最大允许打开文件 Socket 数
- EMQ_TCP_PORT MQTT/TCP 监听端口,默认: 1883
- EMQ_SSL_PORT MQTT/SSL 监听端口,默认: 8883
- EMQ_WS_PORT MQTT/WebSocket 监听端口,默认: 8083
- EMQ_WSS_PORT MQTT/WebSocket/SSL 监听端口,默认: 8084
EMQ 节点连接方式:
node.proto_dist = inet_tcp 或 inet6_tcp 或 inet_tls
Erlang 虚拟机主要参数说明:
- node.process_limit Erlang 虚拟机允许的最大进程数,一个 MQTT 连接会消耗2个 Erlang 进程,所以参数值 > 最大连接数 * 2
- node.max_ports Erlang 虚拟机允许的最大 Port 数量,一个 MQTT 连接消耗1个 Port,所以参数值 > 最大连接数
- node.dist_listen_min Erlang 分布节点间通信使用 TCP 连接端口范围。注: 节点间如有防火墙,需要配置该端口段
- node.dist_listen_max Erlang 分布节点间通信使用 TCP 连接端口范围。注: 节点间如有防火墙,需要配置该端口段
日志参数配置:
log.console = console 或 off 或 file 或 both
log.error.file = log/error.log
log.crash = on 或 off
log.syslog.level = error 或 debug, info, notice, warning, critical, alert, emergency
MQTT 协议参数配置
ClientId 最大允许长度
mqtt.max_clientid_len = 1024
MQTT 最大报文尺寸
mqtt.max_packet_size = 64KB
MQTT 客户端最大允许闲置时间(Socket 建立,未收到 CONNECT 报文)
mqtt.client.idle_timeout = 30
启用客户端连接统计
mqtt.client.enable_stats = off 或 on
强制 GC 设置(0表示禁用)
mqtt.conn.force_gc_count = 100
ACL规则
EMQ 消息服务器通过 ACL(Access Control List) 实现 MQTT 客户端访问控制。
EMQ 支持基于 etc/acl.conf 文件或 MySQL、 PostgreSQL 等插件的访问控制规则。
EMQ 消息服务器默认访问控制,在 etc/emq.conf 中设置:
是否开启匿名认证
mqtt.allow_anonymous = true
默认访问控制(ACL)文件
mqtt.acl_nomatch = allow
mqtt.acl_file = etc/acl.conf
ACL 规则定义在 etc/acl.conf,EMQ 启动时加载到内存。
ACL默认访问规则设置
etc/acl.conf 访问控制规则定义:
允许|拒绝 用户|IP地址|ClientID 发布|订阅 主题列表
%% 允许’dashboard’用户订阅 'KaTeX parse error: Expected 'EOF', got '#' at position 5: SYS/#̲' {allow, {user…SYS/#"]}.
%% 允许本机用户发布订阅全部主题
{allow, {ipaddr, “127.0.0.1”}, pubsub, ["$SYS/#", “#”]}.
%% 拒绝用户订阅’KaTeX parse error: Expected 'EOF', got '#' at position 4: SYS#̲'与'#'主题 {deny, …SYS/#", {eq, “#”}]}.
%% 上述规则无匹配,允许
{allow, all}.
默认规则只允许本机用户订阅’$SYS/#’与’#’
EMQ 消息服务器接收到 MQTT 客户端发布(PUBLISH)或订阅(SUBSCRIBE)请求时,会逐条匹配 ACL 访问控制规则,直到匹配成功返回 allow 或 deny。
MQTT 会话参数设置
mqtt.session.upgrade_qos = off
##可以一次“飞行”的最大QoS 1和2消息数,0表示没有限制。
mqtt.session.max_inflight = 32
##重新尝试重新传输QoS1 / 2消息的间隔。
mqtt.session.retry_interval = 20s
##等待PUBREL的最大数据包,0表示无限制
mqtt.session.max_awaiting_rel = 100
##正在等待PUBREL超时
mqtt.session.await_rel_timeout = 20s
##启用统计
mqtt.session.enable_stats = off
##过期时间
mqtt.session.expiry_interval = 2h
参数设置
MQTT 消息队列参数设置
- mqueue.type 队列类型。simple: 简单队列,priority: 优先级队列
- mqueue.priority 主题(Topic)队列优先级设置
- mqueue.max_length 队列长度, infinity 表示不限制
- mqueue.low_watermark 解除告警水位线
- mqueue.high_watermark 队列满告警水位线
- mqueue.qos0 是否缓存 QoS0 消息
Broker 参数设置
broker_sys_interval 设置系统发布 $SYS 消息周期:
mqtt.broker.sys_interval = 60s
发布订阅(PubSub)参数设置
PubSub池大小,默认值应该是调度程序编号。
mqtt.pubsub.pool_size = 8
mqtt.pubsub.by_clientid = true
异步订阅
mqtt.pubsub.async = true
桥接(Bridge)参数设置
mqtt.bridge.max_queue_len = 10000 队列大小
mqtt.bridge.ping_down_interval = 1s PING节点的时间间隔
插件配置目录
mqtt.plugins.etc_dir = etc/plugins/
用于存储加载的插件名称的文件
mqtt.plugins.loaded_file = data/loaded_plugins
MQTT Listeners 参数说明 :省略
分布集群:
Erlang/OTP 语言平台的分布式程序,由分布互联的 Erlang 运行系统组成,每个 Erlang 运行系统被称为节点(Node),节点(Node) 间通过 TCP 互联,消息传递的方式通信。
EMQ 消息服务器集群基于 Erlang/OTP 分布式设计,集群原理可简述为下述两条规则:
MQTT 客户端订阅主题时,所在节点订阅成功后广播通知其他节点:某个主题(Topic)被本节点订阅。
MQTT 客户端发布消息时,所在节点会根据消息主题(Topic),检索订阅并路由消息到相关节点。
节点桥接:
节点间桥接与集群不同,不复制主题树与路由表,只按桥接规则转发 MQTT 消息。
提供的认证插件
EMQ 消息服务器认证由一系列认证插件(Plugin)提供,系统支持按用户名密码、ClientID 或匿名认证。
系统默认开启匿名认证,通过加载认证插件可开启的多个认证模块组成认证链:
EMQ 2.0 版本提供的认证插件包括:
- emq_auth_clientid ClientId 认证/鉴权插件
- emq_auth_username 用户名密码认证/鉴权插件
- emq_auth_ldap LDAP 认证/鉴权插件
- emq_auth_http HTTP 认证/鉴权插件
- emq_auth_mysql MySQ L认证/鉴权插件
- emq_auth_pgsql Postgre 认证/鉴权插件
- emq_auth_redis Redis 认证/鉴权插件
- emq_auth_mongo MongoDB 认证/鉴权插件
HTTP 插件认证
etc/plugins/emq_auth_http.conf 配置 ‘super_req’, ‘auth_req’:
##Variables: %u = username, %c = clientid, %a = ipaddress, %P = password, %t = topic
auth.http.auth_req = http://127.0.0.1:8080/mqtt/auth
auth.http.auth_req.method = post
auth.http.auth_req.params = clientid=%c,username=%u,password=%P
auth.http.super_req = http://127.0.0.1:8080/mqtt/superuser
auth.http.super_req.method = post
auth.http.super_req.params = clientid=%c,username=%u
启用 HTTP 认证插件:
./bin/emqttd_ctl plugins load emq_auth_http
认证API
@RestController
@RequestMapping("/mqtt")
public class MqttUserCheckController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping(value = "/auth", method = RequestMethod.POST)
public void checkUser(String clientid, String username, String password, HttpServletResponse response) {
logger.info("普通用户;clientid:" + clientid + ";username:" + username + ";password:" + password);
if (checkUser(clientid, username, password)) {
response.setStatus(200);
} else {
response.setStatus(401);
}
}
}
$SYS-系统主题
EMQ 消息服务器周期性发布自身运行状态、MQTT 协议统计、客户端上下线状态到 $SYS/ 开头系统主题。
服务器版本、启动时间与描述消息
- $SYS/brokers 集群节点列表
- S Y S / b r o k e r s / SYS/brokers/ SYS/brokers/{node}/version EMQ 服务器版本
- S Y S / b r o k e r s / SYS/brokers/ SYS/brokers/{node}/uptime EMQ 服务器启动时间
- S Y S / b r o k e r s / SYS/brokers/ SYS/brokers/{node}/datetime EMQ 服务器时间
- S Y S / b r o k e r s / SYS/brokers/ SYS/brokers/{node}/sysdescr EMQ 服务器描述
MQTT 客户端上下线状态消息:$SYS 主题前缀:
S
Y
S
/
b
r
o
k
e
r
s
/
SYS/brokers/
SYS/brokers/{node}/clients/
主题:${clientid}/connected
‘connected’ 消息 JSON 数据:
{ipaddress: “127.0.0.1”, username: “test”, session: false, protocol: 3, connack: 0, ts: 1432648482}
‘disconnected’ 消息 JSON 数据:
{reason: normal, ts: 1432648486}
MQTT消息QoS
MQTT发布消息QoS保证不是端到端的,是客户端与服务器之间的。订阅者收到MQTT消息的QoS级别,最终取决于发布消息的QoS和主题订阅的QoS。
发布消息的QoS | 主题订阅的QoS | 接收消息的QoS |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
0 | 2 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
1 | 2 | 1 |
2 | 0 | 0 |
2 | 1 | 1 |
2 | 2 | 2 |
MQTT协议相关
MQTT会话(Clean Session)
MQTT客户端向服务器发起CONNECT请求时,可以通过’Clean Session’标志设置会话。
‘Clean Session’设置为0,表示创建一个持久会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。
‘Clean Session’设置为1,表示创建一个新的临时会话,在客户端断开时,会话自动销毁。
MQTT连接保活心跳
MQTT客户端向服务器发起CONNECT请求时,通过KeepAlive参数设置保活周期。
客户端在无报文发送时,按KeepAlive周期定时发送2字节的PINGREQ心跳报文,服务端收到PINGREQ报文后,回复2字节的PINGRESP报文。
服务端在1.5个心跳周期内,既没有收到客户端发布订阅报文,也没有收到PINGREQ心跳报文时,主动心跳超时断开客户端TCP连接。
emqttd消息服务器默认按最长2.5心跳周期超时设计。
例子
发送消息
public class MqttSendDemo {
public void mqttPublish(){
//设置clientid的保存形式,默认为以内存保存
MemoryPersistence persistence = new MemoryPersistence();
try {
final MqttClient sampleClient = new MqttClient(ConfigInfo.mBorker, ConfigInfo.mMQTTClientId, persistence);
//定义Mqtt参数
final MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setUserName(ConfigInfo.mUserName);
connOpts.setPassword(ConfigInfo.mPassWord.toCharArray());
//设置客户端连接的serverURI列表
connOpts.setServerURIs(new String[] { ConfigInfo.mBorker });
//设置客户端和服务器是否应记住重新启动和重新连接的状态。
connOpts.setCleanSession(true);
//设置心跳间隔。
connOpts.setKeepAliveInterval(90);
//设置客户端是否会在连接丢失时自动尝试重新连接到服务器。
connOpts.setAutomaticReconnect(true);
sampleClient.setCallback(new MqttCallbackExtended(){
//当与服务器的连接丢失时,将调用此方法。
@Override
public void connectionLost(Throwable throwable) {}
//当消息从服务器到达时,将调用此方法。
@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {}
//当消息传递完成时调用,并且已收到所有确认。
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {}
//成功完成与服务器的连接时调用。
@Override
public void connectComplete(boolean b, String s) {}
});
//连接,并设置使用参数
sampleClient.connect(connOpts);
//发送消息
MqttMessage message = new MqttMessage("将要发送的消息".getBytes());
message.setQos(0);
boolean connect = sampleClient.isConnected();
if(connect){
//设置Topic
String strTopic = "app/data/xxx";
try{
//发送消息
sampleClient.publish(strTopic, message);
}catch (Exception e){
e.printStackTrace();
}
}
} catch (MqttException e) {
e.printStackTrace();
}
}
}
接收消息
public class MqttRceiveDemo {
public void mqttSubscribe(){
//设置clientid的保存形式,默认为以内存保存
MemoryPersistence persistence = new MemoryPersistence();
try {
final MqttClient sampleClient = new MqttClient(ConfigInfo.mBorker, ConfigInfo.mMQTTClientId, persistence);
//定义Mqtt参数
final MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setUserName(ConfigInfo.mUserName);
connOpts.setPassword(ConfigInfo.mPassWord.toCharArray());
//设置客户端连接的serverURI列表
connOpts.setServerURIs(new String[] { ConfigInfo.mBorker });
//设置客户端和服务器是否应记住重新启动和重新连接的状态。
connOpts.setCleanSession(true);
//设置心跳间隔。
connOpts.setKeepAliveInterval(90);
//设置客户端是否会在连接丢失时自动尝试重新连接到服务器。
connOpts.setAutomaticReconnect(true);
sampleClient.setCallback(new MqttCallbackExtended(){
//当与服务器的连接丢失时,将调用此方法。
@Override
public void connectionLost(Throwable throwable) {}
//当消息从服务器到达时,将调用此方法。
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
// subscribe后得到的消息会执行到这里面
System.out.println("接收消息主题 : " + topic);
System.out.println("接收消息Qos : " + message.getQos());
System.out.println("接收消息内容 : " + new String(message.getPayload()));
}
//当消息传递完成时调用,并且已收到所有确认。
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {}
//成功完成与服务器的连接时调用。reconnect -如果为true,则表示连接是自动重新连接的结果。
@Override
public void connectComplete(boolean reconnect, String serverURI) {
//订阅Topic
sampleClient.subscribe(ConfigInfo.mEMQTTTopic, ConfigInfo.mEMQTTQos);
}
});
//连接,并设置使用参数
sampleClient.connect(connOpts);
} catch (MqttException e) {
e.printStackTrace();
}
}
}