Java MQTT V3客户端使用实践

一、先通过一个测试Java MQTT 客户端是否支持多线程并发的实例大概看看如何使用:

生产者代码:

public class MqttClientPublish {

    private static AtomicInteger count = new AtomicInteger();
    private static int qos = 1;
    private static String clientId = "JavaClientPublish";
    private static String broker = "tcp://172.16.30.40:1883";
    private static String topic = "MQTT-JAVA-CLIENT-TEST";
    private static final Object lock = new Object();
    public static final int THREAD_COUNT = 100;
    private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);

    public static void main(String[] args) throws Exception {
        MemoryPersistence persistence = new MemoryPersistence();
        MqttAsyncClient client = new MqttAsyncClient(broker, clientId, persistence);
        CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT);
        MqttConnectOptions connOpts = new MqttConnectOptions();
        connOpts.setMaxInflight(THREAD_COUNT * 10);
        connOpts.setCleanSession(false);
        connOpts.setAutomaticReconnect(true);
        connOpts.setKeepAliveInterval(30);
        client.connect(connOpts,new IMqttActionListener(){
            //监听器快速返回控制非常重要,否则MQTT客户端的操作将会停止
            @Override
            public void onSuccess(IMqttToken asyncActionToken) {
                for (int i = 0; i < THREAD_COUNT; i++) {
                    new Sender(barrier, client).start();
                }
            }

            @Override
            public void onFailure(IMqttToken asyncActionToken, Throwable exception) {

            }
        });
        countDownLatch.await();

        System.out.println("发布完毕!");

    }

    private static class Sender extends Thread {
        private CyclicBarrier barrier;
        private MqttAsyncClient asyncClient;

        public Sender(CyclicBarrier barrier, MqttAsyncClient asyncClient) {
            this.barrier = barrier;
            this.asyncClient = asyncClient;
        }

        @Override
        public void run() {
            try {
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            int index = count.incrementAndGet();
            String content = index + "";
            MqttMessage message = new MqttMessage();
            message.setQos(qos);
            message.setPayload(content.getBytes());
            message.setId(index);
            try {
                asyncClient.publish(topic, message,null,new IMqttActionListener(){

                    @Override
                    public void onSuccess(IMqttToken asyncActionToken) {
                        countDownLatch.countDown();
                    }

                    @Override
                    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {

                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

订阅端代码:

public class MqttClientSubscribe {
    private static AtomicInteger total = new AtomicInteger();
    private static int qos = 1;
    private static String clientId = "JavaClientSubscribe";
    private static String broker = "tcp://172.16.30.40:1883";
    private static String topic = "MQTT-JAVA-CLIENT-TEST";
    private static CountDownLatch countDownLatch = new CountDownLatch(MqttClientPublish.THREAD_COUNT);
    public static void main(String[] args) throws Exception{
        MemoryPersistence persistence = new MemoryPersistence();
        MqttAsyncClient client = new MqttAsyncClient(broker, clientId, persistence);
        MqttConnectOptions connOpts = new MqttConnectOptions();
        connOpts.setCleanSession(false);
        client.connect(connOpts,new IMqttActionListener(){
            @Override
            public void onSuccess(IMqttToken asyncActionToken) {
                try {
                    client.subscribe(topic, qos, (String topic, MqttMessage message) -> {
                        String content = new String(message.getPayload());
                        System.out.println("content : " + content);
                        Integer index = Integer.valueOf(content);
                        total.addAndGet(index);
                        countDownLatch.countDown();
                    });
                }catch (Exception e){
                    e.printStackTrace();
                }
            }

            @Override
            public void onFailure(IMqttToken asyncActionToken, Throwable exception) {

            }
        });

        countDownLatch.await();
        System.out.println("total : " + total.get());
    }
}

先执行订阅端代码,再执行发布端代码,最后运行结果如下:
Java MQTT V3客户端使用实践_第1张图片

Java MQTT V3客户端使用实践_第2张图片

经过多次发布运行,最终结果始终是5050,说明这个客户端是线程安全的。

这里我只是测试客户端的线程是否安全,但是这里的使用方式还是不是特别好,连接和发送的回调是直接在连接和发送动作上面注册的IMqttActionListener接口的实现,这个动作回调会导致整个客户端阻塞,严重影响并发效率,最好的方式是在客户端上面注册MqttCallbackExtended的实现类,这样效率更高。

修改代码实现:

发布端:

public class MqttClientPublish {

    private static AtomicInteger count = new AtomicInteger();
    private static int qos = 1;
    private static String clientId = "JavaClientPublish";
    private static String broker = "tcp://172.16.30.40:1883";
    private static String topic = "MQTT-JAVA-CLIENT-TEST";
    private static final Object lock = new Object();
    public static final int THREAD_COUNT = 100;
    private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);

    public static void main(String[] args) throws Exception {
        MemoryPersistence persistence = new MemoryPersistence();
        MqttAsyncClient client = new MqttAsyncClient(broker, clientId, persistence, new TimerPingSender()
                , ThreadPoolUtil.buildScheduledThreadPoolExecutor(30, "publish-callback"));
        CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT);
        MqttConnectOptions connOpts = new MqttConnectOptions();
        connOpts.setMaxInflight(THREAD_COUNT * 10);
        connOpts.setCleanSession(false);
        connOpts.setAutomaticReconnect(true);
        connOpts.setKeepAliveInterval(30);
        client.setCallback(new MqttCallbackExtended() {

            @Override
            public void connectionLost(Throwable cause) {

            }

            @Override
            public void messageArrived(String topic, MqttMessage message) throws Exception {

            }

            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {
                countDownLatch.countDown();
            }

            @Override
            public void connectComplete(boolean reconnect, String serverURI) {
                for (int i = 0; i < THREAD_COUNT; i++) {
                    new Sender(barrier, client).start();
                }
            }
        });
        client.connect(connOpts);
        countDownLatch.await();

        System.out.println("发布完毕!");

    }

    private static class Sender extends Thread {
        private CyclicBarrier barrier;
        private MqttAsyncClient asyncClient;

        public Sender(CyclicBarrier barrier, MqttAsyncClient asyncClient) {
            this.barrier = barrier;
            this.asyncClient = asyncClient;
        }

        @Override
        public void run() {
            try {
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            int index = count.incrementAndGet();
            String content = index + "";
            MqttMessage message = new MqttMessage();
            message.setQos(qos);
            message.setPayload(content.getBytes());
            message.setId(index);
            try {
                asyncClient.publish(topic, message, null, new IMqttActionListener() {

                    @Override
                    public void onSuccess(IMqttToken asyncActionToken) {
                        System.out.println("消息:[{" + message + "}]已经发出去!");
                    }

                    @Override
                    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {

                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

订阅端:

public class MqttClientSubscribe {
    private static AtomicInteger total = new AtomicInteger();
    private static int qos = 1;
    private static String clientId = "JavaClientSubscribe";
    private static String broker = "tcp://172.16.30.40:1883";
    private static String topic = "MQTT-JAVA-CLIENT-TEST";
    private static CountDownLatch countDownLatch = new CountDownLatch(MqttClientPublish.THREAD_COUNT);

    public static void main(String[] args) throws Exception {
        MemoryPersistence persistence = new MemoryPersistence();
        MqttAsyncClient client = new MqttAsyncClient(broker, clientId, persistence, new TimerPingSender()
                , ThreadPoolUtil.buildScheduledThreadPoolExecutor(30, "subscribe-callback"));
        MqttConnectOptions connOpts = new MqttConnectOptions();
        connOpts.setCleanSession(false);
        connOpts.setAutomaticReconnect(true);
        connOpts.setKeepAliveInterval(30);
        client.setCallback(new MqttCallbackExtended() {

            @Override
            public void connectionLost(Throwable cause) {

            }

            @Override
            public void messageArrived(String topic, MqttMessage message) throws Exception {
                try {
                    String content = new String(message.getPayload());
                    System.out.println("content : " + content);
                    Integer index = Integer.valueOf(content);
                    total.addAndGet(index);
                    countDownLatch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {

            }

            @Override
            public void connectComplete(boolean reconnect, String serverURI) {

            }
        });
        IMqttToken token = client.connect(connOpts);
        token.waitForCompletion();
        client.subscribe(topic, qos);
        countDownLatch.await();
        System.out.println("total : " + total.get());
    }
}

先运行订阅端,然后运行发布端,结果如下图:
Java MQTT V3客户端使用实践_第3张图片
Java MQTT V3客户端使用实践_第4张图片

二、对于以上两种回调到底有什么区别呢?

注册IMqttActionListener监听器是具体某一个消息的回调,这个监听器是设置在MqttToken里面的,实现了MqttCallback的监听器是客户端级别的回调,是设置到mqttClient对象里面的,每个符合条件的消息动作都会回调。
我们以连接动作为例:
Java MQTT V3客户端使用实践_第5张图片

我们再来看看设置的客户端回调:

public void setCallback(MqttCallback callback) {
		this.mqttCallback = callback;
		comms.setCallback(callback);
	}

Java MQTT V3客户端使用实践_第6张图片

客户端程序会在发送消息出去的时候把消息放在一个hashtable里面,等到服务器回复的ack消息回来的时候,和这个消息匹配,这个时候会回调commonsCallback类里面的handleActionComplete方法,如下图:
Java MQTT V3客户端使用实践_第7张图片
那么它们会阻塞mqtt客户端线程吗?这个问题很重要,关系到我们使用mqtt客户端的效率。
经过以上的分析,其实整个调用栈就很清晰了,客户端回调用户的方法,肯定是在commsCallback这个类开启的,它本身是一个线程类,我们看看它的代码:

public void start(String threadName, ExecutorService executorService) {
		this.threadName = threadName;

		synchronized (lifecycle) {
			if (current_state == State.STOPPED) {
				// Preparatory work before starting the background thread.
				// For safety ensure any old events are cleared.
				messageQueue.clear();
				completeQueue.clear();
				
				target_state = State.RUNNING;
				if (executorService == null) {
					new Thread(this).start();
				} else {
					callbackFuture = executorService.submit(this);
				}
			}
		}
		while (!isRunning()) {
			try { Thread.sleep(100); } catch (Exception e) { }
		}			
	}
	if (isRunning()) {
					// Check for deliveryComplete callbacks...
					MqttToken token = null;
					synchronized (completeQueue) {
					    if (!completeQueue.isEmpty()) {
						    // First call the delivery arrived callback if needed
						    token = (MqttToken) completeQueue.elementAt(0);
						    completeQueue.removeElementAt(0);
					    }
					}
					if (null != token) {
						handleActionComplete(token);
					}

这里终于看到调用handleActionComplete方法的地方了,是哪个线程在这里调用的handleActionComplete方法,那么我们的回调方法的逻辑就都是在这个线程里面执行。我们现在就只要找到调用这段代码的线程。
从上面的代码可以看到这个executorService很重要,如果它为空,那么客户端会直接new一个线程来执行回调,如果我们的消息非常多那么这个线程的数量可能就多的惊人了,这种情况应该是所有系统设计者都应该避免的。所以,我们一定要手动设置这个executorService,那么它是从哪里来的呢?我们往上一层一层的查找,终于在MqttAsyncClient的构造方法参数里面找到了它:
Java MQTT V3客户端使用实践_第8张图片

三、常用配置的注意事项

1、maxInflight 参数是发出去消息等待回复的数量,如果你发送消息并发量很高,并且网络延迟大,这个参数要设置大一些,否则会出现以下异常:
Java MQTT V3客户端使用实践_第9张图片

2、keepAliveInterval心跳包间隔,默认是60s,最好根据自身网络情况设置,一般情况30s合适。

3、cleanSession 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,不会清空持久化的消息,并且会重传这些消息;如果设置为true表示每次连接到服务器都以新的身份连接,清空之前持久化的消息,不会再重传。

4、setWill方法,如果项目中需要知道客户端是否掉线可以调用该方法。设置最终端口的通知消息

5、每创建一个客户端会创建一个消息持久化容器,所以一个应用一般只是用一个单例的mqtt客户端。

6、clientid 作为连接标示的,自定义,不能2个连接用同一个clientid,会挤掉线,

你可能感兴趣的:(Java MQTT V3客户端使用实践)