All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.azure.iothub.transport.mqtt.MqttIotHubConnection Maven / Gradle / Ivy

There is a newer version: 1.0.16
Show newest version
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package com.microsoft.azure.iothub.transport.mqtt;

import com.microsoft.azure.iothub.DeviceClientConfig;
import com.microsoft.azure.iothub.IotHubStatusCode;
import com.microsoft.azure.iothub.Message;
import com.microsoft.azure.iothub.auth.IotHubSasToken;
import com.microsoft.azure.iothub.net.IotHubUri;
import com.microsoft.azure.iothub.transport.TransportUtils;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

public class MqttIotHubConnection implements MqttCallback
{
    /** The MQTT connection lock. */
    protected final Object MQTT_CONNECTION_LOCK = new Object();

    private MqttAsyncClient asyncClient;

    public enum ConnectionState
    {
        OPEN, CLOSED
    }

    protected final DeviceClientConfig config;
    protected ConnectionState state = ConnectionState.CLOSED;

    // paho mqtt only supports 10 messages in flight at the same time
    private static final int maxInFlightCount = 10;

    private Queue receivedMessagesQueue  = new LinkedBlockingQueue<>();

    private String publishTopic;
    private String subscribeTopic;
    private MqttConnectOptions connectionOptions = new MqttConnectOptions();
    private String iotHubUserName;

    private int inFlightCount = 0;

    //mqtt connection options
    private static final int keepAliveInterval = 20;
    private static final int mqttVersion = 4;
    private static final boolean setCleanSession = false;
    private static final int qos = 1;

    //string constants
    private static String sslPrefix = "ssl://";
    private static String sslPortSuffix = ":8883";

    /**
     * Constructs an instance from the given {@link DeviceClientConfig}
     * object.
     *
     * @param config the client configuration.
     */
    public MqttIotHubConnection(DeviceClientConfig config) throws IllegalArgumentException
    {
        synchronized (MQTT_CONNECTION_LOCK)
        {
            // Codes_SRS_MQTTIOTHUBCONNECTION_15_003: [The constructor shall throw a new IllegalArgumentException
            // if any of the parameters of the configuration is null or empty.]
            if(config == null){
                throw new IllegalArgumentException("The DeviceClientConfig cannot be null.");
            }
            if(config.getIotHubHostname() == null || config.getIotHubHostname().length() == 0)
            {
                throw new IllegalArgumentException("hostName cannot be null or empty.");
            }
            if (config.getDeviceId() == null || config.getDeviceId().length() == 0)
            {
                throw new IllegalArgumentException("deviceID cannot be null or empty.");
            }
            if(config.getIotHubName() == null || config.getIotHubName().length() == 0)
            {
                throw new IllegalArgumentException("hubName cannot be null or empty.");
            }
            if(config.getDeviceKey() == null || config.getDeviceKey().length() == 0)
            {
                throw new IllegalArgumentException("deviceKey cannot be null or empty.");
            }

            // Codes_SRS_MQTTIOTHUBCONNECTION_15_001: [The constructor shall save the configuration.]
            this.config = config;

            // Codes_SRS_MQTTIOTHUBCONNECTION_15_002: [The constructor shall create the publish and subscribe
            // topic for the specified device id.]
            this.publishTopic = "devices/" + this.config.getDeviceId() + "/messages/events/";
            this.subscribeTopic = "devices/" + this.config.getDeviceId() + "/messages/devicebound/#";
        }
    }

