文章目錄
文章中的API使用流程都是以samples中的實例程序作爲參考
本文基於開源的mqtt c實現:
- Github鏈接: eclipse/paho.mqtt.c
1. 訪問模式
Mqtt C中分爲同步訪問
和異步訪問
模式
- 同步訪問的時候,請求會阻塞到訪問處,知道有結果返回;
- 異步訪問的時候,會將請求委託給Mqtt c client然後直接返回(零等待),最後結果返回之後,會回調對應的回調函數。
- 文章主要講異步模式
2. 主要數據結構
1. MQTTAsync
- MQTTAsync 是一個void*類型的指針,表示一個 Mqtt client(context) 句柄, 即
MQTTAsyncs
/**
* A handle representing an MQTT client. A valid client handle is available
* following a successful call to MQTTAsync_create().
*/
typedef void* MQTTAsync;
2. MQTTAsyncs
用於封裝請求的信息,包括URL,端口,回調函數等訪問參數
typedef struct MQTTAsync_struct
{
char* serverURI; //服務器url
int ssl;
int websocket;
Clients* c;
/* "Global", to the client, callback definitions */
//連接失效的回調函數
//函數原型: typedef void MQTTAsync_connectionLost(void* context, char* cause);
MQTTAsync_connectionLost* cl;
//消息到來的回調函數
//原型:typedef int MQTTAsync_messageArrived(void* context, char* topicName, int topicLen,
// MQTTAsync_message* message);
MQTTAsync_messageArrived* ma;
//消息發送成功的回調通知函數
//原型:typedef void MQTTAsync_deliveryComplete(void* context, MQTTAsync_token token);
MQTTAsync_deliveryComplete* dc;
void* clContext; /* the context to be associated with the conn lost callback*/
void* maContext; /* the context to be associated with the msg arrived callback*/
void* dcContext; /* the context to be associated with the deliv complete callback*/
//連接成功的回調函數
//原型:typedef void MQTTAsync_connected(void* context, char* cause);
MQTTAsync_connected* connected;
void* connected_context; /* the context to be associated with the connected callback*/
//連接斷開的回調函數
//原型:typedef void MQTTAsync_disconnected(void* context, MQTTProperties* properties,
//enum MQTTReasonCodes reasonCode);
MQTTAsync_disconnected* disconnected;
void* disconnected_context; /* the context to be associated with the disconnected callback*/
/* Each time connect is called, we store the options that were used. These are reused in
any call to reconnect, or an automatic reconnect attempt */
MQTTAsync_command connect; /* Connect operation properties */
MQTTAsync_command disconnect; /* Disconnect operation properties */
MQTTAsync_command* pending_write; /* Is there a socket write pending? */
List* responses;
unsigned int command_seqno;
MQTTPacket* pack;
/* added for offline buffering */
MQTTAsync_createOptions* createOptions;
int shouldBeConnected;
/* added for automatic reconnect */
int automaticReconnect;
int minRetryInterval;
int maxRetryInterval;
int serverURIcount;
char** serverURIs;
int connectTimeout;
int currentInterval;
START_TIME_TYPE lastConnectionFailedTime;
int retrying;
int reconnectNow;
/* MQTT V5 properties */
MQTTProperties* connectProps;
MQTTProperties* willProps;
} MQTTAsyncs;
3. MQTTAsync_message
Mqtt的Message結構體
typedef struct
{
/** The eyecatcher for this structure. must be MQTM. */
char struct_id[4];
/** The version number of this structure. Must be 0 or 1.
* 0 indicates no message properties */
int struct_version;
/** The length of the MQTT message payload in bytes. */
int payloadlen;
/** A pointer to the payload of the MQTT message. */
void* payload;
/**
* The quality of service (QoS) assigned to the message.
* There are three levels of QoS:
* <DL>
* <DT><B>QoS0</B></DT>
* <DD>Fire and forget - the message may not be delivered</DD>
* <DT><B>QoS1</B></DT>
* <DD>At least once - the message will be delivered, but may be
* delivered more than once in some circumstances.</DD>
* <DT><B>QoS2</B></DT>
* <DD>Once and one only - the message will be delivered exactly once.</DD>
* </DL>
*/
int qos;
/**
* The retained flag serves two purposes depending on whether the message
* it is associated with is being published or received.
*
* <b>retained = true</b><br>
* For messages being published, a true setting indicates that the MQTT
* server should retain a copy of the message. The message will then be
* transmitted to new subscribers to a topic that matches the message topic.
* For subscribers registering a new subscription, the flag being true
* indicates that the received message is not a new one, but one that has
* been retained by the MQTT server.
*
* <b>retained = false</b> <br>
* For publishers, this indicates that this message should not be retained
* by the MQTT server. For subscribers, a false setting indicates this is
* a normal message, received as a result of it being published to the
* server.
*/
int retained;
/**
* The dup flag indicates whether or not this message is a duplicate.
* It is only meaningful when receiving QoS1 messages. When true, the
* client application should take appropriate action to deal with the
* duplicate message.
*/
int dup;
/** The message identifier is normally reserved for internal use by the
* MQTT client and server.
*/
int msgid;
/**
* The MQTT V5 properties associated with the message.
*/
MQTTProperties properties;
} MQTTAsync_message;
4. MQTTAsync_responseOptions
- MQTTAsync_responseOptions表示每次操作的響應配置,包括
操作成功
,操作失敗
等操作;- 同類的還有:
- MQTTAsync_disconnectOptions : 斷開連接選項
- MQTTAsync_connectOptions: 連接選項
- MQTTAsync_init_options:初始化選項 等等
typedef struct MQTTAsync_responseOptions
{
/** The eyecatcher for this structure. Must be MQTR */
char struct_id[4];
/** The version number of this structure. Must be 0 or 1
* if 0, no MQTTV5 options */
int struct_version;
/**
* A pointer to a callback function to be called if the API call successfully
* completes. Can be set to NULL, in which case no indication of successful
* completion will be received.
*/
MQTTAsync_onSuccess* onSuccess;
/**
* A pointer to a callback function to be called if the API call fails.
* Can be set to NULL, in which case no indication of unsuccessful
* completion will be received.
*/
MQTTAsync_onFailure* onFailure;
/**
* A pointer to any application-specific context. The
* the <i>context</i> pointer is passed to success or failure callback functions to
* provide access to the context information in the callback.
*/
void* context;
/**
* A token is returned from the call. It can be used to track
* the state of this request, both in the callbacks and in future calls
* such as ::MQTTAsync_waitForCompletion.
*/
MQTTAsync_token token;
/**
* A pointer to a callback function to be called if the API call successfully
* completes. Can be set to NULL, in which case no indication of successful
* completion will be received.
*/
MQTTAsync_onSuccess5* onSuccess5;
/**
* A pointer to a callback function to be called if the API call successfully
* completes. Can be set to NULL, in which case no indication of successful
* completion will be received.
*/
MQTTAsync_onFailure5* onFailure5;
/**
* MQTT V5 input properties
*/
MQTTProperties properties;
/*
* MQTT V5 subscribe options, when used with subscribe only.
*/
MQTTSubscribe_options subscribeOptions;
/*
* MQTT V5 subscribe option count, when used with subscribeMany only.
* The number of entries in the subscribe_options_list array.
*/
int subscribeOptionsCount;
/*
* MQTT V5 subscribe option array, when used with subscribeMany only.
*/
MQTTSubscribe_options* subscribeOptionsList;
} MQTTAsync_responseOptions;
5. List
List是一個鏈表, Mqttc中的commands信令隊列就是用該鏈表存儲/獲取的
- static List* commands = NULL; //Commands信令隊列;
- static List* handles = NULL; //clients隊列,記錄所有的client(MQTTAsync)對象;
/**
* Structure to hold all data for one list
*/
typedef struct
{
ListElement *first, /**< first element in the list */
*last, /**< last element in the list */
*current; /**< current element in the list, for iteration */
int count; /**< no of items */
size_t size; /**< heap storage used */
} List;
2. 流程
- Mqtt c的同步沒什麼可說的,類似於調用一個RPC接口一樣的,主要看異步請求的流程
- Mqtt C實現的src/samples文件夾下有異步的subscribe和publish的例子,從這裏可以作爲切入點來看MQTTC
的請求流程
1. 訂閱
- MQTTAsync_subscribe.c
//MQTTAsync_subscribe.c
//main()
int main(int argc, char* argv[])
{
//[*1] 創建一個MQTTAsync的handle
MQTTAsync client;
//設置連接選項參數,這裏默認給了初始化的參數,mqtt-c自己提供的
MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer;//[*2]
//設置斷開連接選項參數,這裏默認給了初始化的參數,mqtt-c自己提供的
MQTTAsync_disconnectOptions disc_opts = MQTTAsync_disconnectOptions_initializer;//[*3s]
int rc;
int ch;
//真正創建一個MQTTAsync 的結構體(這裏其實是MQTTAsyncs結構體,下面解釋)
MQTTAsync_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL);
//設置回調函數
MQTTAsync_setCallbacks(
client/*MQTTAsync handle*/,
client/*context*/,
connlost/*MQTTAsync_connectionLost*/,
msgarrvd/*MQTTAsync_messageArrived*/,
NULL/*MQTTAsync_deliveryComplete*/);
conn_opts.keepAliveInterval = 20; //心跳間隔
conn_opts.cleansession = 1; //在connect/disconnect的時候,是否清除上一個連接的seesion狀態信息緩存
//(重啓一個新的)。這個session狀態用來確認消息質量保證(至少一次或者只有一次)
conn_opts.onSuccess = onConnect; //連接成功的回調函數
conn_opts.onFailure = onConnectFailure; //連接失敗的回調函數
conn_opts.context = client; //上下文對象,即MQTTAsync
//開始請求連接
if ((rc = MQTTAsync_connect(client, &conn_opts)) != MQTTASYNC_SUCCESS)
{
printf("Failed to start connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
while (!subscribed)
#if defined(WIN32)
Sleep(100);
#else
//等待10ms, 一定時間內確保subscribe動作完成
usleep(10000L);
#endif
if (finished)
goto exit;
do
{
ch = getchar();
} while (ch!='Q' && ch != 'q');
disc_opts.onSuccess = onDisconnect;
if ((rc = MQTTAsync_disconnect(client, &disc_opts)) != MQTTASYNC_SUCCESS)
{
printf("Failed to start disconnect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
while (!disc_finished)
#if defined(WIN32)
Sleep(100);
#else
usleep(10000L);
#endif
exit:
MQTTAsync_destroy(&client);
return rc;
}
//--------------------------------------------------------------------------------------
//連接成功回調
void onConnect(void* context, MQTTAsync_successData* response)
{
MQTTAsync client = (MQTTAsync)context;
MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer;//獲取默認的Option
int rc;
printf("Successful connection\n");
printf("Subscribing to topic %s\nfor client %s using QoS%d\n\n"
"Press Q<Enter> to quit\n\n", TOPIC, CLIENTID, QOS);
opts.onSuccess = onSubscribe; //訂閱成功時回調
//原型:typedef void MQTTAsync_onSuccess(void* context,
//MQTTAsync_successData* response);
opts.onFailure = onSubscribeFailure; //訂閱失敗時回調
//原型:typedef void MQTTAsync_onFailure(void* context,
// MQTTAsync_failureData* response);
opts.context = client;
deliveredtoken = 0;
//開始訂閱
if ((rc = MQTTAsync_subscribe(client, TOPIC, QOS, &opts)) != MQTTASYNC_SUCCESS)
{
printf("Failed to start subscribe, return code %d\n", rc);
exit(EXIT_FAILURE);
}
}
//--------------------------------------------------------------------------------------
//訂閱成功
void onSubscribe(void* context, MQTTAsync_successData* response)
{
printf("Subscribe succeeded\n");
subscribed = 1; //標記訂閱成功
}
- MQTTAsync_create
int MQTTAsync_create(MQTTAsync* handle, const char* serverURI, const char* clientId,
int persistence_type, void* persistence_context)
{
return MQTTAsync_createWithOptions(handle, serverURI, clientId, persistence_type,
persistence_context, NULL);
}
//--------------------------------------------------------------------------------------
int MQTTAsync_createWithOptions(MQTTAsync* handle, const char* serverURI, const char* clientId,
int persistence_type, void* persistence_context, MQTTAsync_createOptions* options)
{
int rc = 0;
MQTTAsyncs *m = NULL;
...
//初始化全局對象
if (!global_initialized)
{
#if defined(HEAP_H)
Heap_initialize();
#endif
Log_initialize((Log_nameValue*)MQTTAsync_getVersionInfo());
bstate->clients = ListInitialize(); // 初始化ClientStates列表
Socket_outInitialize();
Socket_setWriteCompleteCallback(MQTTAsync_writeComplete); //Socket寫入完成回調
handles = ListInitialize(); // 創建handles隊列
commands = ListInitialize(); //command列表,代表mqtt信令請求的隊列
...
m = malloc(sizeof(MQTTAsyncs));
*handle = m;//爲handle申請內存,解釋類型爲MQTTAsyncs
memset(m, '\0', sizeof(MQTTAsyncs));
//爲m設置內容
m->serverURI = MQTTStrdup(serverURI);
m->responses = ListInitialize();
//添加該handle到handles鏈表隊列中
ListAppend(handles, m, sizeof(MQTTAsyncs));
m->c = malloc(sizeof(Clients));
memset(m->c, '\0', sizeof(Clients));
m->c->context = m;
m->c->outboundMsgs = ListInitialize();
m->c->inboundMsgs = ListInitialize();
m->c->messageQueue = ListInitialize();
m->c->clientID = MQTTStrdup(clientId);
m->c->MQTTVersion = MQTTVERSION_DEFAULT;
m->shouldBeConnected = 0;
if (options)
{
m->createOptions = malloc(sizeof(MQTTAsync_createOptions));
memcpy(m->createOptions, options, sizeof(MQTTAsync_createOptions));
m->c->MQTTVersion = options->MQTTVersion;
}
...
//將新建的client,添加到ClientStates列表中
ListAppend(bstate->clients, m->c, sizeof(Clients) + 3*sizeof(List));
//m代替handle,設置完成了所有請求的參數,MqttAsync對象創建完畢(MqttAsyncs)
}
Subscribe的過程: 創建連接的參數 -> 然後連接 -> 連接成功 -> 訂閱 -> 訂閱成功 -> 標記訂閱狀態(成功);
2. 發佈
//MQTTAsync_publish.c
//main()
//流程和Subscribe一樣,在main中主要是connect操作,請求mqtt服務器連接
int main(int argc, char* argv[])
{
MQTTAsync client;
MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer;
int rc;
MQTTAsync_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL);
MQTTAsync_setCallbacks(client, NULL, connlost, NULL, NULL);
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
conn_opts.onSuccess = onConnect;
conn_opts.onFailure = onConnectFailure;
conn_opts.context = client;
if ((rc = MQTTAsync_connect(client, &conn_opts)) != MQTTASYNC_SUCCESS)
{
printf("Failed to start connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
printf("Waiting for publication of %s\n"
"on topic %s for client with ClientID: %s\n",
PAYLOAD, TOPIC, CLIENTID);
while (!finished)
#if defined(WIN32)
Sleep(100);
#else
usleep(10000L);
#endif
MQTTAsync_destroy(&client);
return rc;
}
//--------------------------------------------------------------------------------------
//連接成功
void onConnect(void* context, MQTTAsync_successData* response)
{
MQTTAsync client = (MQTTAsync)context;
MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer;
//創建要發送的Message MQTTAsync_message
MQTTAsync_message pubmsg = MQTTAsync_message_initializer;
int rc;
printf("Successful connection\n");
//發送成功的回調
opts.onSuccess = onSend;
opts.context = client;
//設置發送內容(payload)
pubmsg.payload = PAYLOAD;
pubmsg.payloadlen = (int)strlen(PAYLOAD);
pubmsg.qos = QOS;//設置質量等級保證
pubmsg.retained = 0;
deliveredtoken = 0;
//發送
if ((rc = MQTTAsync_sendMessage(client, TOPIC, &pubmsg, &opts)) != MQTTASYNC_SUCCESS)
{
printf("Failed to start sendMessage, return code %d\n", rc);
exit(EXIT_FAILURE);
}
}
//--------------------------------------------------------------------------------------
//發送成功(該用例中發送一次成功後直接斷開了連接)
void onSend(void* context, MQTTAsync_successData* response)
{
MQTTAsync client = (MQTTAsync)context;
//創建斷開連接選項
MQTTAsync_disconnectOptions opts = MQTTAsync_disconnectOptions_initializer;
int rc;
printf("Message with token value %d delivery confirmed\n", response->token);
opts.onSuccess = onDisconnect; //斷開連接成功
opts.context = client;
//斷開連接
if ((rc = MQTTAsync_disconnect(client, &opts)) != MQTTASYNC_SUCCESS)
{
printf("Failed to start sendMessage, return code %d\n", rc);
exit(EXIT_FAILURE);
}
}
3. Mqttc信令發送流程(以Connect爲例)
1. 信令入隊
//MQTTAsync.c
int MQTTAsync_connect(MQTTAsync handle, const MQTTAsync_connectOptions* options)
{
MQTTAsyncs* m = handle; //將void* 轉換爲MQTTAsyncs* 準備填充數據
int rc = MQTTASYNC_SUCCESS;
MQTTAsync_queuedCommand* conn; //執行單位, 這裏代表連接
...
//將options的內容填充給m->connect
m->connect.onSuccess = options->onSuccess;
m->connect.onFailure = options->onFailure;
...
if (sendThread_state != STARTING && sendThread_state != RUNNING)
{
MQTTAsync_lock_mutex(mqttasync_mutex);
sendThread_state = STARTING;
//啓動信令發送線程
Thread_start(MQTTAsync_sendThread, NULL);
MQTTAsync_unlock_mutex(mqttasync_mutex);
}
if (receiveThread_state != STARTING && receiveThread_state != RUNNING)
{
MQTTAsync_lock_mutex(mqttasync_mutex);
receiveThread_state = STARTING;
//啓動信令接收線程
Thread_start(MQTTAsync_receiveThread, handle);
MQTTAsync_unlock_mutex(mqttasync_mutex);
}
...
//將options的內容填充給m->c(Clients)
m->c->keepAliveInterval = options->keepAliveInterval;
...
//填充m的其他參數
...
//爲MQTTAsync_queuedCommand申請內存,初始化並將m賦值給conn->client;
conn = malloc(sizeof(MQTTAsync_queuedCommand));
memset(conn, '\0', sizeof(MQTTAsync_queuedCommand));
conn->client = m;
//將options的內容(回調等)賦值給conn->command;
if (options)
{
conn->command.onSuccess = options->onSuccess;
conn->command.onFailure = options->onFailure;
conn->command.onSuccess5 = options->onSuccess5;
conn->command.onFailure5 = options->onFailure5;
conn->command.context = options->context;
}
conn->command.type = CONNECT;
conn->command.details.conn.currentURI = 0;
//將conn入隊到commands列表(MQTTAsync_createWithOptions中初始化的全局列表)
rc = MQTTAsync_addCommand(conn, sizeof(conn));
...
}
//--------------------------------------------------------------------------------------
static int MQTTAsync_addCommand(MQTTAsync_queuedCommand* command, int command_size)
{
int rc = 0;
...
if ((command->command.type != CONNECT) || (command->client->c->connect_state == NOT_IN_PROGRESS))
command->command.start_time = MQTTAsync_start_clock();
if (command->command.type == CONNECT ||
(command->command.type == DISCONNECT && command->command.details.dis.internal))
{//CONNECT / DISCONNECT 的情況下
MQTTAsync_queuedCommand* head = NULL;
if (commands->first)
head = (MQTTAsync_queuedCommand*)(commands->first->content);
if (head != NULL && head->client == command->client && head->command.type == command->command.type)
MQTTAsync_freeCommand(command); /* ignore duplicate connect or disconnect command */
else
//對於Connect或者Disconnect優先處理, 將command插入到指定的index上,這裏是插入到隊頭;
ListInsert(commands, command, command_size, commands->first); /* add to the head of the list */
}
else
{
//非Connect的情況下,將command續接到隊尾;
ListAppend(commands, command, command_size);
...
}
...
//通知發送線程接觸等待狀態,開始處理髮送隊列中的信令給Mqtt服務器
// 這裏對應喚醒的等待線程是MQTTAsync_sendThread
rc = Thread_signal_cond(send_cond);
...
}
2. 信令出隊
在入隊完畢之後,會喚醒MQTTAsync_sendThread
並解除等待狀態
//MQTTAsync.c
static thread_return_type WINAPI MQTTAsync_sendThread(void* n)
{
...
while (!tostop)
{
int rc;
while (commands->count > 0)
{
//循環處理commands隊列中的信令,處理完成後,跳出循環等待,一直到隊列有內容,會被喚醒並繼續處理
if (MQTTAsync_processCommand() == 0)
break; /* no commands were processed, so go into a wait */
}
...
//隊列爲空之後,等待...
if ((rc = Thread_wait_cond(send_cond, 1)) != 0 && rc != ETIMEDOUT)
Log(LOG_ERROR, -1, "Error %d waiting for condition variable", rc);
...
}
...
return 0;
}
MQTTAsync_processCommand 函數中,就是從隊列獲取一個command信令發送給MQTT服務器,然後返回,並繼續循環處理下一個,直到隊列爲空,返回0;
4. Mqtt C消息接受流程
接受流程在MQTTAsync_receiveThread 線程中, 不斷的輪詢, 以linux的selector 作爲監聽, 查詢是否有已經準備好的消息的socket(有內容寫入),然後解析, 分發接收到的信息.