笔者在上一章对连接报文进行了相关的讲解。这一章笔者想写一个连接报文的例子来加深理解。本来这一章也应该在上一章出现的。可是笔者怕太长了。不好方便阅。所以决定分俩章来。正如笔者上一章所讲的。笔者会用Netty通信框架进行编写。主要因为Netty已经为我们集成了相关MQTT功能。

开发环境

开发工具:intellij idea.(以前我一直在eclipse。最近新版的老报错。所以就放弃了)

Netty包:netty-all-4.1.16.Final.jar。下载网站:http://netty.io/downloads.html

JDK:JAVA 8

第三包:commons-lang3-3.6.jar。下载网站:http://commons.apache.org/proper/commons-lang/download_lang.cgi

MQTT编写

在这里笔者并不打包把客户端的代码一起编写出。事实上关于客户端的开源的代码是非常多的。笔者这里只会略微的编写一下服务端的代码。当然这里代码只是为方更了解MQTT协议。并非企业级的编蜜枣这一点希望读者见谅。为了实现连接报文。笔者定义了三个类。

Main类:用于启动服务。

BrokerHandler类:处理接受来的信息。

BrokerSessionHelper:用于发送信息给客户。

Main类的源码

 public static void main(String[] args) throws Exception  {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup(); Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}); ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
17 .childHandler(new ChannelInitializer<SocketChannel>() {
18 @Override
19 public void initChannel(SocketChannel ch) throws Exception {
20
21 ChannelPipeline p = ch.pipeline();
22
23 p.addFirst("idleHandler", new IdleStateHandler(0, 0, 120));
24 p.addLast("encoder", MqttEncoder.INSTANCE);
25 p.addLast("decoder", new MqttDecoder());
26 p.addLast("logicHandler", new BrokerHandler(65535));
27
28 }
})
.option(ChannelOption.SO_BACKLOG, 511)
.childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture f = b.bind("0.0.0.0", 1883).sync(); f.channel().closeFuture().sync();
}

上面的1、2俩行表是Netty里面俩个线程组。事实上也就是Reactor线程组。bossGroup 用于处理接受来自客户端的连接。workerGroup 用于处理接受客户端的读取信息。13行的ServerBootstrap可以理解为启动服务的一个引导类。主要关键是他的group方法。这样子就可以把俩个线程组关系在一起了。重点就在17行这里。childHandler用于处理IO事件。比如读取客户端进行。然后自己编码。你们可以看到24行的MqttEncoder.INSTANCE和25行的MqttDecoder吧。他们就是用于处理MQTT协议传来的信息进行处理。而26行BrokerHandler类就是笔者来处理每一个报文对应的响应。笔者就不在过多的说了。如果你们不懂的话,可以去看一下Netty框架的知识在过看的话会比较好。