    /**
     * Establishes a connection for the device and IoT Hub given in the client
     * configuration. If the connection is already open, the function shall do
     * nothing.
     *
     * @throws IOException if a connection could not to be established.
     */
    public void open() throws IOException
    {
        synchronized (MQTT_CONNECTION_LOCK)
        {
            //Codes_SRS_MQTTIOTHUBCONNECTION_15_006: [If the MQTT connection is already open,
            // the function shall do nothing.]
            if (this.state == ConnectionState.OPEN)
            {
                return;
            }

            // Codes_SRS_MQTTIOTHUBCONNECTION_15_004: [The function shall establish an MQTT connection
            // with an IoT Hub using the provided host name, user name, device ID, and sas token.]
            try
            {
                IotHubSasToken sasToken = new IotHubSasToken(IotHubUri.getResourceUri(this.config.getIotHubHostname(), this.config.getDeviceId()),
                        this.config.getDeviceKey(),
                        System.currentTimeMillis() / 1000l + this.config.getTokenValidSecs() + 1l);


                this.asyncClient = new MqttAsyncClient(sslPrefix + this.config.getIotHubHostname() + sslPortSuffix,
                        this.config.getDeviceId(), new MemoryPersistence());
                asyncClient.setCallback(this);

                String clientIdentifier = "DeviceClientType=" + URLEncoder.encode(TransportUtils.javaDeviceClientIdentifier + TransportUtils.clientVersion, "UTF-8");
                this.iotHubUserName = this.config.getIotHubHostname() + "/" + this.config.getDeviceId() + "/" + clientIdentifier;

                this.updateConnectionOptions(this.iotHubUserName, sasToken.toString());
                this.connect(connectionOptions);

                this.subscribe();

                this.state = ConnectionState.OPEN;
            }
            // Codes_SRS_MQTTIOTHUBCONNECTION_15_005: [If an MQTT connection is unable to be established
            // for any reason, the function shall throw an IOException.]
            catch (MqttException e)
            {
                throw new IOException("Error initializing MQTT connection:" + e.getMessage());
            }
        }
    }

    /**
     * Closes the connection. After the connection is closed, it is no longer usable.
     * If the connection is already closed, the function shall do nothing.
     *
     */
    public void close()
    {
        synchronized (MQTT_CONNECTION_LOCK)
        {
            // Codes_SRS_MQTTIOTHUBCONNECTION_15_007: [If the MQTT session is closed, the function shall do nothing.]
            if (this.state == ConnectionState.CLOSED) {
                return;
            }

            // Codes_SRS_MQTTIOTHUBCONNECTION_15_006: [**The function shall close the MQTT connection.]
            try
            {
                IMqttToken disconnectToken = this.asyncClient.disconnect();
                disconnectToken.waitForCompletion();
            }
            catch (MqttException e)
            {
                //do nothing, since connection is closed anyway.
            }

            this.state = ConnectionState.CLOSED;
            this.asyncClient = null;
        }
    }

    /**
     * Sends an event message.
     *
     * @param message the event message.
     *
     * @return the status code from sending the event message.
     *
     * @throws IllegalStateException if the MqttIotHubConnection is not open
     */
    public IotHubStatusCode sendEvent(Message message) throws IllegalStateException
    {
        synchronized (MQTT_CONNECTION_LOCK)
        {
            // Codes_SRS_MQTTIOTHUBCONNECTION_15_010: [If the message is null or empty,
            // the function shall return status code BAD_FORMAT.]
            if (message == null || message.getBytes() == null || message.getBytes().length == 0)
            {
                return IotHubStatusCode.BAD_FORMAT;
            }

            // Codes_SRS_MQTTIOTHUBCONNECTION_15_013: [If the MQTT connection is closed,
            // the function shall throw an IllegalStateException.]
            if (this.state == ConnectionState.CLOSED)
            {
                throw new IllegalStateException("Cannot send event using a closed MQTT connection");
            }

            // Codes_SRS_MQTTIOTHUBCONNECTION_15_008: [The function shall send an event message
            // to the IoT Hub given in the configuration.]
            // Codes_SRS_MQTTIOTHUBCONNECTION_15_009: [The function shall send the message payload.]
            // Codes_SRS_MQTTIOTHUBCONNECTION_15_011: [If the message was successfully received by the service,
            // the function shall return status code OK_EMPTY.]
            IotHubStatusCode result = IotHubStatusCode.OK_EMPTY;

            try
            {
                inFlightCount++;
                while (inFlightCount >= maxInFlightCount)
                {
                    Thread.sleep(10);
                }

                MqttMessage mqttMessage = new MqttMessage(message.getBytes());
                mqttMessage.setQos(qos);

                IMqttDeliveryToken publishToken = asyncClient.publish(publishTopic, mqttMessage);

            }
            // Codes_SRS_MQTTIOTHUBCONNECTION_15_012: [If the message was not successfully
            // received by the service, the function shall return status code ERROR.]
            catch(Exception e)
            {
                result = IotHubStatusCode.ERROR;
            }

            return result;
        }
    }

