前言
首先说明一下,netty实现并封装了mqtt协议,同时也为其写好了编解码器,但是再了解并搭建之前,尤其是还不了解netty和mqtt的同学,必须要清楚一件事:mqtt协议的所具备的功能都是需要你自己实现的。
简单举个例子,rabbitmq消息中间件应该都知道,我们在使用rabbit的时候只需要定义交换机、队列,然后生产者和消费者分别往指定队列发送消息和监听指定队列消息即可互相收发,但是MQTT只是一种协议,说白了就是一种概念,告诉你这种协议是什么样的,netty并没有帮你实现如何订阅发布,你需要根据自己具体的需求,按照mqtt协议的规范去实现主题订阅发布的功能。
不单是netty,凡是用到mqtt协议的,大概都是这种情况,也可能是博主开始研究的时候走入了误区,混淆了概念,后来才反应过来,当然,明白的就直接看正文吧。
MQTT协议概念
组成部分
- 固定头
包含消息的类型(Message Type)和QoS级别等标志位。
消息类型:
名称 | 描述 | 方向 |
---|---|---|
CONNECT | 客户端请求与服务端建立连接 | C->S(服务端接收) |
CONNACK | 服务端确认连接建立 | S->C(客户端接收) |
PUBLISH | 发布消息【QoS 0级别,最多分发一次】,生产者只会发送一次消息,不关心消息是否被代理服务端或消费者收到 | 双向都可 |
PUBACK | 收到发布消息确认,客户端接收【QoS 1级别,至少分发一次】,保证消息发送到服务端(也就是代理服务器broker),如果没收到或一定时间没收到服务端的ack,就会重发消息 | 双向都可 |
PUBREC | 收到发布消息【QoS 2级别】只分发一次消息,且保证到达 ,这三步保证消息有且仅有一次传递给消费者 | 双向都可 |
PUBREL | 释放发布消息【QoS 2级别】 | 双向都可 |
PUBCOMP | 完成发布消息【QoS 2级别】 | 双向都可 |
SUBSCRIBE | 订阅请求 | C->S(服务端接收) |
SUBACK | 订阅确认 | S->C(客户端接收) |
UNSUBSCRIBE | 取消订阅 | C->S(服务端接收) |
UNSUBACK | 取消订阅确认 | S->C(客户端接收) |
PING | 客户端发送PING(连接保活)命令 | |
PINGRSP | PING命令回复 | S->C(客户端接收) |
DISCONNECT | 断开连接 |
- 可变头
包含协议名,协议版本,连接标志,心跳间隔时间,连接返回码,主题名等。 - 消息体
包含消息内容,也就是payload。
实现mqtt协议
NettyServer服务端
/**
* @author: zhouwenjie
* @description: netty启动配置类
* @create: 2020-04-03 11:43
**/
@Slf4j
@Component
public class MqttServer {
@Autowired
private MqttServerChannelInitializer mqttServerChannelInitializer;
private NioEventLoopGroup bossGroup;
private NioEventLoopGroup workerGroup;
private ChannelFuture future;
@Value("${driver.mqtt.socket_port}")
private int socketPort;
public void start() {
//创建接收请求和处理请求的实例(默认线程数为 CPU 核心数乘以2也可自定义)
bossGroup = new NioEventLoopGroup(3);
workerGroup = new NioEventLoopGroup(6);
try {
//创建服务端启动辅助类(boostrap 用来为 Netty 程序的启动组装配置一些必须要组件,例如上面的创建的两个线程组)
ServerBootstrap socketBs = new ServerBootstrap();
//channel 方法用于指定服务器端监听套接字通道
//socket配置
socketBs.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
//ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,
// 函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,
// 服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,
// 多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
.option(ChannelOption.SO_BACKLOG, 1024)
//快速复用,防止服务端重启端口被占用的情况发生
.option(ChannelOption.SO_REUSEADDR, true)
.childHandler(mqttServerChannelInitializer)
//如果TCP_NODELAY没有设置为true,那么底层的TCP为了能减少交互次数,会将网络数据积累到一定的数量后,
// 服务器端才发送出去,会造成一定的延迟。在互联网应用中,通常希望服务是低延迟的,建议将TCP_NODELAY设置为true
.childOption(ChannelOption.TCP_NODELAY, true)
//默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
.childOption(ChannelOption.SO_KEEPALIVE, true);
future = socketBs.bind(socketPort).sync();
if (future.isSuccess()) {
log.info("[*MQTT驱动服务端启动成功]");
future.channel().closeFuture().sync();
} else {
log.info("[~~~MQTT驱动服务端启动失败~~~]");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
/**
* 这里可以手动关闭,同时在整个springboot应用停止的时候,这里也得以调用【@PreDestroy】
* 参考 https://www.cnblogs.com/CreatorKou/p/11606870.html
*/
@PreDestroy
public void shutdown() {
// 优雅关闭两个 EventLoopGroup 对象
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
log.info("[*MQTT服务端关闭成功]");
}
}
配置管道
/**
* @author: zhouwenjie
* @description: 配置管道 服务端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器
* @create: 2020-04-03 14:14
**/
@Component
public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Autowired
private ServerMqttHandler serverMqttHandler;
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new MqttDecoder(1024 * 8));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast(serverMqttHandler);
}
}
handlder处理器
@Slf4j
@Component
@ChannelHandler.Sharable
public class ServerMqttHandler extends SimpleChannelInboundHandler<MqttMessage> {
public static final ConcurrentHashMap<String, ChannelHandlerContext> clientMap = new ConcurrentHashMap<String, ChannelHandlerContext>();
@Value("${driver.mqtt.address_list}")
private List<String> addressList;
@Autowired
private MqttMsgBack mqttMsgBack;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
//判断连接是否合法
if (addressList.contains(address.getHostString())) {
clientMap.put(ctx.channel().id().toString(), ctx);
super.handlerAdded(ctx);
} else {
ctx.close();
}
}
/**
* 功能描述: 客户端终止连接服务器会触发此函数
*
* @param ctx
* @return void
* @author zhouwenjie
* @date 2020/4/3 16:47
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
delSubCache(ctx);
super.channelInactive(ctx);
}
/**
* 功能描述: 有客户端发消息会触发此函数
*
* @param ctx
* @param mqttMessage
* @return void
* @author zhouwenjie
* @date 2020/4/3 16:48
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
if (null != mqttMessage) {
log.info("接收mqtt消息:" + mqttMessage);
MqttFixedHeader mqttFixedHeader = mqttMessage.fixedHeader();
switch (mqttFixedHeader.messageType()) {
// ----------------------接收消息端(服务端)可能会触发的事件----------------------------------------------------------------
case CONNECT:
// 在一个网络连接上,客户端只能发送一次CONNECT报文。服务端必须将客户端发送的第二个CONNECT报文当作协议违规处理并断开客户端的连接
// 建议connect消息单独处理,用来对客户端进行认证管理等 这里直接返回一个CONNACK消息
mqttMsgBack.connectionAck(ctx, mqttMessage);
break;
case PUBLISH:
// 收到消息,返回确认,PUBACK报文是对QoS 1等级的PUBLISH报文的响应,PUBREC报文是对PUBLISH报文的响应
mqttMsgBack.publishAck(ctx, mqttMessage);
break;
case PUBREL:
// 释放消息,PUBREL报文是对QoS 2等级的PUBREC报文的响应,此时我们应该回应一个PUBCOMP报文
mqttMsgBack.publishComp(ctx, mqttMessage);
break;
case SUBSCRIBE:
// 客户端订阅主题
// 客户端向服务端发送SUBSCRIBE报文用于创建一个或多个订阅,每个订阅注册客户端关心的一个或多个主题。
// 为了将应用消息转发给与那些订阅匹配的主题,服务端发送PUBLISH报文给客户端。
// SUBSCRIBE报文也(为每个订阅)指定了最大的QoS等级,服务端根据这个发送应用消息给客户端
mqttMsgBack.subscribeAck(ctx, mqttMessage);
break;
case UNSUBSCRIBE:
// 客户端取消订阅
// 客户端发送UNSUBSCRIBE报文给服务端,用于取消订阅主题
mqttMsgBack.unsubscribeAck(ctx, mqttMessage);
break;
case PINGREQ:
// 客户端发起心跳
mqttMsgBack.pingResp(ctx, mqttMessage);
break;
case DISCONNECT:
// 客户端主动断开连接
// DISCONNECT报文是客户端发给服务端的最后一个控制报文, 服务端必须验证所有的保留位都被设置为0
break;
// ----------------------服务端作为发送消息端可能会接收的事件----------------------------------------------------------------
case PUBACK:
case PUBREC:
//QoS 2级别,响应一个PUBREL报文消息,PUBACK、PUBREC这俩都是ack消息
//PUBACK报文是对QoS 1等级的PUBLISH报文的响应,如果一段时间没有收到客户端ack,服务端会重新发送消息
mqttMsgBack.receivePubAck(ctx, mqttMessage);
break;
case PUBCOMP:
//收到qos2级别接收端最后一次发送过来的确认消息
mqttMsgBack.receivePubcomp(ctx, mqttMessage);
break;
default:
break;
}
}
}
/**
* 功能描述: 心跳检测
*
* @param ctx 这里的作用主要是解决断网,弱网的情况发生
* @param evt
* @return void
* @author zhouwenjie
* @date 2020/4/3 17:02
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
String socketString = ctx.channel().remoteAddress().toString();
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
log.info("Client: " + socketString + " READER_IDLE 读超时");
delSubCache(ctx);
ctx.disconnect();
}
}
}
/**
* 功能描述:
*
* @param ctx
* @param cause
* @return void
* @author 发生异常会触发此函数
* @date 2020/4/3 16:49
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
}
private void delSubCache(ChannelHandlerContext ctx){
String id = ctx.channel().id().toString();
clientMap.remove(id);
//删除订阅主题
Set<String> topicSet = MqttMsgBack.ctMap.get(id);
if (CollUtil.isNotEmpty(topicSet)) {
ConcurrentHashMap<String, HashSet<String>> subMap = MqttMsgBack.subMap;
ConcurrentHashMap<String, MqttQoS> qoSMap = MqttMsgBack.qoSMap;
for (String topic : topicSet) {
if (subMap != null) {
HashSet<String> ids = subMap.get(topic);
if (CollUtil.isNotEmpty(ids)) {
ids.remove(id);
if (CollUtil.isEmpty(ids)) {
subMap.remove(topic);
}
}
}
if (qoSMap != null) {
qoSMap.remove(topic + "-" + id);
}
}
}
MqttMsgBack.ctMap.remove(id);
}
}
配置处理器,请务必仔细查看代码中的注释以及结合本专栏另一篇文章,这样就会更好的理解了
/**
* @author: zhouwenjie
* @description: 对接收到的消息进行业务处理
* @create: 2023-04-07 16:29
* CONNECT 1 C->S 客户端请求与服务端建立连接 (服务端接收)
* CONNACK 2 S->C 服务端确认连接建立(客户端接收)
* PUBLISH 3 CóS 发布消息 (服务端接收【QoS(服务质量等级) 0级别,最多分发一次】)-->生产者只会发送一次消息,不关心消息是否被代理服务端或消费者收到
* PUBACK 4 CóS 收到发布消息确认(客户端接收【QoS 1级别,至少分发一次】) -->保证消息发送到服务端(也就是代理服务器broker),如果没收到或一定时间没收到服务端的ack,就会重发消息
* PUBREC 5 CóS 收到发布消息(客户端接收【QoS 2级别】)|
* PUBREL 6 CóS 释放发布消息(服务端接收【QoS 2级别】)|只分发一次消息,且保证到达 -->这三步保证消息有且仅有一次传递给消费者
* PUBCOMP 7 CóS 完成发布消息(客户端接收【QoS 2级别】)|
* SUBSCRIBE 8 C->S 订阅请求(服务端接收)
* SUBACK 9 S->C 订阅确认(客户端接收)
* UNSUBSCRIBE 10 C->S 取消订阅(服务端接收)
* UNSUBACK 11 S->C 取消订阅确认(客户端接收)
* PINGREQ 12 C->S 客户端发送PING(连接保活)命令(服务端接收)
* PINGRESP 13 S->C PING命令回复(客户端接收)
* DISCONNECT 14 C->S 断开连接 (服务端接收)
* <p>
* 注意:在我们发送消息的时候,一定要确认好等级,回复确认的消息统一设置为qos=0;
* 比如,我们需要发送最高等级的消息就将qos设置为2,当我们接收方收到这个等级的消息的时候判断一下等级,设置好消息类型,然后qos因为是回复消息,
* 所以全部设置成0,而当发送端收到消息之后,比如PUBREC,那么我们还将发送(注意不是回复)PUBREL给接收端,希望对方回复确认一下,所以qos设置为1。
* 综上可知:发送端没有回复确认消息之说,只有发送消息,接收端没有发送消息之说,只有回复确认消息,搞清楚这个概念,在设置参数的时候就明了。
**/
@Slf4j
@Component
public class MqttMsgBack {
@Value("${driver.mqtt.user_name}")
private String userName;
@Value("${driver.mqtt.password}")
private String password;
@Value("${driver.mqtt.wait_time}")
private long waitTime;
/**
* 功能描述:存放主题和其订阅的客户端集合
*/
public static final ConcurrentHashMap<String, HashSet<String>> subMap = new ConcurrentHashMap<String, HashSet<String>>();
/**
* 功能描述:存放订阅是的服务质量等级,只有发送小于或等于这个服务质量的消息给订阅者
*/
public static final ConcurrentHashMap<String, MqttQoS> qoSMap = new ConcurrentHashMap<String, MqttQoS>();
/**
* 功能描述:存放客户端和其所订阅的主题集合,用来在客户端断开的时候删除订阅中的客户端
*/
public static final ConcurrentHashMap<String, Set<String>> ctMap = new ConcurrentHashMap<String, Set<String>>();
/**
* 功能描述:存放需要缓存的消息,一边发送给新订阅的客户端
*/
public static final ConcurrentHashMap<String, MqttPublishMessage> cacheRetainedMessages = new ConcurrentHashMap<String, MqttPublishMessage>();
/**
* 功能描述:缓存需要重复发送的消息,以便在收到ack的时候将消息内存释放掉
*/
public static final ConcurrentHashMap<String, ByteBuf> cacheRepeatMessages = new ConcurrentHashMap<String, ByteBuf>();
/**
* 确认连接请求
*
* @param ctx
* @param mqttMessage
*/
public void connectionAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
MqttConnectMessage mqttConnectMessage;
try {
mqttConnectMessage = (MqttConnectMessage) mqttMessage;
//获取连接者的ClientId
String clientIdentifier = mqttConnectMessage.payload().clientIdentifier();
//查询用户名密码是否正确
String userNameNow = mqttConnectMessage.payload().userName();
String passwordNow = mqttConnectMessage.payload().password();
if (userName.equals(userNameNow) && password.equals(passwordNow)) {
MqttFixedHeader mqttFixedHeaderInfo = mqttConnectMessage.fixedHeader();
MqttConnectVariableHeader mqttConnectVariableHeaderInfo = mqttConnectMessage.variableHeader();
//构建返回报文, 可变报头
MqttConnAckVariableHeader mqttConnAckVariableHeaderBack = new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, mqttConnectVariableHeaderInfo.isCleanSession());
//构建返回报文, 固定报头 至多一次(至少—次,只有一次)
MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.CONNACK, mqttFixedHeaderInfo.isDup(), AT_MOST_ONCE, mqttFixedHeaderInfo.isRetain(), 0x02);
//构建连接回复消息体
MqttConnAckMessage connAck = new MqttConnAckMessage(mqttFixedHeaderBack, mqttConnAckVariableHeaderBack);
ctx.writeAndFlush(connAck);
//设置节点名
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
log.info("终端登录成功,ID号:{},IP信息:{},终端号:{}", clientIdentifier, address.getHostString(), address.getPort());
} else {
//如果用户名密码错误则提示对方
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0x02);
MqttConnAckVariableHeader variableHeader = new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, false);
MqttConnAckMessage mqttConnAckMessage = new MqttConnAckMessage(fixedHeader, variableHeader);
ctx.writeAndFlush(mqttConnAckMessage);
ctx.close();
log.error("连接失败:" + MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD.name());
}
} catch (ClassCastException e) {
//转换失败,对方发送的协议版本不兼容
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0x02);
MqttConnAckVariableHeader variableHeader = new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION, false);
MqttConnAckMessage mqttConnAckMessage = new MqttConnAckMessage(fixedHeader, variableHeader);
ctx.writeAndFlush(mqttConnAckMessage);
ctx.close();
e.printStackTrace();
log.error("连接失败:" + MqttConnectReturnCode.CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION.name());
}
}
/**
* 根据qos发布确认
* 1:表示发送的消息需要一直持久保存(不受服务器重启影响),不但要发送给当前的订阅者,并且以后新来的订阅了此Topic name的订阅者会马上得到推送。
* 0:仅仅为当前订阅者推送此消息。
* 假如服务器收到一个空消息体(zero-length payload)、RETAIN = 1、那么就代表需要将这个缓存消息删除掉,不再继续推送给新订阅者,前提是一定保证空消息体。
* 两个条件需要对应上才能删除,消息体为空、主题名称对应.
* 注意:对于每个主题,只能保留一条消息。当发布一个带有RETAIN标志的新消息时,它将替换上一条保留的消息,因此在同一主题下只会存在一条保留消息。
*
* @param ctx
* @param mqttMessage 拷贝的数据需要释放,其他的消息,比如连接消息,不用转发,会在handler中自动释放
*/
public void publishAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
MqttPublishMessage mqttPublishMessage = (MqttPublishMessage) mqttMessage;
MqttFixedHeader mqttFixedHeaderInfo = mqttPublishMessage.fixedHeader();
MqttQoS qos = mqttFixedHeaderInfo.qosLevel();
//得到主题
MqttPublishVariableHeader variableHeader = mqttPublishMessage.variableHeader();
String topicName = variableHeader.topicName();
//将消息发送给订阅的客户端
ByteBuf byteBuf = mqttPublishMessage.payload();
HashSet<String> set = subMap.get(topicName);
if (set != null) {
for (String channelId : set) {
ChannelHandlerContext context = ServerMqttHandler.clientMap.get(channelId);
if (context != null && context.channel().isActive()) {
MqttQoS cacheQos = qoSMap.get(topicName + "-" + channelId);
if (cacheQos != null && qos.value() <= cacheQos.value()) {
// retainedDuplicate()增加引用计数器,不至于后续操作byteBuf出现错误,引用计数器为0的情况,这里会清除retainedDuplicate的操作有:
// SimpleChannelInboundHandler处理器、编码器 MqttEncoder、和最后确认的时候释放,所以每次操作消息之前,先进行一次retainedDuplicate
byteBuf.retainedDuplicate();
context.writeAndFlush(mqttPublishMessage);
if (qos == AT_LEAST_ONCE || qos == EXACTLY_ONCE) {
//只发送服务质量等级小于等于订阅时客户端指定的服务质量等级
// 创建一个新的缓冲区
byteBuf.retainedDuplicate();
//防止内存溢出,最后在消息被ack或者客户端断开掉线的时候,拿到并进行释放
cacheRepeatMessages.put(channelId, byteBuf);
cachePublishMsg(qos, byteBuf, variableHeader, mqttFixedHeaderInfo, context);
}
}
} else {
if (context != null) {
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
log.error(address.getHostString() + "转发订阅消息提醒:客户端连接异常~");
}
//防止客户端频繁上下线导致id变化,带来不必要的空指针
ServerMqttHandler.clientMap.remove(channelId);
//删除订阅主题
Set<String> topicSet = ctMap.get(channelId);
if (CollUtil.isNotEmpty(topicSet)) {
for (String topic : topicSet) {
if (subMap != null) {
HashSet<String> ids = subMap.get(topic);
if (CollUtil.isNotEmpty(ids)) {
ids.remove(channelId);
if (CollUtil.isEmpty(ids)) {
subMap.remove(topic);
}
}
}
if (qoSMap != null) {
qoSMap.remove(topic + "-" + channelId);
}
}
}
ctMap.remove(channelId);
}
}
}
// 缓存消息给后订阅的客户端
boolean retain = mqttFixedHeaderInfo.isRetain();
if (retain) {
if (byteBuf.readableBytes() > 0) {
byteBuf.retainedDuplicate();
cacheRetainedMessages.put(topicName, mqttPublishMessage);
} else {
MqttPublishMessage message = cacheRetainedMessages.get(topicName);
if (message != null) {
cacheRetainedMessages.remove(topicName);
// 这里需要手动删除ByteBuf缓存,因为一直会有一份缓存在内存中备用
boolean release = ReferenceCountUtil.release(message);
log.info("缓存消息给后订阅的客户端释放成功失败:{}", release);
}
}
}
//返回消息给发送端
switch (qos) {
//至多一次
case AT_MOST_ONCE:
break;
//至少一次
case AT_LEAST_ONCE:
//构建返回报文, 可变报头
MqttMessageIdVariableHeader mqttMessageIdVariableHeaderBack = MqttMessageIdVariableHeader.from(mqttPublishMessage.variableHeader().packetId());
//构建返回报文, 固定报头
MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(PUBACK, mqttFixedHeaderInfo.isDup(), AT_MOST_ONCE, mqttFixedHeaderInfo.isRetain(), 0x02);
//构建PUBACK消息体
MqttPubAckMessage pubAck = new MqttPubAckMessage(mqttFixedHeaderBack, mqttMessageIdVariableHeaderBack);
ctx.writeAndFlush(pubAck);
break;
//刚好一次
case EXACTLY_ONCE:
//构建返回报文,固定报头
MqttFixedHeader mqttFixedHeaderBack2 = new MqttFixedHeader(MqttMessageType.PUBREC, false, AT_MOST_ONCE, false, 0x02);
//构建返回报文,可变报头
MqttPubReplyMessageVariableHeader mqttPubReplyMessageVariableHeader = new MqttPubReplyMessageVariableHeader(mqttPublishMessage.variableHeader().packetId(), MqttPubReplyMessageVariableHeader.REASON_CODE_OK, MqttProperties.NO_PROPERTIES);
MqttMessage mqttMessageBack = new MqttMessage(mqttFixedHeaderBack2, mqttPubReplyMessageVariableHeader);
ctx.writeAndFlush(mqttMessageBack);
break;
default:
break;
}
}
private void cachePublishMsg(MqttQoS qos, ByteBuf byteBuf, MqttPublishVariableHeader variableHeader, MqttFixedHeader mqttFixedHeaderInfo, ChannelHandlerContext context) {
//缓存一份消息,规定时间内没有收到ack,用作重发,重发时将isDup设置为true,代表重复消息
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, qos, false, mqttFixedHeaderInfo.remainingLength());
MqttPublishMessage cachePubMessage = new MqttPublishMessage(fixedHeader, variableHeader, byteBuf);
ScheduledFuture<?> scheduledFuture = TimerData.scheduledThreadPoolExecutor.scheduleAtFixedRate(new MonitorMsgTime(variableHeader.packetId(), cachePubMessage, context), waitTime, waitTime, TimeUnit.MILLISECONDS);
TimerData.scheduledFutureMap.put(variableHeader.packetId(), scheduledFuture);
}
/**
* 发布完成 qos2
*
* @param ctx
* @param mqttMessage
*/
public void publishComp(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
MqttMessageIdVariableHeader messageIdVariableHeader = (MqttMessageIdVariableHeader) mqttMessage.variableHeader();
//构建返回报文, 固定报头
MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0x02);
//构建返回报文, 可变报头
MqttPubReplyMessageVariableHeader mqttPubReplyMessageVariableHeader = new MqttPubReplyMessageVariableHeader(messageIdVariableHeader.messageId(), MqttPubReplyMessageVariableHeader.REASON_CODE_OK, MqttProperties.NO_PROPERTIES);
MqttMessage mqttMessageBack = new MqttMessage(mqttFixedHeaderBack, mqttPubReplyMessageVariableHeader);
ctx.writeAndFlush(mqttMessageBack);
}
/**
* 订阅确认
* 订阅和取消订阅没有qos2级别,默认就是1级别
* 需要存储订阅主题和客户端、客户端和订阅主题、验证防止重复订阅、发送缓存消息
*
* @param ctx
* @param mqttMessage
*/
public void subscribeAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
MqttSubscribeMessage mqttSubscribeMessage = (MqttSubscribeMessage) mqttMessage;
MqttMessageIdVariableHeader messageIdVariableHeader = mqttSubscribeMessage.variableHeader();
//构建返回报文, 可变报头
MqttMessageIdVariableHeader variableHeaderBack = MqttMessageIdVariableHeader.from(messageIdVariableHeader.messageId());
MqttSubscribePayload subscribePayload = mqttSubscribeMessage.payload();
List<MqttTopicSubscription> mqttTopicSubscriptions = subscribePayload.topicSubscriptions();
List<Integer> grantedQoSLevels = new ArrayList<Integer>();
String id = ctx.channel().id().toString();
//存储客户端订阅的主题集合,断开或者异常连接时,删除订阅ctMap和subMap里的值
Set<String> topicSet = ctMap.get(id);
if (topicSet == null) {
topicSet = new HashSet<String>();
}
for (MqttTopicSubscription subscription : mqttTopicSubscriptions) {
String topicName = subscription.topicName();
HashSet<String> contexts = subMap.get(topicName);
if (contexts == null) {
contexts = new HashSet<String>();
}
//先判断主题是否已经订阅过了,防止重复订阅
boolean isSub = contexts.contains(topicName);
if (!isSub) {
MqttQoS qos = subscription.option().qos();
//存储主题被订阅的客户端集合
contexts.add(id);
qoSMap.put(topicName + "-" + id, qos);
subMap.put(topicName, contexts);
//存储客户端订阅的主题集合
topicSet.add(topicName);
}
//存储客户端订阅的主题集合
int value = subscription.qualityOfService().value();
grantedQoSLevels.add(value);
}
// 构建返回报文 有效负载
MqttSubAckPayload payloadBack = new MqttSubAckPayload(grantedQoSLevels);
// 构建返回报文 固定报头
MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
// 构建返回报文 订阅确认
MqttSubAckMessage subAck = new MqttSubAckMessage(mqttFixedHeaderBack, variableHeaderBack, payloadBack);
ctx.writeAndFlush(subAck);
for (String topic : topicSet) {
MqttQoS cacheQos = qoSMap.get(topic + "-" + id);
//查看订阅的主题是否需要需要发送消息
MqttPublishMessage mqttMsg = cacheRetainedMessages.get(topic);
if (mqttMsg != null) {
MqttFixedHeader mqttFixedHeaderInfo = mqttMsg.fixedHeader();
MqttQoS qos = mqttFixedHeaderInfo.qosLevel();
if (cacheQos != null && qos.value() <= cacheQos.value()) {
if (mqttMsg != null) {
MqttPublishVariableHeader variableHeader = mqttMsg.variableHeader();
ByteBuf payload = mqttMsg.payload();
//引用计数器增加
payload.retainedDuplicate();
ctx.writeAndFlush(mqttMsg);
// 开启消息重发机制
if (qos == AT_LEAST_ONCE || qos == EXACTLY_ONCE) {
//引用计数器增加
payload.retainedDuplicate();
cacheRepeatMessages.put(id, payload);
cachePublishMsg(qos, payload, variableHeader, mqttFixedHeaderInfo, ctx);
}
}
}
}
}
}
/**
* 取消订阅确认
*
* @param ctx
* @param mqttMessage
*/
public void unsubscribeAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
MqttUnsubscribeMessage mqttUnsubscribeMessage = (MqttUnsubscribeMessage) mqttMessage;
MqttMessageIdVariableHeader messageIdVariableHeader = mqttUnsubscribeMessage.variableHeader();
// 构建返回报文 可变报头
MqttMessageIdVariableHeader variableHeaderBack = MqttMessageIdVariableHeader.from(messageIdVariableHeader.messageId());
// 构建返回报文 固定报头
MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0x02);
// 构建返回报文 取消订阅确认
MqttUnsubAckMessage unSubAck = new MqttUnsubAckMessage(mqttFixedHeaderBack, variableHeaderBack);
log.info("取消订阅回复:{}", unSubAck);
//删除本地订阅客户端
String id = ctx.channel().id().toString();
List<String> topics = mqttUnsubscribeMessage.payload().topics();
Set<String> topicSet = ctMap.get(id);
for (String topic : topics) {
if (subMap != null) {
HashSet<String> ids = subMap.get(topic);
if (CollUtil.isNotEmpty(ids)) {
ids.remove(id);
if (CollUtil.isEmpty(ids)) {
subMap.remove(topic);
}
}
}
if (qoSMap != null) {
qoSMap.remove(topic + "-" + id);
}
if (CollUtil.isNotEmpty(topicSet)) {
topicSet.remove(topic);
}
}
ctx.writeAndFlush(unSubAck);
}
/**
* 心跳响应
*
* @param ctx
* @param mqttMessage
*/
public void pingResp(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessage mqttMessageBack = new MqttMessage(fixedHeader);
log.info("心跳回复:{}", mqttMessageBack.toString());
ctx.writeAndFlush(mqttMessageBack);
}
/**
* ------------------------------------------------------服务端作为发送消息端可能会接收的事件----------------------------------------------------------------
* <p>
* 收到接收方消息确认,qos>1的情况,应该删除消息缓存(缓存消息保存到线程定时中了,循环发送取消,消息缓存也没有了),取消消息重发机制
*
* @param ctx
* @param mqttMessage
*/
public void receivePubAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
MqttFixedHeader fixedHeader = mqttMessage.fixedHeader();
MqttMessageType messageType = fixedHeader.messageType();
if (messageType == PUBACK) {
MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) mqttMessage.variableHeader();
int messageId = variableHeader.messageId();
//等级为1的情况,直接删除原始消息,取消消息重发机制
ScheduledFuture<?> scheduledFuture = TimerData.scheduledFutureMap.remove(messageId);
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
//移除消息记录
ByteBuf byteBuf = cacheRepeatMessages.remove(ctx.channel().id().toString());
if (byteBuf != null) {
// 释放内存
byteBuf.release();
}
}
if (messageType == PUBREC) {
//等级为2的情况,收到PUBREC报文消息,先停止消息重发机制,再响应一个PUBREL报文并且构建消息重发机制
MqttPubReplyMessageVariableHeader variableHeader = (MqttPubReplyMessageVariableHeader) mqttMessage.variableHeader();
int messageId = variableHeader.messageId();
//构建返回报文,固定报头
MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.PUBREL, false, AT_LEAST_ONCE, false, 0);
//构建返回报文,可变报头
MqttPubReplyMessageVariableHeader mqttPubReplyMessageVariableHeader = new MqttPubReplyMessageVariableHeader(messageId, MqttPubReplyMessageVariableHeader.REASON_CODE_OK, MqttProperties.NO_PROPERTIES);
MqttMessage mqttMessageBack = new MqttMessage(mqttFixedHeaderBack, mqttPubReplyMessageVariableHeader);
//删除初始消息重发机制
ScheduledFuture<?> scheduledFuture = TimerData.scheduledFutureMap.remove(messageId);
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
//释放消息缓存
ByteBuf byteBuf = cacheRepeatMessages.remove(ctx.channel().id().toString());
if (byteBuf != null) {
byteBuf.release();
}
ctx.writeAndFlush(mqttMessageBack);
//重发机制要放在最下方,否则,一旦出错,会多次出发此机制
cachePubrelMsg(messageId, ctx);
}
}
private void cachePubrelMsg(int messageId, ChannelHandlerContext context) {
//缓存一份消息,规定时间内没有收到ack,用作重发,重发时将isDup设置为true,代表重复消息
//构建返回报文,固定报头
MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.PUBREL, true, AT_LEAST_ONCE, false, 0);
//构建返回报文,可变报头
MqttMessageIdVariableHeader mqttMessageIdVariableHeaderBack = MqttMessageIdVariableHeader.from(messageId);
MqttMessage mqttMessageBack = new MqttMessage(mqttFixedHeaderBack, mqttMessageIdVariableHeaderBack);
ScheduledFuture<?> scheduledFuture = TimerData.scheduledThreadPoolExecutor.scheduleAtFixedRate(new MonitorMsgTime(messageId, mqttMessageBack, context), waitTime, waitTime, TimeUnit.MILLISECONDS);
TimerData.scheduledFutureMap.put(messageId, scheduledFuture);
}
/**
* 功能描述: 接收到最后一次确认,取消上次PUBREL的消息重发机制
*
* @param ctx
* @param mqttMessage
* @return void
* @author zhouwenjie
* @date 2023/6/9 16:00
*/
public void receivePubcomp(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
MqttPubReplyMessageVariableHeader variableHeader = (MqttPubReplyMessageVariableHeader) mqttMessage.variableHeader();
int messageId = variableHeader.messageId();
ScheduledFuture<?> scheduledFuture = TimerData.scheduledFutureMap.remove(messageId);
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
ByteBuf byteBuf = cacheRepeatMessages.remove(ctx.channel().id().toString());
if (byteBuf != null) {
byteBuf.release();
}
}
}
消息重发机制
/**
* @author: zhouwenjie
* @description: 轮询线程信息存储
* @create: 2021-04-29 08:06
**/
public class TimerData {
public static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(10);
public static ConcurrentHashMap<Integer, ScheduledFuture<?>> scheduledFutureMap = new ConcurrentHashMap<>();
}
/**
* @author: zhouwenjie
* @description: 判断策略相关消息是否在规定时间段内发送,获取结束状态
* @create: 2021-01-07 16:09
**/
@Slf4j
public class MonitorMsgTime implements Runnable {
private Integer packetId;
private MqttMessage mqttMessage;
private ChannelHandlerContext ctx;
public MonitorMsgTime(Integer packetId, MqttMessage mqttMessage, ChannelHandlerContext ctx) {
this.packetId = packetId;
this.mqttMessage = mqttMessage;
this.ctx = ctx;
}
@Override
public void run() {
//注意,整个执行过程中,代码报错,线程就会终止
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
if (ctx != null && ctx.channel().isActive()) {
log.info("重复发送消息给客户端:" + address.getHostString());
if (mqttMessage instanceof MqttPublishMessage) {
//推送的原始消息,每次推送,都需要重新拷贝一份
try {
MqttPublishMessage mqttPublishMessage = (MqttPublishMessage) mqttMessage;
ByteBuf byteBuf = mqttPublishMessage.payload();
byteBuf.retainedDuplicate();
ctx.writeAndFlush(mqttPublishMessage);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
} else {
//回复的ack类型消息
ctx.writeAndFlush(mqttMessage);
}
} else {
log.error(address.getHostString() + " 客户端断开,结束重复发送");
//如果离线了,就不发了
ScheduledFuture<?> scheduledFuture = TimerData.scheduledFutureMap.remove(packetId);
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
}
}
}
测试
这里使用MQTT.fx工具,免费使用版本1.7.1,下载地址,密码:6nst。
这个软件是可以多开的,多开的时候记得设置好,尤其是clientId,不要一样。
中文可能会乱码,这个工具好像不支持中文。
其他
如果想了解更详细的协议可以参考这篇文章,MQTT协议分析。
客户端实现请参考:springboot+netty+mqtt客户端实现