BrokerHandler类的源码

 public class BrokerHandler extends SimpleChannelInboundHandler<MqttMessage> {
private MqttVersion version;
private String clientId;
private String userName;
private String brokerId;
private boolean connected;
private boolean cleanSession;
private int keepAlive;
private int keepAliveMax;
private MqttPublishMessage willMessage; public BrokerHandler(int keepAliveMax) { this.keepAliveMax = keepAliveMax;
} @Override
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
protected void channelRead0(ChannelHandlerContext ctx, MqttMessage msg) throws Exception { if (msg.decoderResult().isFailure()) { Throwable cause = msg.decoderResult().cause(); if (cause instanceof MqttUnacceptableProtocolVersionException) { BrokerSessionHelper.sendMessage(
ctx,
MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION, false),
null),
"INVALID",
null,
true); } else if (cause instanceof MqttIdentifierRejectedException) { BrokerSessionHelper.sendMessage(
ctx,
MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED, false),
null),
"INVALID",
null,
true);
} ctx.close(); return;
} switch (msg.fixedHeader().messageType()) {
case CONNECT:
onConnect(ctx, (MqttConnectMessage) msg);
break;
case PUBLISH:
onPublish(ctx, (MqttPublishMessage) msg);
break;
case PUBACK:
onPubAck(ctx, msg);
break;
case PUBREC:
onPubRec(ctx, msg);
break;
case PUBREL:
onPubRel(ctx, msg);
break;
case PUBCOMP:
onPubComp(ctx, msg);
break;
case SUBSCRIBE:
onSubscribe(ctx, (MqttSubscribeMessage) msg);
break;
case UNSUBSCRIBE:
onUnsubscribe(ctx, (MqttUnsubscribeMessage) msg);
break;
case PINGREQ:
onPingReq(ctx);
break;
case DISCONNECT:
onDisconnect(ctx);
break;
} } private void onConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { this.version = MqttVersion.fromProtocolNameAndLevel(msg.variableHeader().name(), (byte) msg.variableHeader().version());
this.clientId = msg.payload().clientIdentifier();
this.cleanSession = msg.variableHeader().isCleanSession(); if (msg.variableHeader().keepAliveTimeSeconds() > 0 && msg.variableHeader().keepAliveTimeSeconds() <= this.keepAliveMax) {
this.keepAlive = msg.variableHeader().keepAliveTimeSeconds();
} //MQTT 3.1之后可能存在为空的客户ID。所以要进行处理。如果客户ID是空,而且还在保存处理相关的信息。这样子是不行。
//必须有客户ID我们才能存保相关信息。
if (StringUtils.isBlank(this.clientId)) {
if (!this.cleanSession) { BrokerSessionHelper.sendMessage(
ctx,
MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED, false),
null),
"INVALID",
null,
true); ctx.close(); return; } else {
this.clientId = java.util.UUID.randomUUID().toString();
}
} //有可能发送俩次的连接包。如果已经存在连接就是关闭当前的连接。
if (this.connected) {
ctx.close();
return;
} boolean userNameFlag = msg.variableHeader().hasUserName();
boolean passwordFlag = msg.variableHeader().hasPassword();
this.userName = msg.payload().userName(); String password = "" ;
if( msg.payload().passwordInBytes() != null && msg.payload().passwordInBytes().length > 0)
password = new String(msg.payload().passwordInBytes()); boolean mistake = false; //如果有用户名标示,那么就必须有密码标示。
//当有用户名标的时候,用户不能为空。
//当有密码标示的时候,密码不能为空。
if (userNameFlag) {
if (StringUtils.isBlank(this.userName))
mistake = true;
} else {
if (StringUtils.isNotBlank(this.userName) || passwordFlag) mistake = true;
} if (passwordFlag) { if (StringUtils.isBlank(password)) mistake = true;
} else {
if (StringUtils.isNotBlank(password)) mistake = true;
} if (mistake) {
BrokerSessionHelper.sendMessage(
ctx,
MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, false),
null),
this.clientId,
null,
true);
ctx.close();
return;
} BrokerSessionHelper.sendMessage(
ctx,
MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, !this.cleanSession),
null),
this.clientId,
null,
true); ChannelHandlerContext lastSession = BrokerSessionHelper.removeSession(this.clientId);
if (lastSession != null) {
lastSession.close();
} String willTopic = msg.payload().willTopic();
String willMessage = "";
if(msg.payload().willMessageInBytes() != null && msg.payload().willMessageInBytes().length > 0)
willMessage = new String(msg.payload().willMessageInBytes()); if (msg.variableHeader().isWillFlag() && StringUtils.isNotEmpty(willTopic) && StringUtils.isNotEmpty(willMessage)) { this.willMessage = (MqttPublishMessage) MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.PUBLISH, false, MqttQoS.valueOf(msg.variableHeader().willQos()), msg.variableHeader().isWillRetain(), 0),
new MqttPublishVariableHeader(willTopic, 0),
Unpooled.wrappedBuffer(willMessage.getBytes())
);
} this.connected = true;
BrokerSessionHelper.saveSession(this.clientId, ctx);
} private void onSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage msg) {
} private void onUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage msg) {
} private void onPingReq(ChannelHandlerContext ctx) {
} private void onDisconnect(ChannelHandlerContext ctx) { if (!this.connected) {
ctx.close();
return;
} BrokerSessionHelper.removeSession(this.clientId, ctx); this.willMessage = null; this.connected = false; ctx.close(); } private void onPubComp(ChannelHandlerContext ctx, MqttMessage msg) { } private void onPubRel(ChannelHandlerContext ctx, MqttMessage msg) {
} private void onPubRec(ChannelHandlerContext ctx, MqttMessage msg) {
} private void onPubAck(ChannelHandlerContext ctx, MqttMessage msg) {
} private void onPublish(ChannelHandlerContext ctx, MqttPublishMessage msg) {
}
}