    /**
     * Receives a message, if one exists.
     *
     * @return the message received, or null if none exists.
     *
     * @throws IllegalStateException if the connection state is currently closed.
     */
    public Message receiveMessage() throws IllegalStateException
    {
        // Codes_SRS_MQTTIOTHUBCONNECTION_15_015: [If the MQTT connection is closed,
        // the function shall throw an IllegalStateException.]
        if (this.state == ConnectionState.CLOSED)
        {
            throw new IllegalStateException("The MQTT connection is currently closed. Call open() before attempting" +
                    "to receive a message.");
        }

        Message message = null;

        // Codes_SRS_MQTTIOTHUBCONNECTION_15_014: [The function shall attempt to consume a message
        // from the received messages queue.]

        if (this.receivedMessagesQueue.size() > 0)
        {
            message = this.receivedMessagesQueue.remove();
        }

        return message;
    }

    /**
     * Event fired when the connection with the MQTT broker is lost.
     * @param throwable Reason for losing the connection.
     */
    @Override
    public void connectionLost(Throwable throwable)
    {
        synchronized (MQTT_CONNECTION_LOCK)
        {
            // The connection was closed by calling the close() method, meaning we don't want to re-establish it.
            if (this.asyncClient == null)
            {
                return;
            }

            // Codes_SRS_MQTTIOTHUBCONNECTION_15_016: [The function shall attempt to reconnect to the IoTHub
            // in a loop with an exponential backoff until it succeeds]
            this.state = ConnectionState.CLOSED;
            int currentReconnectionAttempt = 0;
            while (this.state == ConnectionState.CLOSED)
            {
                try
                {
                    this.open();
                }
                catch (IOException e)
                {
                    try
                    {
                        currentReconnectionAttempt++;

                        // Codes_SRS_MQTTIOTHUBCONNECTION_15_018: [The maximum wait interval
                        // until a reconnect is attempted shall be 60 seconds.]
                        Thread.sleep(generateSleepInterval(currentReconnectionAttempt) * 1000);
                    }
                    catch (InterruptedException exception)
                    {
                        // do nothing, reconnection attempts will continue
                    }
                }
            }
        }
    }

    @Override
    public void messageArrived(String topic, MqttMessage mqttMessage)
    {
        // Codes_SRS_MQTTIOTHUBCONNECTION_15_019: [A new message is created using the payload
        // from the received MqttMessage]
        Message message = new Message(mqttMessage.getPayload());

        // Codes_SRS_MQTTIOTHUBCONNECTION_15_020: [The newly created message is added to the received messages queue.]

        this.receivedMessagesQueue.add(message);
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken)
    {
        inFlightCount--;
    }

    private void connect(MqttConnectOptions connectionOptions) throws MqttException
    {
        if (!this.asyncClient.isConnected())
        {
            IMqttToken connectToken = this.asyncClient.connect(connectionOptions);
            connectToken.waitForCompletion();
        }
    }

    private void subscribe() throws MqttException
    {
        IMqttToken subToken = this.asyncClient.subscribe(this.subscribeTopic, qos);
        subToken.waitForCompletion();
    }

    /**
     * Generates the connection options for the mqtt broker connection.
     *
     * @param userName the user name for the mqtt broker connection.
     * @param userPassword the user password for the mqtt broker connection.
     */
    private void updateConnectionOptions(String userName, String userPassword)
    {
        this.connectionOptions.setKeepAliveInterval(keepAliveInterval);
        this.connectionOptions.setCleanSession(setCleanSession);
        this.connectionOptions.setMqttVersion(mqttVersion);
        this.connectionOptions.setUserName(userName);
        this.connectionOptions.setPassword(userPassword.toCharArray());
    }

    private static byte[] sleepIntervals = {1, 2, 4, 8, 16, 32, 60};
    /** Generates a reconnection time with an exponential backoff
     * and a maximum value of 60 seconds.
     *
     * @param currentAttempt the number of attempts
     * @return the sleep interval until the next attempt.
     */
    private byte generateSleepInterval(int currentAttempt)
    {
        if (currentAttempt > 7)
        {
            return sleepIntervals[6];
        }
        else if (currentAttempt > 0)
        {
            return sleepIntervals[currentAttempt - 1];
        }
        else
        {
            return 0;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy