C版本MQTT移植Android

2,559 阅读7分钟

一、库下载

在github搜索paho.mqtt.c,或者直接在浏览器输入该项目地址https://github.com/eclipse/paho.mqtt.c,你可以选择在浏览器打包下载,也可以利用git版本工具clone该项目到本地。

二、预处理

对于C,C++的工程来说,移植的方案无非两种,一种是编译成静态库或者动态库,一种是直接复制源文件到你的工程,但是考虑交叉编译环境的搭建比较麻烦,所以暂时采用的是复制源文件的方式来进行移植。

1.创建AndroidC++工程

确保你的Android Studio已经配置NDK环境,然后新建一个C++工程.

2.复制源文件

创建完成之后,在cpp目录下新建mqtt目录,将paho.mqtt.c目录下的src里面的内容全部复制到刚才新建的mqtt文件夹下面,其中不包含samples目录以及VersionInfo.h.in文件.

3.编写CmakeList.txt

当前我的Android Studio版本为3.2以上,采用的是cmake来编译C++,根据CmakeList.txt可以嵌套,改造下mqtt目录下的CmakeList.txt文件,以此让Android Studio来编译mqtt,改造的代码如下:

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")

add_library(
        mqtt
        STATIC
        Base64.c
        Clients.c
        Heap.c
        LinkedList.c
        Log.c
        Messages.c
        MQTTAsync.c
        MQTTClient.c
        MQTTPacket.c
        MQTTPacketOut.c
        MQTTPersistence.c
        MQTTPersistenceDefault.c
        MQTTProperties.c
        MQTTProtocolClient.c
        MQTTProtocolOut.c
        MQTTReasonCodes.c
        MQTTVersion.c
        OsWrapper.c
        SHA1.c
        Socket.c
        SocketBuffer.c
        SSLSocket.c
        StackTrace.c
        Thread.c
        Tree.c
        utf-8.c
        WebSocket.c
)

在主CmakeList.txt文件中只需要引入子CmakeList.txt就行了

add_subdirectory(src/main/cpp/mqtt)

到这里mqtt的引入基本就完成了,rebuild工程一下就可以了

三、选择适合你的开发方式

C版本的mqtt库有两套API实现,一套是同步,一套是异步,如果你采用同步的模式来进行开发,那么你必须自己考虑线程的消息传递问题,在Android中你可以认为是在C++层实现自己的Handler机制。所以我们采用异步的API来实现客户端。

四、主要的API介绍

int MQTTAsync_create(MQTTAsync* handle, const char* serverURI, const char* clientId,
		int persistence_type, void* persistence_context);

该函数创建了一个用于连接到特定服务器,使用特定持久存储的MQTT客户端。 handle->指向MQTT客户端句柄的指针。句柄被成功从函数中返回的客户端引用所填充 serverURI->以空结尾的字符串,其指定客户端将连接到的服务器。其格式为protocol://host:port。现在的(protocol)协议必须是tcp或ssl,而host可以指定为IP地址或域名。例如, 要使用默认 MQTT 端口连接到本地计算机上运行的服务器, 请指定为 tcp://localhost:1883。 client Id->客户端标识符(clientId)是一个以空结尾的 UTF-8 编码字符串,客户端连接到服务器时将它传递过去。 persistence_type->客户端所使用的持久类型。MQTTCLIENT_PERSISTENCE_NONE-使用内存持久化。如果客户端运行的设备或系统出故障或关闭, 则任何正在运行的消息的当前状态都将丢失, 甚至在 QoS1 和 QoS2 中也可能无法传递某些消息; MQTTCLIENT_PERSISTENCE_DEFAULT-使用默认的持久化机制(文件系统)。正在运行消息的状态被保存在持久存储中,以便在意外出现时对消息的丢失提供一些保护; MQTTCLIENT_PERSISTENCE_USER-使用程序指定的持久化实现。使用这种类型,应用程序可对持久化机制进行控制,应用程序必须实现MQTTClient_persistence 接口。 persistence_context->如果应用程序使用的是MQTTCLIENT_PERSISTENCE_NONE持久化,该参数不使用,而且值应该设置为NULL。对于MQTTCLIENT_PERSISTENCE_DEFAULT持久化,应该设置持久化目录的位置(如果设置为NULL,则使用工作目录作为持久化目录)。使用MQTTCLIENT_PERSISTENCE_USER持久化,则将此参数指向有效的MQTTClient_persistence结构。