BrokerHandler类

19 行中的channelRead0方法中有俩个参数。一个为ChannelHandlerContext(通首的上下文)。一个是MqttMessage(客户端来的MQTT报文)。我们接下来动作都是跟MqttMessage来做相关的逻辑处理。这一点从55行就可以看出来。我们可以判断他是什么类型的报文。笔者这里只实现连接报文的处理。21行的代码msg.decoderResult().isFailure()是用来判断传过来的报文是不是正确的。事实上是Netty框架帮我们做了第一层的验证。23行就是获得发生的异常。

从第99行onConnect方法开始就是处理连接报文的处理。笔者这里只做下面相关的处理。

1.验证保持连接(Keep Alive)的有效性。代码如下

      if (msg.variableHeader().keepAliveTimeSeconds() > 0 && msg.variableHeader().keepAliveTimeSeconds() <= this.keepAliveMax) {
this.keepAlive = msg.variableHeader().keepAliveTimeSeconds();
}

2.验证客户ID为空的时候,还要求保存会话状。这是不合理的。因为我的会话状态是跟根客户ID来保存。否则的话,随更给一个。反正后面还是清除会话状态。那么为什么会有空的呢?主要是在MQTT 3.1.1里面指出客户ID可以为空了。

  if (StringUtils.isBlank(this.clientId)) {
if (!this.cleanSession) { BrokerSessionHelper.sendMessage(
ctx,
MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED, false),
null),
"INVALID",
null,
true); ctx.close(); return; } else {
this.clientId = java.util.UUID.randomUUID().toString();
}
}

3.判断是否是第二次连接报文。如果是的话,就要断开了。

  if (this.connected) {
ctx.close();
return;
}

4.判断用户和密码是否合法性。比如上一章出讲到的只有在用户名标志为1的时候,密码才可以出现。

   boolean userNameFlag = msg.variableHeader().hasUserName();
boolean passwordFlag = msg.variableHeader().hasPassword();
this.userName = msg.payload().userName(); String password = "" ;
if( msg.payload().passwordInBytes() != null && msg.payload().passwordInBytes().length > 0)
password = new String(msg.payload().passwordInBytes()); boolean mistake = false; //如果有用户名标示,那么就必须有密码标示。
//当有用户名标的时候,用户不能为空。
//当有密码标示的时候,密码不能为空。
if (userNameFlag) {
if (StringUtils.isBlank(this.userName))
mistake = true;
} else {
if (StringUtils.isNotBlank(this.userName) || passwordFlag) mistake = true;
} if (passwordFlag) { if (StringUtils.isBlank(password)) mistake = true;
} else {
if (StringUtils.isNotBlank(password)) mistake = true;
} if (mistake) {
BrokerSessionHelper.sendMessage(
ctx,
MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, false),
null),
this.clientId,
null,
true);
ctx.close();
return;
}

6.接受客户端了。事实上笔者还有很多没有做的事情。比如保存会状态的处理。因为主要是为学习所以就没有讲出来。在加上会话状态存保就要思考保存在哪里。同时还有一个就是用户的合法性验证没有处理。

   BrokerSessionHelper.sendMessage(
ctx,
MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, !this.cleanSession),
null),
this.clientId,
null,
true);

7.处理当前报文的遗嘱。

    String willTopic = msg.payload().willTopic();
