MQTT & EMQ X
mqtt协议
MQTT协议(消息队列遥测传输协议) 是基于 Publish/Subscribe 模式的一种低开销、低带宽占用的即时通讯协议。是基于TCP协议传输的;他也有UDP版本,叫做MQTT-SN。
Qos (消息服务质量)
消息服务质量 又称 可靠传输保证;他又有三种消息发布服务质量
支持 QoS0 "至多一次"传输(如果Bit 1和Bit 2都为0,表示QoS 0)
支持 QoS1 "至少一次"传输(如果Bit 1为1,表示QoS 1)
支持 QoS2 "只有一次"传输(如果Bit 2为1,表示QoS 2)
订阅者收到MQTT消息的QoS级别,最终取决于发布消息的QoS和主题订阅的QoS中低的那个
协议原理
生产者和消费者是通过订阅主题来通信,主题是以 ‘/’ 为分隔符区分不同的层级。协议方法有15个动作
(1)CONNECT:客户端连接到服务器
(2)CONNACK:连接确认
(3)PUBLISH:发布消息
(4)PUBACK:发布确认
(5)PUBREC:发布的消息已接收
(6)PUBREL:发布的消息已释放
(7)PUBCOMP:发布完成
(8)SUBSCRIBE:订阅请求
(9)SUBACK:订阅确认
(10)UNSUBSCRIBE:取消订阅
(11)UNSUBACK:取消订阅确认
(12)PINGREQ:客户端发送心跳
(13)PINGRESP:服务端心跳响应
(14)DISCONNECT:断开连接
(15)AUTH:认证
mqtt协议的网络传输开销小,主要是因为其简洁的请求报文(1个字节的固定报头 和 2个字节的心跳报文)。协议数据包:由三部分组成:
固定报文头:表示数据包类型及数据包的分组,如连接,发布,订阅,心跳等。上面的15个动作都是数据包类型
可变报文头:有些固定报文头是没有可变报文头的,
报文体:有些固定报文头是没有报文体的,报文体就是消息体。
mqtt之所以可以在弱网下发送消息就是因为简洁的报文+qos
EMQ X
EMQX 是 MQTT Broker 的一种实现,属于数据采集这一层。
前端的硬件通过 MQTT 协议与位于数据采集层的 EMQ X 交互,通过 EMQ X 提供的数据接口,将数据保存到后台的持久化平台中(各种关系型数据库和 NOSQL 数据库),或者流式数据处理框架等,上层应用通过这些数据分析后得到的结果呈现给最终用户。
安装
拉取镜像
docker pull emqx/emqx:v4.1.0
运行容器就直接可以启动了
docker run -tid --name emqx -p 1883:1883 -p 8083:8083 -p 8081:8081 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx:v4.1.0
地址:http://192.168.200.129:18083
默认用户名:admin,默认密码:public
常用命令:
启动 emqx start
查看状态 emqx_ctl status
停止 emqx stop
重启 emqx restart
卸载 rpm -e emqx
延迟队列
开启延迟队列
对正常的topic前面加两个层级,就可以成为延迟队列了。($delayed是关键字,普通队列不要用哦)
生产者:$delayed/时间(秒)/topic
消费者:直接监听topic就好了
共享队列(点对点)
queue
EMQ X 支持两种格式的共享订阅前缀:
示例 前缀 真实主题名
$queue/t/1 $queue/ t/1
$share/abc/t/1 $share/abc t/1
多个消费者监听 $queue/t/1,生产者发送 t/1
默认是随机策略,设置轮询策略
1. 进入容器 docker exec -it 容器id /bin/sh
2. 编辑配置 vi emqx.conf , /开头 直接搜索指定配置,把random替换成轮询
/broker.shared_subscription_strategy
3. 最后保存重启 cd ../bin/ && emqx restart
多个此时有三个消费者:
a监听 $queue/t/1
b监听 $queue/t/1
c监听 t/1
此时如果有发送者往 t/1 发送3条消息,那么c会收到3条,而ab会以轮询的方式共享3条。
share
EMQ X 会向两个群组 g1 和 g2 同时发送 msg1
s1,s2,s3 中只有一个会收到 msg1
s4,s5 中只有一个会收到 msg1
静态代理订阅
就是我们的broker 不用指定订阅的topic,但是配置规则后,默认就会监听这些 topic 了
1. 开启静态订阅
2. 指定订阅规则
在 emqx.conf 配置规则并重启。比如我这里就是配置了4个规则,指定了他们的队列和qos等级。
在配置代理订阅的主题时,EMQ X 提供了 %c 和 %u 两个占位符供用户使用,EMQ X 会在执行代理订阅时将配置中的 %c 和 %u 分别替换为客户端的 Client ID 和 Username,需要注意的是,%c 和 %u 必须占用一整个主题层级。(在连接emqx broker的时候会分配一个clientId,username是自己自定义的)
module.subscription.<number>.topic = <topic>
module.subscription.<number>.qos = <qos>
<number>表示第几个规则;<topic>表示队列;<qos>是质量等级
这种方式每次都要改配置,比较麻烦;目前只有收费版才有 动态订阅功能。
保留消息
生产者发送消息后,消费者再去监听这个消息,是拿不到的。只有设置为“保留消息”,消费者才可以拿到。如果往一个队列里面发送了多条消息,消费者此时监听只能拿到最近的一条消息。
我们还可以自定义保留规则
vi etc/plugins/emqx_retainer.conf
安全认证
- 开启http认证
- 配置 http 认证 地址,请求方式,参数
3. 开发认证服务接口(我们连接emqx , emqx 就会根据我们上面的配置信息发送认证请求)
import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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; /** * @title: HTML 验证 * @author: jay.wu * @date: 2022/2/21 18:57 */ @Slf4j @RestController @RequestMapping("/mqtt") public class AuthController { private HashMap<String,String> users; @PostConstruct public void init(){ users = new HashMap<>(); users.put("user","123456"); users.put("emq-client2","123456");//testtopic/# users.put("emq-client3","123456");// testtopic/123 users.put("admin","admin"); } @PostMapping("/auth") public ResponseEntity auth(@RequestParam("clientid") String clientid, @RequestParam("username") String username, @RequestParam("password") String password){ log.info("emqx http认证组件开始调用任务服务完成认证,clientid={},username={},password={}",clientid,username,password); String value = users.get(username); if(StringUtils.isEmpty(value)){ return new ResponseEntity(HttpStatus.UNAUTHORIZED); } if(!value.equals(password)){ return new ResponseEntity(HttpStatus.UNAUTHORIZED); } return new ResponseEntity(HttpStatus.OK); } @PostMapping("/superuser") public ResponseEntity superuser(@RequestParam("clientid") String clientid, @RequestParam("username") String username){ log.info("emqx 查询是否是超级用户,clientid={},username={}",clientid,username); if(clientid.contains("admin") || username.contains("admin")){ log.info("用户{}是超级用户",username); return new ResponseEntity(HttpStatus.OK); }else { log.info("用户{}不是超级用户",username); return new ResponseEntity(HttpStatus.UNAUTHORIZED); } } @PostMapping("/acl") public ResponseEntity acl(@RequestParam("access")int access, @RequestParam("username")String username, @RequestParam("clientid")String clientid, @RequestParam("ipaddr")String ipaddr, @RequestParam("topic")String topic, @RequestParam("mountpoint")String mountpoint){ log.info("EMQX发起客户端操作授权查询请求,access={},username={},clientid={},ipaddr={},topic={},mountpoint={}", access,username,clientid,ipaddr,topic,mountpoint); if(username.equals("emq-client2") && topic.equals("testtopic/#") && access == 1){ log.info("客户端{}有权限订阅{}",username,topic); return new ResponseEntity(HttpStatus.OK); } if(username.equals("emq-client3") && topic.equals("testtopic/123") && access == 2){ log.info("客户端{}有权限向{}发布消息",username,topic); return new ResponseEntity(HttpStatus.OK); } log.info("客户端{},username={},没有权限对主题{}进行{}操作",clientid,username,topic,access==1?"订阅":"发布"); return new ResponseEntity(HttpStatus.UNAUTHORIZED); //return new ResponseEntity(HttpStatus.OK); } }