int MQTTAsync_setCallbacks(MQTTAsync handle, void* context,
									MQTTAsync_connectionLost* cl,
									MQTTAsync_messageArrived* ma,
									MQTTAsync_deliveryComplete* dc);

该函数为特定的客户端创建回调函数。如果您的客户端应用程序不使用特定的回调函数,请将相关参数设置为NULL。 调用MQTTClient_setCallbacks()使客户端进入多线程模式。 任何必要的消息确认和状态通信都在后台处理,而不需要客户端应用程序的任何干预。 handle->指向MQTT客户端句柄的指针。句柄被成功从函数中返回的客户端引用所填充 context->指向任何应用程序特定上下文的指针。 上下文指针被传递给每个回调函数,以提供对回调中的上下文信息的访问。 cl->指向MQTTClient_connectionLost()回调函数的指针。 如果您的应用程序不处理断开连接,您可以将其设置为NULL。 ma->指向MQTTClient_messageArrived()回调函数的指针。 当您调用MQTTClient_setCallbacks()时,必须指定此回调函数。 dc->指向MQTTClient_deliveryComplete()回调函数的指针。 如果您的应用程序同步发布,或者您不想检查是否成功发送,则可以将其设置为NULL。

int MQTTAsync_connect(MQTTAsync handle, const MQTTAsync_connectOptions* options);

此函数尝试使用指定的选项将先前创建的客户端连接到MQTT服务器。 handle->指向MQTT客户端句柄的指针。句柄被成功从函数中返回的客户端引用所填充 options->指向有效的MQTTClient_connectOptions结构的指针。

int MQTTAsync_subscribe(MQTTAsync handle, const char* topic, int qos, MQTTAsync_responseOptions* response)

此功能尝试将客户订阅到单个主题,该主题可能包含通配符。 此函数还指定服务质量。 handle->指向MQTT客户端句柄的指针。句柄被成功从函数中返回的客户端引用所填充 topic->订阅的主题,可使用通配符。 qos->订阅的请求服务质量

int MQTTAsync_sendMessage(MQTTAsync handle, const char* destinationName, const MQTTAsync_message* message,
													 MQTTAsync_responseOptions* response)

此功能尝试将客户订阅到单个主题,该主题可能包含通配符。 此函数还指定服务质量。 handle->指向MQTT客户端句柄的指针。句柄被成功从函数中返回的客户端引用所填充 topicName->与信息相关的主题。 msg->指向有效的 MQTTClient_message 结构的指针, 其中包含要发布消息的有效负载和属性 dt->指向MQTTClient_deliveryToken的指针。当函数成功返回时,dt会被赋值为代表消息的token。如果程序中没有使用传递token,将其设置为NULL。

五、实现

1.结构介绍

C++层需要一个类来管理我们的MQTT的创建,连接,消息发送与主题订阅,JAVA层需要一个类用来与C++层交互并且提供必要的方法在JAVA层可以调用到C++层的方法,并且在C++层消息到来的时候,能通过JNI的方式反向调用JAVA的方法,把消息抛到JAVA层以及一些必要的回调也抛到JAVA层,比如连接成功失败的回调,消息发送成功失败的回调,订阅成功失败的回调。

2.异步模式中对于回调的设置