String willMessage = "";
if(msg.payload().willMessageInBytes() != null && msg.payload().willMessageInBytes().length > 0)
willMessage = new String(msg.payload().willMessageInBytes()); if (msg.variableHeader().isWillFlag() && StringUtils.isNotEmpty(willTopic) && StringUtils.isNotEmpty(willMessage)) { this.willMessage = (MqttPublishMessage) MqttMessageFactory.newMessage(
new MqttFixedHeader(MqttMessageType.PUBLISH, false, MqttQoS.valueOf(msg.variableHeader().willQos()), msg.variableHeader().isWillRetain(), 0),
new MqttPublishVariableHeader(willTopic, 0),
Unpooled.wrappedBuffer(willMessage.getBytes())
);
}

如果你看到这个类的最后代码的时候,会发现笔者也写了相关的ACNNACK响应。他的内容比较简单。大家看代码吧。

 private void onDisconnect(ChannelHandlerContext ctx) {

         if (!this.connected) {
ctx.close();
return;
} BrokerSessionHelper.removeSession(this.clientId, ctx); this.willMessage = null; this.connected = false; ctx.close(); }

BrokerSessionHelper类的源码

public class BrokerSessionHelper {

    private static final Map<String, ChannelHandlerContext> sessionRepository = new ConcurrentHashMap<>();

    public static void saveSession(String clientId, ChannelHandlerContext session) {
sessionRepository.put(clientId, session);
} public static ChannelHandlerContext getSession(String clientId) { return sessionRepository.get(clientId);
} public static ChannelHandlerContext removeSession(String clientId) { return sessionRepository.remove(clientId);
} public static boolean removeSession(String clientId, ChannelHandlerContext session) {
return sessionRepository.remove(clientId, session);
} /**
* 发送信息
*
* @param msg
* @param clientId
* @param packetId
* @param flush
*/
public static void sendMessage(MqttMessage msg, String clientId, Integer packetId, boolean flush) {
ChannelHandlerContext ctx = getSession(clientId);
if (ctx == null) {
String pid = packetId == null || packetId <= 0 ? "" : String.valueOf(packetId);
return;
}
sendMessage(ctx, msg, clientId, packetId, flush);
} /**
* 发送信息
*
* @param ctx
* @param msg
* @param clientId
* @param packetId
* @param flush
*/
public static void sendMessage(ChannelHandlerContext ctx, MqttMessage msg, String clientId, Integer packetId, boolean flush) {
String pid = packetId == null || packetId <= 0 ? "" : String.valueOf(packetId);
ChannelFuture future = flush ? ctx.writeAndFlush(msg) : ctx.write(msg);
future.addListener(f -> {
if (f.isSuccess()) { } else { }
});
}
}

BrokerSessionHelper类就是用于存放当前服务器上相关通道信息。同时用于发送返回的相关报文。读者们可以进行看代码吧。

这个时候就你们只按照以前面讲的去做。就可以抓到报文了。客户端的话。笔者只用前面说的MQTTLens来测试。

