目录
1、Dashboard
EMQX 提供了 Dashboard 以方便用户管理设备与监控相关指标。通过 Dashboard可以查看服务器基本信息、负载情况和统计数据,可以查看某个客户端的连接状态等信息甚至断开其连接,也可以动态加载和卸载指定插件。除此之外,EMQ X Dashboard 还提供了规则引擎的可视化操作界面,同时集成了一个简易的 MQTT 客户端工具供用户测试使用。
EMQX Dashboard 功能由 emqx-dashboard 插件实现,该插件默认处于启用状态,它将在 EMQX 启动时自 动加载。如果你希望禁用 Dashboard 功能,你可以将 data/loaded_plugins 中的 {emqx_dashboard, true} 修改为 {emqx_dashboard, false} 。
1.1 查看和配置Dashboard
EMQ X Dashboard 是一个 Web 应用程序,你可以直接通过浏览器来访问它,无需安装任何其他软件。 当 EMQX 成功运行在你的本地计算机上且 EMQX Dashboard 被默认启用时,通过访问 http://localhost:18083 来查看Dashboard,默认用户名是 admin ,密码是 public 。
如果EMQX是基于docker容器部署的,可以在容器中的 etc/plugins/emqx_dashboard.conf 中查看或修改 EMQ X Dashboard 的配置。 EMQ X Dashboard 配置项可以分为默认用户与监听器两个部分:
默认用户
EMQ X Dashboard 可以配置多个用户,但在配置文件中仅支持配置默认用户。 需要注意的是,一旦您通过 Dashboard 修改了默认用户的密码,则默认用户的相关信息将以您在 Dashboard 上的最新改动为准,配置文件中的默认用户配置将被忽略。
监听器
EMQ X Dashboard 支持 HTTP 和 HTTPS 两种 Listener,但默认只启用了监听端口为 18083 的 HTTP Listener。
1.2 Dashboard界面
为了使用户在操作和浏览中可以快速地定位和切换当前位置,EMQ X Dashboard 采用了侧边导航的模式,默 认情况下 Dashboard 包含以下一级导航项目:
最新版本EMQ X Broker的Dashboard界面布局略有不同,增加了些导航,但基本都差不多
1.2.1 ADMIN
Users
您可以在 Users 页面查看和管理能够访问和操作 Dashboard 的用户:
Settings
目前 EMQ X Dashboard 仅支持修改主题和语言两种设置:
1.2.2 MONITORING
EMQ X Dashboard 提供了非常丰富的数据监控项目,完整地覆盖了服务端与客户端,这些信息都将在MONITORING 下的页面中被合理地展示给用户。
Overview
Overview 作为 Dashboard 的默认展示页面,提供了 EMQ X 当前节点的详细信息和集群其他节点的关键信息,以帮助用户快速掌握每个节点的状态。
Clients
Clients 页面提供了连接到指定节点的客户端列表,同时支持通过 Client ID 直接搜索客户端。除了查看客户端的基本信息,您还可以点击每条记录右侧的 Kick Out 按钮踢掉该客户端,注意此操作将断开客户端连接并终结其会话。
Clients 页面使用快照的方式来展示客户端列表,因此当客户端状态发生变化时页面并不会自动刷新,需要您手动刷新浏览器来获取最新客户端数据。
如果你无法在客户端列表获取到你需要的信息,你可以单击 Client ID 来查看客户端的详细信息。
我们将客户端详情中的各个字段分为了 连接,会话 和 指标 三类,以下为各字段的说明:
连接
会话
指标
在客户端详情的 Subscriptions 标签页中,您可以查看当前客户端的订阅信息,以及新建或取消订阅:
Topics
展示系统所有的Topic情况
Subscriptions
Subscriptions 页面提供了指定节点下的所有订阅信息,并且支持用户通过 Client ID 查询指定客户端的所有订阅。
1.2.3 RULE ENGINE
用 EMQ X 的规则引擎可以灵活地处理消息和事件,例如将消息转换成指定格式后存入数据库表或者重新发送到消息队列等等。为了方便用户更好地使用规则引擎,EMQ X Dashboard 提供了相应的可视化操作页面,您可以 点击 RULE ENGINE 导航项目来访问这些页面。
鉴于规则引擎的相关概念比较复杂, 涉及到的操作可能会占据相当大的篇幅,后面会单独写一篇博客来介绍。
1.2.4 MANAGEMENT
目前 EMQ X Dashboard 的 MANAGEMENT 导航项目下主要包括扩展插件 的监控管理页面和用于 HTTP API 认证的 AppID 与 AppSerect 的管理页面。
Plugins
Plugins 页面列举了 EMQ X 能够发现的所有插件,包括 EMQ X 官方插件与您遵循 EMQ X 官方标准自行开发的插件,您可以在此页面查看插件当前的运行状态以及随时启停插件。
可以看到,除了emqx-dashboard以外, EMQ X 还将默认启动 emqx-rule-engine等4个插件。
Applications
Applications 页面列举了当前已创建的应用,您可以在此页面进行诸如创建应用、临时禁用或启动某个应用的访问权限等操作。EMQ X 会创建一个 AppID 为 admin ,AppSecret 为 publish 的默认应用方便用户首次访问:
您可以点击 Application 页面右上角的 New App 按钮来创建一个新的应用,其中 AppID 与 AppSecret 是必 选项。创建完成后您可以点击 View 按钮来查看应用详情,AppSecret 也会在详情中显示。以下是相关字段的说明:
1.2.5 TOOLS
目前 EMQ X Dashboard 的 TOOLS 导航项目下主要包括 WebSocket 客户端工具页面以及 HTTP API 速查页面。
Websocket
Websocket 页面为您提供了一个简易但有效的 WebSocket 客户端工具,它包含了连接、订阅和发布功能,同时还能查看自己发送和接收的报文数据,我们期望它可以帮助您快速地完成某些场景或功能的测试验证:
HTTP API
HTTP API 页面列举了 EMQ X 目前支持的所有 HTTP API 及其说明:
2、认证
2.1 简介
身份认证是大多数应用的重要组成部分,MQTT 协议支持用户名密码认证,启用身份认证能有效阻止非法客户端的连接。
EMQ X 中的认证指的是当一个客户端连接到 EMQ X 的时候,通过服务器端的配置来控制客户端连接服务器的权限。
EMQ X 的认证支持包括两个层面:
-
MQTT 协议本身在 CONNECT 报文中指定用户名和密码,EMQ X 以插件形式支持基于 Username、 ClientID、HTTP、JWT、LDAP 及各类数据库如 MongoDB、MySQL、PostgreSQL、Redis 等多种形式的认证。
-
在传输层上,TLS 可以保证使用客户端证书的客户端到服务器的身份验证,并确保服务器向客户端验证服务器证书。也支持基于 PSK 的 TLS/DTLS 认证。
2.1.1 认证方式
EMQ X 支持使用内置数据源(文件、内置数据库)、JWT、外部主流数据库和自定义 HTTP API 作为身份认证数据源。
连接数据源、进行认证逻辑通过插件实现的,每个插件对应一种认证方式,使用前需要启用相应的插件。
客户端连接时插件通过检查其 username/clientid 和 password 是否与指定数据源的信息一致来实现对客户端的身份认证。
EMQ X 支持的认证方式:
内置数据源
Username 认证
Cliend ID 认证
使用配置文件与 EMQ X 内置数据库提供认证数据源,通过 HTTP API 进行管理,足够简单轻量。
外部数据库
LDAP 认证
MySQL 认证
PostgreSQL 认证
Redis 认证
MongoDB 认证
外部数据库可以存储大量数据,同时方便与外部设备管理系统集成。
其他
HTTP 认证
JWT 认证
JWT 认证可以批量签发认证信息,HTTP 认证能够实现复杂的认证鉴权逻辑。
更改插件配置后需要重启插件才能生效,部分认证鉴权插件包含 ACL 功能
认证结果
任何一种认证方式最终都会返回一个结果:
认证成功:经过比对客户端认证成功
认证失败:经过比对客户端认证失败,数据源中密码与当前密码不一致忽略认证(ignore):当前认证方式中未查找到认证数据,无法显式判断结果是成功还是失败,交由认证链下一认证方式或匿名认证来判断
匿名认证
EMQ X 默认配置中启用了匿名认证,任何客户端都能接入 EMQ X。没有启用认证插件或认证插件没有显式允许/拒绝(ignore)连接请求时,EMQ X 将根据匿名认证启用情况决定是否允许客户端连接。
配置匿名认证开关:
# 进入 etc/emqx.conf
## Value: true | false
allow_anonymous = true
生产环境中请禁用匿名认证。
注意:我们需要进入到容器内部修改该配置,然后重启EMQ X服务
密码加盐规则与哈希方法
EMQ X 多数认证插件中可以启用哈希方法,数据源中仅保存密码密文,保证数据安全。
启用哈希方法时,用户可以为每个客户端都指定一个 salt(盐)并配置加盐规则,数据库中存储的密码是按照。加盐规则与哈希方法处理后的密文。
以 MySQL 认证为例:
加盐规则与哈希方法配置:
# 进入etc/plugins/emqx_auth_mysql.conf
## 不加盐,仅做哈希处理
auth.mysql.password_hash = sha256
## salt 前缀:使用 sha256 加密 salt + 密码 拼接的字符串 auth.mysql.password_hash = salt,sha256
## salt 后缀:使用 sha256 加密 密码 + salt 拼接的字符串 auth.mysql.password_hash = sha256,salt
## pbkdf2 with macfun iterations dklen
## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
## auth.mysql.password_hash = pbkdf2,sha256,1000,20
如何生成认证信息
-
为每个客户端分用户名、Client ID、密码以及 salt(盐)等信息
-
使用与 MySQL 认证相同加盐规则与哈希方法处理客户端信息得到密文
-
将客户端信息写入数据库,客户端的密码应当为密文信息
2.1.2 EMQX身份认证流程
根据配置的认证 SQL 结合客户端传入的信息,查询出密码(密文)和 salt(盐)等认证数据,没有查询结果时,认证将终止并返回 ignore 结果 。
根据配置的加盐规则与哈希方法计算得到密文,没有启用哈希方法则跳过此步 。
将数据库中存储的密文与当前客户端计算的到的密文进行比对,比对成功则认证通过,否则认证失败 。
写入数据的加盐规则、哈希方法与对应插件的配置一致时认证才能正常进行。更改哈希方法会造成现有认证数据失效。
认证链
当同时启用多个认证方式时,EMQ X 将按照插件开启先后顺序进行链式认证:
-
一旦认证成功,终止认证链并允许客户端接入
-
一旦认证失败,终止认证链并禁止客户端接入
-
直到最后一个认证方式仍未通过,根据匿名认证配置判定
-
匿名认证开启时,允许客户端接入
-
匿名认证关闭时,禁止客户端接入
-
同时只启用一个认证插件可以提高客户端身份认证效率。
2.2 Username 认证
Username 认证使用配置文件预设客户端用户名与密码,支持通过 HTTP API 管理认证数据。
Username 认证不依赖外部数据源,使用上足够简单轻量。使用这种认证方式前需要开启插件,我们可以在Dashboard里找到这个插件并开启。
插件:
emqx_auth_username
哈希方法
Username 认证默认使用 sha256 进行密码哈希加密,可在 etc/plugins/emqx_auth_username.conf 中更改:
配置哈希方法后,新增的预设认证数据与通过 HTTP API 添加的认证数据将以哈希密文存储在 EMQ X 内置数据库中。
2.2.1 预设认证数据
可以通过配置文件预设认证数据,编辑配置文件:etc/plugins/emqx_auth_username.conf
插件启动时将读取预设认证数据并加载到 EMQ X 内置数据库中,节点上的认证数据会在此阶段同步至集群中。
预设认证数据在配置文件中使用了明文密码,出于安全性与可维护性考虑应当避免使用该功能。
2.2.2 HTTP API 管理认证数据
EMQ X提供了对应的HTTP API用以维护内置数据源中的认证信息,我们可以添加/查看/取消/更改认证数据 。
我们通过VSCode来访问EMQ X的API /auth_username 完成认证数据的相关操作
打开VSCode下载插件
接下来我们就可以区VSCode去编写代码。
创建一个html文件
1、查看已有认证用户数据: GET api/v4/auth_username
@hostname = 192.168.200.129
@port=18083
@contentType=application/json
@userName=admin
@password=public
#############查看已有用户认证数据##############
GET http://{{hostname}}:{{port}}/api/v4/auth_username HTTP/1.1 Content-Type: {{contentType}}
Authorization: Basic {{userName}}:{{password}}
返回
2、添加认证数据API 定义: POST api/v4/auth_username{ "username": "emqx_u", "password": "emqx_p"}
########添加用户认证数据##############
POST http://{{hostname}}:{{port}}/api/v4/auth_username HTTP/1.1
Content-Type: {{contentType}}
Authorization: Basic {{userName}}:{{password}}
{ "username": "user", "password": "123456" }
返回
3、更改指定用户名的密码API 定义: PUT api/v4/auth_username/${username}{ "password": "emqx_new_p"}
指定用户名,传递新密码进行更改,再次连接时需要使用新密码进行连接:
###########更改指定用户名的密码#############
PUT http://{{hostname}}:{{port}}/api/v4/auth_username/user HTTP/1.1
Content-Type: {{contentType}}
Authorization: Basic {{userName}}:{{password}}
{ "password": "user" }
返回
4、查看指定用户名信息API 定义: GET api/v4/auth_username/${username}
指定用户名,查看相关用户名、密码信息,注意此处返回的密码是使用配置文件指定哈希方式加密后的密码:
###########查看指定用户名信息#############
GET http://{{hostname}}:{{port}}/api/v4/auth_username/user HTTP/1.1
Content-Type: {{contentType}}
Authorization: Basic {{userName}}:{{password}}
返回
5:删除认证数据API 定义: DELETE api/v4/auth_username/${username}
用以删除指定认证数据
###########删除指定的用户信息#############
DELETE http://{{hostname}}:{{port}}/api/v4/auth_username/user HTTP/1.1
Content-Type: {{contentType}}
Authorization: Basic {{userName}}:{{password}}
返回
2.2.3 MQTTX客户端验证
使用mqtt客户端工具验证使用username连接登录的功能。从 MQTT X: Cross-platform MQTT 5.0 Desktop Client 这个地址下载对应操作系统的mqtt客户端工具。
1、新建连接,参数配置如下
在对应的输入框内输入username和password,clientId这里目前可以随便输入(因为基于clientId的认证功能还没有启用),之后点连接,连接成功。用户名和密码如果输入错误的话是连接不成功的。
2、再次创建一个客户端连接,可作为消息的订阅者,上一个连接作为发布者,如下
3、订阅者添加订阅
订阅完成后
4、上一个客户端连接作为消息的发布者来进行消息的发布
5、查看订阅者是否已经接收到消息
2.3 Client ID 认证
Client ID 认证使用配置文件预设客户端Client ID 与密码,支持通过 HTTP API 管理认证数据。
Client ID 认证不依赖外部数据源,使用上足够简单轻量,使用该种认证方式时需要开启 emqx_auth_clientid 插件,直接在DashBoard中开启即可。
哈希方法
Client ID 认证默认使用 sha256 进行密码哈希加密,可在etc/plugins/emqx_auth_clientid.conf 中更改:
配置哈希方法后,新增的预设认证数据与通过 HTTP API 添加的认证数据将以哈希密文存储在 EMQ X 内置数据库中。
2.3.1 预设认证数据
可以通过配置文件预设认证数据,编辑配置文件: etc/plugins/emqx_auth_clientid.conf
插件启动时将读取预设认证数据并加载到 EMQ X 内置数据库中,节点上的认证数据会在此阶段同步至集群中。
预设认证数据在配置文件中使用了明文密码,出于安全性与可维护性考虑应当避免使用该功能。
2.3.2 HTTP API 管理认证数据
我们使用VSCode来通过EMQ X的API来添加和查看Client ID的认证数据。
1、添加认证数据API 定义: POST api/v4/auth_clientid{ "clientid": "emqx_c", "password": "emqx_p"}
####添加clientId和密码#####
POST http://{{hostname}}:{{port}}/api/v4/auth_clientid HTTP/1.1
Content-Type: {{contentType}}
Authorization: Basic {{userName}}:{{password}}
{ "clientid": "emq-client1", "password": "123456" }
返回
2、查看已经添加的认证数据API 定义: GET api/v4/auth_clientid
#############获取所有详细信息########
GET http://{{hostname}}:{{port}}/api/v4/auth_clientid HTTP/1.1
Content-Type: {{contentType}}
Authorization: Basic {{userName}}:{{password}}
返回
3:更改指定 Client ID 的密码API 定义: PUT api/v4/auth_clientid/${clientid}{ "password": "emqx_new_p"} 指定 Client ID,传递新密码进行更改,再次连接时需要使用新密码进行连接:
#############更改指定 Client ID 的密码########
PUT http://{{hostname}}:{{port}}/api/v4/auth_clientid/emq-client1 HTTP/1.1
Content-Type: {{contentType}}
Authorization: Basic {{userName}}:{{password}}
{ "password": "654321" }
返回
2.3.3 MQTTX客户端验证
使用mqtt客户端工具验证使用Client ID连接登录的功能
此时用户名字段需要输入一个,但是可以随便填写!
2.4 HTTP认证
HTTP 认证使用外部自建 HTTP 应用认证数据源,根据 HTTP API 返回的数据判定认证结果,能够实现复杂的认证鉴权逻辑。启用该功能需要将 emqx_auth_http 插件启用,并且修改该插件的配置文件,在里面指定HTTP认证接口的url。 emqx_auth_http 插件同时还包含了ACL的功能,我们暂时还用不上,通过注释将其禁用。
1:在Dashboard中中开启 emqx_auth_http 插件,同时为了避免误判我们可以停止通过username,clientID 进行认证的插件 emqx_auth_clientid , emqx_auth_username
2.4.1 认证原理
EMQ X 在设备连接事件中使用当前客户端相关信息作为参数,向用户自定义的认证服务发起请求查询权限, 通过返回的 HTTP 响应状态码 (HTTP statusCode) 来处理认证请求。
认证失败:API 返回 4xx 状态码
认证成功:API 返回 200 状态码
忽略认证:API 返回 200 状态码且消息体 ignore
2.4.2 HTTP 请求信息
HTTP API 基础请求信息,配置证书、请求头与重试规则。
加盐规则与哈希方法
HTTP 在请求中传递明文密码,加盐规则与哈希方法取决于 HTTP 应用。
2.4.3 认证请求
进行身份认证时,EMQ X 将使用当前客户端信息填充并发起用户配置的认证查询请求,查询出该客户端在 HTTP 服务器端的认证数据。
打开etc/plugins/emqx_auth_http.conf配置文件,通过修改如下内容:修改完成后需要重启EMQX服务 。
HTTP 请求方法为 GET 时,请求参数将以 URL 查询字符串的形式传递;POST、PUT 请求则将请求参数以普通表单形式提交(content-type 为 x-www-form-urlencoded)。
你可以在认证请求中使用以下占位符,请求时 EMQ X 将自动填充为客户端信息:
%u:用户名
%c:Client ID
%a:客户端 IP 地址
%r:客户端接入协议
%P:明文密码
%p:客户端端口
%C:TLS 证书公用名(证书的域名或子域名),仅当 TLS 连接时有效
%d:TLS 证书 subject,仅当 TLS 连接时有效
推荐使用 POST 与 PUT 方法,使用 GET 方法时明文密码可能会随 URL 被记录到传输过程中的服务器日志中。
2.4.4 认证服务开发
创建基于springboot的应用程序: emqx-demo
1、pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jie</groupId>
<artifactId>emqx-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>emqx-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2、创建application.yml配置文件并配置
server:
port: 8991
spring:
application:
name: emqx-demo
3、创建Controller:com.jie.emqxdemo.mqtt.AuthController;编写如下
package com.jie.emqxdemo.controller.mqtt;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
/**
* @description:接受认证请求
* @author: jie
* @time: 2022/3/29 20:57
*/
@RestController
@RequestMapping("/mqtt")
@Slf4j
public class AuthController {
/**
* 用于存储数据 实际开发应该是存储在数据库中
*/
private Map<String, String> users;
@PostConstruct
public void init() {
//实际的密码应该是密文,mqtt的http认证组件传输过来的密码是明文,我们需要自己进行加密验证
users = new HashMap<>();
users.put("user", "123456");
users.put("emq-client2", "123456");
users.put("emq-client3", "123456");
}
/**
* @description:接受消息 如果属性名对应不上可以用@RequestParam注解做映射
* @author: jie
* @time: 2022/3/29 21:03
*/
@PostMapping("/auth")
public ResponseEntity<?> auth(@RequestParam("clientid") String clientid,
@RequestParam("username") String username,
@RequestParam("password") String password) {
log.info("emqx认证组件调用自定义的认证服务开始认证,clientid={},username={},password= {}", clientid, username, password);
//在此处可以进行复杂也的认证逻辑,但是我们为了演示方便做一个固定操作
String value = users.get(username);
if (StringUtils.isEmpty(value)) {
return new ResponseEntity<Object>(HttpStatus.UNAUTHORIZED);
}
if (!value.equals(password)) {
return new ResponseEntity<Object>(HttpStatus.UNAUTHORIZED);
}
return new ResponseEntity<Object>(HttpStatus.OK);
}
}
2.4.5 MQTTX客户端验证
使用MQTTX客户端工具连接EMQX服务器,如下
我这里不知道为什么一直连接不上,往哪位大佬在评论区指点一二,谢谢!
这个地方的Client-ID随便输入,因为在验证的代码里没有对该字段做校验,之后点连接,发现会连接成功,然后可以去自定义的认证服务中查看控制台输出,证明基于外部的http验证接口生效了。在实际项目开发过程中,HTTP接口校验的代码不会这么简单,账号和密码之类的数据肯定会存在后端数据库中,代码会通过传入的数据和数据库中的数据做校验,如果成功才会校验成功,否则校验失败。
当然EMQ X除了支持我们之前讲过的几种认证方式外,还支持其他的认证方式,比如:MySQL认证、PostgreSQL认证、Redis认证、MongoDB认证,对于其他这些认证方式只需要开启对应的EMQ X插件并且配置对应的配置文件,将对应的数据保存到相应的数据源即可。
3、客户端SDK
在实际项目中我们要针对接MQTT消息代理服务端,从而向其发布消息、订阅消息等来完成我们自己的业务逻辑的开发。EMQ X针对不同的客户端语言都提供了不同的SDK工具包,可以在官网上查看并下下载:
3.1 Eclipse Paho Java
3.1.1 Paho介绍
Paho Java客户端是用Java编写的MQTT客户端库,用于开发在JVM或其他Java兼容平台(例如Android)上运行的应用程序。
Paho不仅可以对接EMQ X Broker,还可以对接满足符合MQTT协议规范的消息代理服务端,目前Paho可以支持到MQTT5.0以下版本。MQTT3.3.1协议版本基本能满足百分之九十多的接入场景。
Paho Java客户端提供了两个API:
MqttAsyncClient提供了一个完全异步的API,其中活动的完成是通过注册的回调通知的。
MqttClient是MqttAsyncClient周围的同步包装器,在这里,功能似乎与应用程序同步。
3.1.2 Paho实现消息收发
1、找到项目:emqx-demo,添加maven依赖。
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
2、编写用于加载yml自定义配置的配置类的代码:com.jie.emqxdemo.properties.Mqttproperties
package com.jie.emqxdemo.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @description:用于加载yml自定义配置的配置类
* @author: jie
* @time: 2022/3/31 21:12
*/
@Configuration
@ConfigurationProperties(prefix = "mqtt")
@Data
public class Mqttproperties {
private String brokerUrl;
private String clientId;
private String username;
private String password;
@Override
public String toString() {
return "Mqttproperties{" +
"brokerUrl='" + brokerUrl + '\'' +
", clientId='" + clientId + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
3、编写消息回调的代码:com.jie.emqxdemo.client.MessageCallback
package com.jie.emqxdemo.client;
import jdk.nashorn.internal.parser.Token;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.stereotype.Component;
/**
* @description:消息回调
* @author: jie
* @time: 2022/3/31 22:12
*/
@Slf4j
@Component
public class MessageCallback implements MqttCallback {
/**
* @description:丢失对服务端的连接后触发该方法回调,此处可以做一些特殊处理,比如重连
* @author: jie
* @time: 2022/3/31 22:14
*/
@Override
public void connectionLost(Throwable throwable) {
log.info("丢失了对服务的连接");
}
/**
* @description:应用收到消息后出发的回调
* 该方法由mqtt客户端同步调用,在此方法未正确返回之前,不会发送ack确认消息到broker
* 一旦该方法向外抛出了异常客户端将异常关闭,当再次连接时;所有QoS1,QoS2且客户端未进行ack确认的消息都将由 broker服务器再次发送到客户端
* @author: jie
* @time: 2022/3/31 22:15
*/
@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
log.info("订阅到了消息;topic={},messageid={},qos={},msg={}",
s,
mqttMessage.getId(),
mqttMessage.getQos(),
new String(mqttMessage.getPayload()));
}
/**
* @description:消息发布完成产生的回调
* @author: jie
* @time: 2022/3/31 22:18
*/
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
int messageId = iMqttDeliveryToken.getMessageId();
String[] topics = iMqttDeliveryToken.getTopics();
log.info("消息发送完成,messageId={},topics={}",messageId,topics);
}
}
4、编写QOS枚举类:com.jie.emqxdemo.enums.QosEnum
package com.jie.emqxdemo.enums;
/**
* @description:Qos枚举列
* @author: jie
* @time: 2022/3/31 21:54
*/
public enum QosEnum {
QOS0(0),QOS1(1),QOS2(2);
private final int value;
QosEnum(int value){
this.value = value;
}
/**
* @description:获取枚举值
* @author: jie
* @time: 2022/3/31 21:56
*/
public int value(){
return this.value;
}
}
5、在application.yml中添加自定义的配置:
mqtt:
broker-url: tcp://192.168.58.149:1883
client-id: emq-client
username: user
password: 123456
6、客户端封装类:com.jie.emqxdemo.client.
package com.jie.emqxdemo.client;
import com.jie.emqxdemo.enums.QosEnum;
import com.jie.emqxdemo.properties.Mqttproperties;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
/**
* @description:客户端封装类
* @author: jie
* @time: 2022/3/31 21:02
*/
@Component
@Slf4j
public class EmqClient {
private IMqttClient iMqttClient;
/**
* @description:将配置类注入进来
*/
@Autowired
private Mqttproperties mqttproperties;
@Autowired
private MqttCallback mqttCallback;
/**
* @description:初始化
* @author: jie
* @time: 2022/3/31 21:22
*/
@PostConstruct
public void init(){
//持久化机制 持久化到内存 MqttDefaultFilePersistence(持久化到本地的文件系统中)
MqttClientPersistence mqttClientPersistence = new MemoryPersistence();
try {
iMqttClient = new MqttClient(mqttproperties.getBrokerUrl(),mqttproperties.getClientId(),mqttClientPersistence);
} catch (MqttException e) {
log.error("初始化客户端mqttClient对象失败,errormsg={} brokerurl={},clientId= {}",e.getMessage(), mqttproperties.getBrokerUrl(),mqttproperties.getClientId());
}
}
/**
* @description:连接服务端方法
* @author: jie
* @time: 2022/3/31 21:33
*/
public void connect(String username,String password){
//连接选项对象
MqttConnectOptions options = new MqttConnectOptions();
//自动重连
options.setAutomaticReconnect(true);
options.setUserName(username);
options.setPassword(password.toCharArray());
//临时的会话
options.setCleanSession(true);
//方法回调
iMqttClient.setCallback(mqttCallback);
try {
iMqttClient.connect(options);
} catch (MqttException e) {
log.error("连接mqtt broker失败,失败原因:{}",e.getMessage());
}
}
/**
* @description:断开连接
* @author: jie
* @time: 2022/3/31 21:48
*/
@PreDestroy
public void disConnect(){
try {
iMqttClient.disconnect();
} catch (MqttException e) {
log.error("断开连接产生的异常,异常信息{}",e.getMessage());
}
}
/**
* @description:重新连接
* @author: jie
* @time: 2022/3/31 21:51
*/
public void reConnect(){
try {
iMqttClient.reconnect();
} catch (MqttException e) {
log.error("重新连接失败,失原因:{}",e.getMessage());
}
}
/**
* @description:发布消息 topic:主题 msg:消息 qos:qos retain:是否是保留消息
* @author: jie
*/
public void publish(String topic, String msg, QosEnum qos,boolean retain){
MqttMessage mqttMessage = new MqttMessage();
//设置消息体
mqttMessage.setPayload(msg.getBytes(StandardCharsets.UTF_8));
//qos
mqttMessage.setQos(qos.value());
try {
iMqttClient.publish(topic,mqttMessage);
} catch (MqttException e) {
log.error("发布消息失败,失败原因:{},topic={},msg={},qos={},retain={}",e.getMessage(),topic,msg,qos,retain);
}
}
/**
* @description:订阅消息 topicFilter:订阅的主题 qos :qos
* @author: jie
* @time: 2022/3/31 22:04
*/
public void subscribe(String topicFilter,QosEnum qos){
//订阅主题
try {
iMqttClient.subscribe(topicFilter,qos.value());
} catch (MqttException e) {
log.error("订阅主题失败,errormsg={},topicFilter:{},qos:{}",e.getMessage(),topicFilter,qos);
}
}
/**
* @description:取消订阅
* @author: jie
* @time: 2022/3/31 22:08
*/
public void unSubscribe(String topicFilter){
try {
iMqttClient.unsubscribe(topicFilter);
} catch (MqttException e) {
log.error("取消订阅失败,errormsg={},topicFilter={}",e.getMessage(),topicFilter);
}
}
}
7、编写消息发布和订阅的测试,在启动类中添加如下代码(单纯偷懒行为)
package com.jie.emqxdemo;
import com.jie.emqxdemo.client.EmqClient;
import com.jie.emqxdemo.enums.QosEnum;
import com.jie.emqxdemo.properties.Mqttproperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
public class EmqxDemoApplication {
public static void main(String[] args) {
SpringApplication.run(EmqxDemoApplication.class, args);
}
@Autowired
private EmqClient emqClient;
@Autowired
private Mqttproperties mqttproperties;
@PostConstruct
public void init(){
//连接服务端
emqClient.connect(mqttproperties.getUsername(),mqttproperties.getPassword());
//订阅一个主题
emqClient.subscribe("testtopic/#", QosEnum.QOS2);
//开启一个新的线程,每隔五秒去向 testtopic/123
new Thread(()->{
while (true){
emqClient.publish("testtopic/123","mqtt msg:"+ LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME),QosEnum.QOS2,false);
try {
TimeUnit.SECONDS.sleep(5);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
测试:
在Dashboard中开启使用username进行认证的组件,其他组件停止即可,然后启动项目,查看 控制台输出即可
3.2 MQTT.js
MQTT.js是MQTT协议的客户端JS库,是用JavaScript为node.js和浏览器编写的。
GitHub项目地址: GitHub - mqttjs/MQTT.js: The MQTT client for Node.js and the browser
3.2.1 API列表
大家直接去官方文档查看吧,GitHub项目地址: GitHub - mqttjs/MQTT.js: The MQTT client for Node.js and the browser ,然后往下翻。
3.2.2 MQTT.js实现消息收发
我们写一个HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>mqtt.js测试</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js" ></script>
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js" ></script>
<style type="text/css">
div{
width: 300px;
height: 300px;
float: left;
border: red solid 1px;
}
</style>
<script type="text/javascript">
$(function (){
// 连接选项
const options = { clean: true, // 保留回话
connectTimeout: 4000, // 超时时间
// 认证信息
clientId: 'emqx_client_h5', username: 'user', password: '123456',
}
const connectUrl = "ws://192.168.58.149:8083/mqtt";
const client = mqtt.connect(connectUrl,options);
/**
* mqtt.Client相关事件
*/
//当重新连接启动触发回调
client.on('reconnect', () => {
$("#div1").text("正在重连.....");
});
//连接断开后触发的回调
client.on("close",function () {
$("#div1").text("客户端已断开连接.....");
});
//从broker接收到断开连接的数据包后发出。MQTT 5.0特性
client.on("disconnect",function (packet) {
$("#div1").text("从broker接收到断开连接的数据包....."+packet);
});
//客户端脱机下线触发回调
client.on("offline",function () {
$("#div1").text("客户端脱机下线.....");
});
//当客户端无法连接或出现错误时触发回调
client.on("error",(error) =>{
$("#div1").text("客户端出现错误....."+error);
});
//以下两个事件监听粒度细 //当客户端发送任何数据包时发出。这包括published()包以及MQTT用于管理订阅和连接的包
client.on("packetsend",(packet)=>{
$("#div1").text("客户端已发出数据包....."+packet);
});
//当客户端接收到任何数据包时发出。这包括来自订阅主题的信息包以及MQTT用于管理订阅和连接 的信息包
client.on("packetreceive",(packet)=>{
$("#div1").text("客户端已收到数据包....."+packet);
});
//成功连接后触发的回调
client.on("connect",function (connack) {
$("#div1").text("成功连接上服务器"+new Date());
//订阅某主题
/**
* client.subscribe(topic/topic array/topic object, [options], [callback])
* topic:一个string类型的topic或者一个topic数组,也可以是一个对象
* options */
client.subscribe("testtopic/#",{qos:2});
//每隔2秒发布一次数据
setInterval(publish,2000)
});
function publish() {
//发布数据
/**
* client.publish(topic,message,[options], [callback])
* message: Buffer or String
* options:{
* qos:0, //默认0
* retain:false, //默认false
* dup:false, //默认false
* properties:{}
* }
* callback:function (err){}
*/
const message = "h5 message "+Math.random()+new Date();
client.publish("testtopic/123",message,{qos:2});
$("#div2").text("客户端发布了数据:"+message);
}
//当客户端接收到发布消息时触发回调
/**
* topic:收到的数据包的topic
* message:收到的数据包的负载playload
* packet:收到的数据包 */
client.on('message', (topic, message,packet) => {
$("#div3").text("客户端收到订阅消息,topic="+topic+";消息数据:"+message+";数据 包:"+packet);
});
//页面离开自动断开连接
$(window).bind("beforeunload",()=>{
$("#div1").text("客户端窗口关闭,断开连接");
client.disconnect();
})
})
</script>
</head>
<body>
<div id="div1"></div>
<div id="div2"></div>
<div id="div3"></div>
</body>
</html>
测试:启动项目前将启动类 EmqxDemoApplication 中init方法上的注解注释掉,启动后访问如下地址查看网页端的输出。
4. 日志与追踪
4.1 控制日志输出
EMQ X 支持将日志输出到控制台或者日志文件,或者同时使用两者。可在 emqx.conf 中配置 :
log.to = both
log.to 默认值是 both,可选的值为:
offff: 完全关闭日志功能
fifile: 仅将日志输出到文件
console: 仅将日志输出到标准输出(emqx 控制台)
both: 同时将日志输出到文件和标准输出(emqx 控制台)
4.2 日志级别
EMQ X 的日志分 8 个等级, 由低到高分别为:
debug < info < notice < warning < error < critical < alert < emergency
EMQ X 的默认日志级别为 warning,可在 emqx.conf 中修改:
log.level = warning
此配置将所有 log handler 的配置设置为 warning。
4.3 日志文件和日志滚动
EMQ X 的默认日志文件目录在 ./log (zip包解压安装) 或者 /var/log/emqx (二进制包安装)。可在emqx.conf 中配置:
log.dir = log
在文件日志启用的情况下 (log.to = fifile 或 both),日志目录下会有如下几种文件:
emqx.log.N: 以 emqx.log 为前缀的文件为日志文件,包含了 EMQ X 的所有日志消息。比如 emqx.log.1 , emqx.log.2 ...
emqx.log.siz 和 emqx.log.idx: 用于记录日志滚动信息的系统文件。
run_erl.log: 以 emqx start 方式后台启动 EMQ X 时,用于记录启动信息的系统文件。
erlang.log.N: 以 erlang.log 为前缀的文件为日志文件,是以 emqx start 方式后台启动 EMQ X 时,控 制台日志的副本文件。比如 erlang.log.1 , erlang.log.2 ...
可在 emqx.conf 中修改日志文件的前缀,默认为 emqx.log :
log.file = emqx.log
EMQ X 默认在单日志文件超过 10MB 的情况下,滚动日志文件,最多可有 5 个日志文件:第 1 个日志文件为emqx.log.1,第 2 个为 emqx.log.2,并以此类推。
当最后一个日志文件也写满 10MB 的时候,将从序号最小的日志的文件开始覆盖。文件大小限制和最大日志文件个数可在 emqx.conf 中修改:
log.rotation.size = 10MB
log.rotation.count = 5
4.4 针对日志级别输出日志文件
如果想把大于或等于某个级别的日志写入到单独的文件,可以在 emqx.conf 中配置 log..file : 将 info 及info 以上的日志单独输出到 info.log.N 文件中:
log.info.file = info.log
将 error 及 error 以上的日志单独输出到 error.log.N 文件中
log.error.file = error.log
4.5 日志格式
可在 emqx.conf 中修改单个日志消息的最大字符长度,如长度超过限制则截断日志消息并用 ... 填充。默认不限制长度:
将单个日志消息的最大字符长度设置为 8192:
log.chars_limit = 8192
日志消息的格式为(各个字段之间用空格分隔)
date time level client_info module_info msg
date: 当地时间的日期。格式为:YYYY-MM-DD
time: 当地时间,精确到毫秒。格式为:hh:mm:ss.ms
level: 日志级别,使用中括号包裹。格式为:[Level]
client_info: 可选字段,仅当此日志消息与某个客户端相关时存在。其格式为:ClientId@Peername 或ClientId 或 Peername
module_info: 可选字段,仅当此日志消息与某个模块相关时存在。其格式为:[Module Info]
msg: 日志消息内容。格式任意,可包含空格。
日志消息举例
2022-04-1 16:10:03.872 [debug] <<"mqttjs_9e49354bb3">>@127.0.0.1:57105 [MQTT/WS] SEND CONNACK(Q0, R0, D0, AckFlags=0, ReasonCode=0)
此日志消息里各个字段分别为:
date: 2022-04-1
time: 16:10:03.872
level: [debug]
client_info: <<"mqttjs_9e49354bb3">>@127.0.0.1:57105
module_info: [MQTT/WS]
msg: SEND CONNACK(Q0, R0, D0, AckFlags=0, ReasonCode=0)
2022-04-1 16:10:08.474 [warning] [Alarm Handler] New Alarm: system_memory_high_watermark, Alarm Info: []
此日志消息里各个字段分别为:
date: 2020-02-18
time: 16:10:08.474
level: [warning]
module_info: [Alarm Handler]
msg: New Alarm: system_memory_high_watermark, Alarm Info: [ ]
注意此日志消息中,client_info 字段不存在。
4.6 日志级别和log handlers
EMQ X 使用了分层的日志系统,在日志级别上,包括全局日志级别 (primary log level)、以及各 log hanlder的日志级别。
[Primary Level] -- global log level and filters
/ \
[Handler 1] [Handler 2] -- log levels and filters at each handler
log handler 是负责日志处理和输出的工作进程,它由 log handler id 唯一标识,并负有如下任务:
接收什么级别的日志
如何过滤日志消息
将日志输出到什么地方
我们来看一下 emqx 默认安装的 log handlers:
emqx_ctl log handlers list
fifile: 负责输出到日志文件的 log handler。它没有设置特殊过滤条件,即所有日志消息只要级别满足要求就输出。输出目的地为日志文件。
default: 负责输出到控制台的 log handler。它没有设置特殊过滤条件,即所有日志消息只要级别满足要求就输出。输出目的地为控制台。
ssl_handler: ssl 的 log handler。它的过滤条件设置为当日志是来自 ssl 模块时输出。输出目的地为控制台。
日志消息输出前,首先检查消息是否高于 primary log level,日志消息通过检查后流入各 log handler,再检查各 handler 的日志级别,如果日志消息也高于 handler level,则由对应的 handler 执行相应的过滤条件,过滤条件通过则输出。
设想一个场景,假设 primary log level 设置为 info,log handler default (负责输出到控制台) 的级别设置为debug,log handler file (负责输出到文件) 的级别设置为 warning:
虽然 console 日志是 debug 级别,但此时 console 日志只能输出 info 以及 info 以上的消息,因为经过primary level 过滤之后,流到 default 和 fifile 的日志只剩下 info 及以上的级别;
emqx.log.N 文件里面,包含了 warning 以及 warning 以上的日志消息。
在日志级别小节中提到的 log.level 是修改了全局的日志级别。这包括 primary log level 和各个 handlers的日志级别,都设置为了同一个值。
Primary Log Level 相当于一个自来水管道系统的总开关,一旦关闭则各个分支管道都不再有水流通过。这个机制保证了日志系统的高性能运作。
4.7 运行时修改日志级别
可以使用 EMQ X 的命令行工具 emqx_ctl 在运行时修改 emqx 的日志级别:
修改全局日志级别:
例如,将 primary log level 以及所有 log handlers 的级别设置为 debug:
emqx_ctl log set-level debug
修改主日志级别:
例如,将 primary log level 设置为 debug:
emqx_ctl log primary-level debug
修改某个log handler的日志级别:
例如,将 log handler file 设置为 debug:
emqx_ctl log handlers set-level file debug
4.8 日志追踪
EMQ X 支持针对 ClientID 或 Topic 过滤日志并输出到文件。在使用日志追踪功能之前,必须将 primary log level 设置为 debug:
emqx_ctl log primary-level debug 1111
开启 ClientID 日志追踪,将所有 ClientID 为 emq-demo 的日志都输出到log/my_client.log:
emqx_ctl log primary-level debug debug
emqx_ctl trace start client emq-demo log/emq-demo.log
开启 Topic 日志追踪,将主题能匹配到 'testtopic/#' 的消息发布日志输出到 log/topic_testtopic.log:
emqx_ctl log primary-level debug debug
emqx_ctl trace start topic 'testtopic/#' log/topic_testtopic.log trace topic testtopic/# successfully 1234512345
提示:即使 emqx.conf 中, log.level 设置为 error,使用消息追踪功能仍然能够打印出某 client 或 topic 的 debug 级别的信息。这在生产环境中非常有用。