Mqtt C實現記錄,流程分析

文章中的API使用流程都是以samples中的實例程序作爲參考
本文基於開源的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. 訂閱

  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; //標記訂閱成功
}
  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(有內容寫入),然後解析, 分發接收到的信息.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章