MQTT——编写连接报文的更多相关文章

  1. MQTT——连接报文

    学习MQTT协议.如果只是看了相关文档就认为可以了.那是一个错误的观念.笔者为了能更好的去理解MQTT协议.看了不少相关的开源Broker的项目.可惜这些项目一般都是不完全的.不过从这些项目中笔者至少 ...

  2. MQTT——取消订阅报文和断开连接报文

    笔者已经把连接报文,订阅报文,发布报文都讲解了完了.而接下来就是取消订阅报文和断开连接报文.和其他的报文比较的话,他们显示非常简单.甚至笔者觉得可以不必要拿出来讲.只要看一下MQTT文档就没有什么不清 ...

  3. eclipse编写连接MySQL的简单动态网页

    准备工作 下载Tomcat,建议使用最新版.下载并安装MySQL数据库,为了方便操作数据库,可以下载Navicat Premium,最新版不会提示不支持密码加密方式,所以下载最新版.除此之外,要想连接 ...

  4. MQTT v5.0------SUBSCRIBE 报文

    SUBSCRIBE 报文 固定报头: 剩余长度字段 表示可变报头的长度加上有效载荷的长度,被编码为变长字节整数. 可变报头 SUBSCRIBE报文可变报头按顺序包含以下字段:报文标识符(Packet ...

  5. 19-ESP8266 SDK开发基础入门篇--C# TCP客户端编写 , 连接和断开

    https://www.cnblogs.com/yangfengwu/p/11130428.html 渐渐的看过去,,,好多节了... 这节做一个C# TCP客户端 新建项目啥子的就不详细截图写了,自 ...

  6. C#与SQL Server连接时,如何编写连接字符串?

    一.Windows身份验证时: String conStr = "Data Source=数据库服务器地址;Initial Catalog=数据库名称;Integrated Security ...

  7. 关于MQTT连接的属性

    连接相关的属性. 这些属性是MQTT的连接报文中连接标志字, 包含一些用于指定 MQTT 连接行为的参数. 1.清理会话(Clean Session) 客户端和服务端可以保存会话状态,以支持跨网络连接 ...

  8. MQTT——控制报文格式

    解控制报文格式是学习MQTT中,笔者认为最为重要的一个知识点.MQTT的所有行为都离不开他.控制报文可以分为三个部分组成,分别为:固定报头.可以变报头.有效载荷部分. 注意:上面的说的报文的类型.是指 ...

  9. MQTT——订阅报文

    我们已经把相关的连接报文搞定了.笔者想来想去还是决定先讲解一下订阅报文(SUBSCRIBE ).如果传统的通信方式是客户端和服务端之间一般就直接传输信息.但是MQTT的通信方式是通过发布/订阅的方式进 ...

随机推荐

  1. JVM菜鸟进阶高手之路九(解惑)

    转载请注明原创出处,谢谢! 在第八系列最后有些疑惑的地方,后来还是在我坚持不懈不断打扰笨神,阿飞,ak大神等,终于解决了该问题.第八系列地址:http://www.jianshu.com/p/7f7c ...

  2. Servlet 3.0 使用注解配置URl提示404错误

    我的环境是  Eclipse oxygen + Servlet 3.0 因为3.0已经开始使用注解了 之前我都是配置listenner 还有Servlet mapping  在 web.xml 中 就 ...

  3. RabbitMQ消息队列之一:RabbitMQ的环境安装及配置

    RabbitMQ简介: MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法.应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们 ...

  4. easyui动态生成列

    需求:一个id对应多个key value 将id作为标识列 key值作为表头 value作为值显示.数据表可分为两张表 param数据表: 下表一个id对应上表多个key及value 如下图 id_p ...

  5. 关于 char 、 wchar_t 、 TCHAR 、 _T() ||| 宏 _T 、 TEXT 、 _TEXT 、 L

    char :单字节变量类型,最多表示256个字符,wchar_t :宽字节变量类型,用于表示Unicode字符,它实际定义在<string.h>里:typedef unsigned sho ...

  6. Mysql配置文件my.cnf详细说明

    [表名大小写配置] MySQL在Linux下数据库名.表名.列名.别名大小写规则:  1.数据库名与表名是严格区分大小写  2.表的别名是严格区分大小写  3.列名与列的别名在所有的情况下均是忽略大小 ...

  7. Laravel5 控制器

    Request 一.取值 1.取值 echo $request->input('name','这是默认值'); 2.取得所有值 $array=$request->all(); 3.判断值是 ...

  8. Android打包版本号设置

    之前没有设置过打包的命名,每次打包都是默认的"app-realease.apk",之后手动修改名字来显示出它是一个新版本. 晚上学习了如何配置打包名称,很简单,修改build.gr ...

  9. GCD SUM 强大的数论,容斥定理

    GCD SUM Time Limit: 8000/4000MS (Java/Others) Memory Limit: 128000/64000KB (Java/Others) SubmitStatu ...

  10. Nginx详细安装部署教程

    一.Nginx简介 Nginx是一个web服务器也可以用来做负载均衡及反向代理使用,目前使用最多的就是负载均衡,具体简介我就不介绍了百度一下有很多,下面直接进入安装步骤 二.Nginx安装 1.下载N ...