通过MQTTAsync_setCallbacks方法来设置连接断开,消息到达,消息传递的回调,这些可以简单称为被动回调,主动方式下,例如在connect的时候,可以通过MQTTAsync_connectOptions的onSuccess和onFailure字段来设置连接成功和失败的回调,在publishmessage的时候,可以通过MQTTAsync_responseOptions的onSuccess和onFailure字段来设置发送成功和失败的回调,在subscribe的时候,可以通过MQTTAsync_responseOptions的onSuccess和onFailure字段来设置发送成功和失败的回调。

3.对于C++引入C工程问题

extern "C" {
#include "mqtt/MQTTAsync.h"
};

4.C++层子线程调用JAVA类方法

JNIEnv *jniEnv;
    if (javaVM->AttachCurrentThread(&jniEnv, 0) != JNI_OK) {
        return;
    }
    jniEnv->CallVoidMethod();
    javaVM->DetachCurrentThread();

一般我们都会封装类来进行调用的封装,例如可以创建JavaCall的类,主要字段保存Java虚拟机实例以及各方法的id,如下:

class WlJavaCall {

public:
    _JavaVM *javaVM = NULL;
    JNIEnv *jniEnv = NULL;
    jmethodID jmid_on_msg_arrived;
    jmethodID jmid_on_connection_success;
    jmethodID jmid_on_connection_failed;
    jmethodID jmid_on_connection_lost;
    jmethodID jmid_on_send_success;
    jmethodID jmid_on_send_failed;
    jmethodID jmid_on_subscribe_success;
    jmethodID jmid_on_subscribe_failed;
    jobject jobj;

public:
    WlJavaCall(_JavaVM *javaVM, JNIEnv *env, jobject *jobj);

    ~WlJavaCall();

    void onMsgArrived(const char *topic, int size, jbyte *msg);

    void onConnectionSuccess();

    void onConnectionFailed(int code, const char*msg);

    void onConnectionLost(const char *cause);

    void onSendSuccess();

    void onSendFailed(const char *cause);

    void onSubscribeSuccess();

    void onSubscribeFailed();

    void release();
};

在构造函数中我们获取对应的方法的id

WlJavaCall::WlJavaCall(_JavaVM *vm, JNIEnv *env, jobject *obj) {
    javaVM = vm;
    jniEnv = env;
    jobj = *obj;
    jobj = env->NewGlobalRef(jobj);
    jclass jlz = jniEnv->GetObjectClass(jobj);
    if (!jlz) {
        return;
    }
    jmid_on_msg_arrived = jniEnv->GetMethodID(jlz, "onMsgArrived", "(Ljava/lang/String;[B)V");
    jmid_on_connection_success = jniEnv->GetMethodID(jlz, "onConnectionSuccess", "()V");
    jmid_on_connection_failed = jniEnv->GetMethodID(jlz, "onConnectionFailed", "(ILjava/lang/String;)V");
    jmid_on_connection_lost = jniEnv->GetMethodID(jlz, "onConnectionLost", "(Ljava/lang/String;)V");
    jmid_on_send_success = jniEnv->GetMethodID(jlz, "onSendSuccess", "()V");
    jmid_on_send_failed = jniEnv->GetMethodID(jlz, "onSendFailed", "(Ljava/lang/String;)V");
    jmid_on_subscribe_success = jniEnv->GetMethodID(jlz, "onSubscribeSuccess", "()V");
    jmid_on_subscribe_failed = jniEnv->GetMethodID(jlz, "onSubscribeFailde", "()V");
}

这样我们在其他C++类中就能很方便的调用到JAVA类中的方法了

5.JAVA类中方法多线程问题

因为底层我们采用了MQTT的异步模式,所以在进行调用JAVA层方法的时候,难免会遇到多线程问题,所以方法必要时加锁。

6.消息传递给UI线程

在消息到来的时候,我们先是在C++层收到消息,然后调用JAVA类的方法抛到JAVA层,那么当前还是在子线程中,所以我们可以通过Handler来将收到的消息传递到UI线程中。

部分参考: www.cnblogs.com/edan/p/1040…