MQTT-Java 连接协议

329 阅读5分钟

目标

理解客户端连接MQTT服务器协议

准备

参见 “MQTT 实践” 完成测试工程配置

客户端初始化整体流程

MQTT-JAVA版本.jpg

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-JAVA-连接协议.jpg

MQTT 协议规范对照解析

MQTT 协议结构

整体结构 :固定头+可变头+内容

协议-包结构.png

连接协议详细解析

连接数据 : 头
请求数据头日志

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

协议-连接-固定头.png

固定头, 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

协议-连接-协议名称.png

  • 日志中,从第三个字节开始的第7个字节【Protocol Version】: 00000101

协议-连接-协议版本号.png

  • 日志中,从第三个字节开始的第8个字节【Connect Flags】:00000010

协议-连接-标识.png

  • 日志中,从第三个字节开始的第9个字节【Keep Alive】:00000000 00111100
    其值为60,单位为秒

协议-连接-保活.png

  • 日志中,从第三个字节开始的第11个字节【Property Length】:00000000

协议-连接-属性.png