目标
理解客户端连接MQTT服务器协议
准备
参见 “MQTT 实践” 完成测试工程配置
客户端初始化整体流程
MQTT客户端工程代码修改
连接测试代码
import org.eclipse.paho.mqttv5.client.*;
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.eclipse.paho.mqttv5.common.MqttSubscription;
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
import org.eclipse.paho.mqttv5.common.packet.UserProperty;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
public static void main(String[] args) {
String topic = "harvey";
String content = "Message from MQTTHarvey'InteIIiJ IDEA ";
int qos = 2;
String broker = "tcp://127.0.0.1:1883";
String clientId = "InteIIiJ IDEA Client 1";
MemoryPersistence persistence = new MemoryPersistence();
try {
MqttConnectionOptions connOpts = new MqttConnectionOptions();
MqttAsyncClient sampleClient = new MqttAsyncClient(broker, clientId, persistence);
//开始连接服务器
IMqttToken token = sampleClient.connect(connOpts);
token.waitForCompletion();
System.out.println("Connected");
} catch(MqttException me) {
System.out.println("reason "+me.getReasonCode());
System.out.println("msg "+me.getMessage());
System.out.println("loc "+me.getLocalizedMessage());
System.out.println("cause "+me.getCause());
System.out.println("excep "+me);
me.printStackTrace();
}
}
}
客户端发送信息至MQTT远端调试日志
MqttOutputStream.java文件
/**
* Writes an <code>MqttWireMessage</code> to the stream.
* @param message The {@link MqttWireMessage} to send
* @throws IOException if an exception is thrown when writing to the output stream.
* @throws MqttException if an exception is thrown when getting the header or payload
*/
public void write(MqttWireMessage message) throws IOException, MqttException {
final String methodName = "write";
byte[] bytes = message.getHeader();
byte[] pl = message.getPayload();
if(this.clientState.getOutgoingMaximumPacketSize() != null &&
bytes.length+pl.length > this.clientState.getOutgoingMaximumPacketSize() ) {
// Outgoing packet is too large
throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_OUTGOING_PACKET_TOO_LARGE);
}
//调试日志S BY HARVEY
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");//设置日期格式
String date = df.format(new Date());// new Date()为获取当前系统时间,也可使用当前时间戳
StringBuffer sb = new StringBuffer();
for(byte b : bytes){
sb.append(Tools.toBinaryString(b&0xff)).append(" ");
}
System.out.println(date + " " +"Send Message Header(二进制内容) : " + sb.toString());
System.out.println(date + " " +"Send Message Header(字符串内容) : " + new String(bytes, "UTF-8"));
//调试日志E
out.write(bytes,0,bytes.length);
clientState.notifySentBytes(bytes.length);
int offset = 0;
int chunckSize = 1024;
while (offset < pl.length) {
int length = Math.min(chunckSize, pl.length - offset);
out.write(pl, offset, length);
offset += chunckSize;
clientState.notifySentBytes(length);
}
//调试日志 BY HARVEY
System.out.println(date + " " +"Send Message payload(字符串内容) : " + pl.length + " " + new String(pl, "UTF-8"));
// @TRACE 529= sent {0}
log.fine(CLASS_NAME, methodName, "529", new Object[]{message});
}
客户端接收MQTT远端消息调试日志
MqttInputStream.java文件
/**
* Reads an <code>MqttWireMessage</code> from the stream.
* If the message cannot be fully read within the socket read timeout,
* a null message is returned and the method can be called again until
* the message is fully read.
* @return The {@link MqttWireMessage}
* @throws IOException if an exception is thrown when reading from the stream
* @throws MqttException if the message is invalid
*/
public MqttWireMessage readMqttWireMessage() throws IOException, MqttException {
final String methodName ="readMqttWireMessage";
MqttWireMessage message = null;
try {
// read header
if (remLen < 0) {
// Assume we can read the whole header at once.
// The header is very small so it's likely we
// are able to read it fully or not at all.
// This keeps the parser lean since we don't
// need to cope with a partial header.
// Should we lose synch with the stream,
// the keepalive mechanism would kick in
// closing the connection.
bais.reset();
byte first = in.readByte();
clientState.notifyReceivedBytes(1);
byte type = (byte) ((first >>> 4) & 0x0F);
if ((type < MqttWireMessage.MESSAGE_TYPE_CONNECT) ||
(type > MqttWireMessage.MESSAGE_TYPE_AUTH)) {
// Invalid MQTT message type...
throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_INVALID_MESSAGE);
}
//调试日志S BY HARVEY
String firstByte = Integer.toBinaryString(first&0xff);
final String[] PACKET_NAMES = { "reserved", "CONNECT", "CONNACK", "PUBLISH", "PUBACK", "PUBREC",
"PUBREL", "PUBCOMP", "SUBSCRIBE", "SUBACK", "UNSUBSCRIBE", "UNSUBACK", "PINGREQ", "PINGRESP", "DISCONNECT",
"AUTH" };
System.out.println("接收到第一个字节 : " + firstByte + ", 包类型:" + PACKET_NAMES[type]);
//调试日志E
byte reserved = (byte) (first & 0x0F);
MqttWireMessage.validateReservedBits(type, reserved);
remLen = MqttDataTypes.readVariableByteInteger(in).getValue();
bais.write(first);
bais.write(MqttWireMessage.encodeVariableByteInteger((int)remLen));
packet = new byte[(int)(bais.size()+remLen)];
if(this.clientState.getIncomingMaximumPacketSize() != null &&
bais.size()+remLen > this.clientState.getIncomingMaximumPacketSize() ) {
// Incoming packet is too large
throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_INCOMING_PACKET_TOO_LARGE);
}
packetLen = 0;
}
// read remaining packet
if (remLen >= 0) {
// the remaining packet can be read with timeouts
readFully();
// reset packet parsing state
remLen = -1;
byte[] header = bais.toByteArray();
System.arraycopy(header,0,packet,0, header.length);
message = MqttWireMessage.createWireMessage(packet);
//调试日志S BY HARVEY
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");//设置日期格式
String date = df.format(new Date());// new Date()为获取当前系统时间,也可使用当前时间戳
byte [] hs = message.getHeader();
StringBuilder sb = new StringBuilder();
for(byte b : hs){
sb.append(Tools.toBinaryString(b&0xff)).append(" ");
}
System.out.println(date + " " +"Receive Message Header(二进制内容) : " + sb.toString());
System.out.println(date + " " +"Receive Message Header(字符串内容) : " + new String(hs, "UTF-8"));
byte [] ps = message.getPayload();
System.out.println(date + " " +"Receive Message payload(字符串内容) : " + new String(ps, "UTF-8"));
//调试日志E
// @TRACE 530= Received {0}
log.fine(CLASS_NAME, methodName, "530",new Object[] {message});
}
} catch (SocketTimeoutException e) {
// ignore socket read timeout
}
return message;
}
客户端连接MQTT服务器日志
2023-08-24 20:03:45:486 **Send Message Header**(二进制内容) : 00010000 00100011 00000000 00000100 01001101 01010001 01010100 01010100 00000101 00000010 00000000 00111100 00000000
2023-08-24 20:03:45:486 **Send Message Header**(字符串内容) : #MQTT<<br>
2023-08-24 20:03:45:486 **Send Message payload**(字符串内容) : 24 InteIIiJ IDEA Client 1
2023-08-24 20:03:45:489 **Receive Message Header**(二进制内容) : 00100000 00001001 00000000 00000000 00000110 00100001 00000000 00010100 00100010 00000000 00001010
2023-08-24 20:03:45:489 **Receive Message Header**(字符串内容) : !"
2023-08-24 20:03:45:489 **Receive Message payload**(字符串内容) :
Connected
日志“二进制内容”分析
MQTT 协议规范对照解析
MQTT 协议结构
整体结构 :固定头+可变头+内容
连接协议详细解析
连接数据 : 头
请求数据头日志
2023-08-24 20:03:45:486 **Send Message Header**(二进制内容) : 00010000 00100011 00000000 00000100 01001101 01010001 01010100 01010100 00000101 00000010 00000000 00111100 00000000
参见 “3.1 CONNECT – Connection Request”
固定头, 2 个字节
0001000 : 代表CONNECT
00100011 : 代表剩余数据长度(从数据头第三个字节开始 + payload字节数):35
可变头,11 个字节
Protocol Name(6个字节) + Protocol Version(1个字节) + Connect Flags(1个字节) + Keep Alive(2个字节)+ Property Length(1个字节)
- 日志中,从第三个字节开始的6个字节【Protocol Name】: 00000000 00000100 01001101 01010001 01010100 01010100
- 日志中,从第三个字节开始的第7个字节【Protocol Version】: 00000101
- 日志中,从第三个字节开始的第8个字节【Connect Flags】:00000010
- 日志中,从第三个字节开始的第9个字节【Keep Alive】:00000000 00111100
其值为60,单位为秒
- 日志中,从第三个字节开始的第11个字节【Property Length】:00000000