目录
1、MQTT客户端功能
以"记者-电视台-观众"的模式来理解,客户端具体的流程是这样的:
- 客户端1:观众打电话到电视台:connect
- 客户端1:观众向电视台订阅"财经新闻": Subscribe 某个 Topic
- 客户端2:记者打电话到电视台:connect
- 客户端2:记者向电视台发布"财经新闻":Public某个Topic的某个Playload
- 服务器:电视台向"订阅了财经新闻的观众"发布"某条消息":Public某个Playload给Subscriber
整个过程中,电视台和记者、电视台和观众直接的电话要保存连接状态,还要时不时确认一下:
- 记者要时不时给电视台喊一声"喂":确保电视台还正常
- 观众要时不时给电视台喊一声"喂":确保电视台还正常
2、客户端软件实现过程
- 连接服务器
- 订阅:
- 发布订阅请求,等待回应
- 循环:读取Publish信息(得到订阅的信息),处理
- 发布:
- 发送数据包即可
- PING
- 循环:确保自己、对方还活着
- mqtt_packet_handle > mqtt_keep_alive
需要一个循环!
3、程序分层
至少可以分为3层:
- 最上层:APP
- 中间层:MQTT
- 平台层:实现多线程、定时器、网卡收发数据
4、情景分析
4.1 连接服务器
函数调用过程:
main
client = mqtt_lease();
mqtt_set_port(client, "1883");
mqtt_set_host(client, "www.jiejie01.top");
mqtt_connect(client);
mqtt_connect_with_results(c);
rc = network_init(c->mqtt_network, c->mqtt_host, c->mqtt_port, NULL);
rc = network_connect(c->mqtt_network);
nettype_tcp_connect(n);
platform_net_socket_connect//比如用WiFi模块 就需要提供此函数 用此函数通过串口发送AT指令给WiFi模块 才能成功将数据发送出去
4.2 创建线程
函数调用过程:
main
mqtt_connect(client);
mqtt_connect_with_results(c);
rc = network_init(c->mqtt_network, c->mqtt_host, c->mqtt_port, NULL);
rc = network_connect(c->mqtt_network);
/* send connect packet */
if ((rc = mqtt_send_packet(c, len, &connect_timer)) != MQTT_SUCCESS_ERROR)
goto exit;
if (mqtt_wait_packet(c, CONNACK, &connect_timer) == CONNACK) {
}
/* connect success, and need init mqtt thread */
c->mqtt_thread= platform_thread_init("mqtt_yield_thread", mqtt_yield_thread,c, ...);
图示解析如下:
4.3 发布消息
函数调用过程:
main
res = pthread_create(&thread1, NULL, mqtt_publish_thread, client);
mqtt_publish_thread
mqtt_publish(client, "topic1", &msg);
// 1. 构造消息
mqtt_message_t msg;
memset(&msg, 0, sizeof(msg));
msg.payload = (void *) buf;
msg.payloadlen = xxx;
mqtt_publish(client, "topic1", &msg);
// 1.1 根据MQTT协议构造数据包
// 1.2 根据平台相关的函数发送数据包(从最外层函数依次进到最底层)
mqtt_send_packet
network_write
nettype_tcp_write
platform_net_socket_write_timeout
4.4 订阅消息
消息何时到来?不知道!
所以,必定是某个内核线程不断查询网卡:
-
读网卡数据
- 得到数据的话就判断、处理
1、调用订阅消息函数
mqtt_subscribe(client, "topic1", QOS0, topic1_handler);
订阅消息:调用此函数 就可以去订阅某个消息 当接收到该主题的消息时 第四个参数的函数就会被调用
源代码解析如下:
int mqtt_subscribe(mqtt_client_t* c, const char* topic_filter, mqtt_qos_t qos, message_handler_t handler)
{
int rc = MQTT_SUBSCRIBE_ERROR;
int len = 0;
uint16_t packet_id;
platform_timer_t timer;
MQTTString topic = MQTTString_initializer;
topic.cstring = (char *)topic_filter;
message_handlers_t *msg_handler = NULL;//定义一个消息处理的结构体
if (CLIENT_STATE_CONNECTED != mqtt_get_client_state(c))
RETURN_ERROR(MQTT_NOT_CONNECT_ERROR);
packet_id = mqtt_get_next_packet_id(c);
platform_mutex_lock(&c->mqtt_write_lock);
/* serialize subscribe packet and send it */
len = MQTTSerialize_subscribe(c->mqtt_write_buf, c->mqtt_write_buf_size, 0, packet_id, 1, &topic, (int*)&qos);
if (len <= 0)
goto exit;
if ((rc = mqtt_send_packet(c, len, &timer)) != MQTT_SUCCESS_ERROR)
goto exit;
if (NULL == handler)
handler = default_msg_handler; /* if handler is not specified, the default handler is used */
/* create a message and record it */
msg_handler = mqtt_msg_handler_create(topic_filter, qos, handler);
if (NULL == msg_handler) {
rc = MQTT_MEM_NOT_ENOUGH_ERROR;
goto exit;
}
rc = mqtt_ack_list_record(c, SUBACK, packet_id, len, msg_handler);//创建完毕的handler记录下来并放入一个链表中
exit:
platform_mutex_unlock(&c->mqtt_write_lock);
RETURN_ERROR(rc);
}
消息处理结构体定义如下:
typedef struct mgtt message {
mqtt_qos_t qos;
uint8_t retained;
uint8 t dup;
uint16_t id;
size_t payloadlen;
void *payload;
}mgtt message t;
handler创建函数如下:
//handler创建函数
static message_handlers_t *mqtt_msg_handler_create(const char* topic_filter, mqtt_qos_t qos, message_handler_t handler)
{
message_handlers_t *msg_handler = NULL;
msg_handler = (message_handlers_t *) platform_memory_alloc(sizeof(message_handlers_t));//分配内存
if (NULL == msg_handler)
return NULL;
mqtt_list_init(&msg_handler->list);
msg_handler->qos = qos;//记录服务质量
msg_handler->handler = handler;//记录处理函数 /* register callback handler */
msg_handler->topic_filter = topic_filter;//记录收到的主题
return msg_handler;
}
第一步:创建一个消息处理的结构体 第二步:将收到的数据放入链表中 将链表中的数据与消息处理结构体进行逐一对比 如果主题匹配 则调用其中的处理函数
2、创建核心线程
//核心线程代码如下(主要看while循环那部分):
static void mqtt_yield_thread(void *arg)
{
int rc;
client_state_t state;
mqtt_client_t *c = (mqtt_client_t *)arg;
platform_thread_t *thread_to_be_destoried = NULL;
state = mqtt_get_client_state(c);
if (CLIENT_STATE_CONNECTED != state) {
MQTT_LOG_W("%s:%d %s()..., mqtt is not connected to the server...", __FILE__, __LINE__, __FUNCTION__);
platform_thread_stop(c->mqtt_thread); /* mqtt is not connected to the server, stop thread */
}
while (1) {
rc = mqtt_yield(c, c->mqtt_cmd_timeout);
if (MQTT_CLEAN_SESSION_ERROR == rc) {
MQTT_LOG_W("%s:%d %s()..., mqtt clean session....", __FILE__, __LINE__, __FUNCTION__);
network_disconnect(c->mqtt_network);
mqtt_clean_session(c);
goto exit;
} else if (MQTT_RECONNECT_TIMEOUT_ERROR == rc) {
MQTT_LOG_W("%s:%d %s()..., mqtt reconnect timeout....", __FILE__, __LINE__, __FUNCTION__);
}
}
exit:
thread_to_be_destoried = c->mqtt_thread;
c->mqtt_thread = (platform_thread_t *)0;
platform_thread_destroy(thread_to_be_destoried);
}
那么该函数的执行过程如下:有一个线程 该线程主要执行一个死循环 死循环中调用读消息相关函数去读网络数据 如果读到的网络数据是一个主题的消息(Publish发过来的消息) 那么就会去分辨主题Topic 再调用对应函数。
该函数会做如下事情:1、读数据(Read Packet)并处理数据 2、若一直没读取到数据,则会时不时发送Ping给服务器,保持心跳 3、处理各种错误
3、处理数据
//得到数据后的处理
static int mqtt_packet_handle(mqtt_client_t* c, platform_timer_t* timer)
{
int rc = MQTT_SUCCESS_ERROR;
int packet_type = 0;
rc = mqtt_read_packet(c, &packet_type, timer);//读取数据
switch (packet_type) {
case 0: /* timed out reading packet or an error occurred while reading data*/
if (MQTT_BUFFER_TOO_SHORT_ERROR == rc) {
MQTT_LOG_E("the client read buffer is too short, please call mqtt_set_read_buf_size() to reset the buffer size");
/* don't return directly, you need to stay active, because there is data readable now, but the buffer is too small */
}
break;
case CONNACK: /* has been processed */
goto exit;
case PUBACK:
case PUBCOMP:
rc = mqtt_puback_and_pubcomp_packet_handle(c, timer);
break;
case SUBACK:
rc = mqtt_suback_packet_handle(c, timer);
break;
case UNSUBACK:
rc = mqtt_unsuback_packet_handle(c, timer);
break;
case PUBLISH:
rc = mqtt_publish_packet_handle(c, timer);
break;
case PUBREC:
case PUBREL:
rc = mqtt_pubrec_and_pubrel_packet_handle(c, timer);
break;
case PINGRESP:
c->mqtt_ping_outstanding = 0; /* keep alive ping success */
break;
default:
break;
}
rc = mqtt_keep_alive(c);//发送心跳包
exit:
if (rc == MQTT_SUCCESS_ERROR)//处理各种错误
rc = packet_type;
RETURN_ERROR(rc);
}
4、流程梳理
1、订阅消息:对什么主题感兴趣,收到该消息之后要调用处理函数进行相应操作
mqtt_subscribe(client, "topic1", QOS0, topic1_handler);
2、将收到的消息放入链表中记录并对比
rc = mqtt_ack_list_record(c, SUBACK, packet_id, len, msg_handler);//创建完毕的handler记录下来并放入一个链表中
3、创建一个线程
static void mqtt_yield_thread(void *arg);
4、在线程中创建循环并处理数据包
rc = mqtt_yield(c, c->mqtt_cmd_timeout);//存在于while循环中
rc = mqtt_packet_handle(c, &timer);//存在于mqtt_yield函数内部
5、读取数据包
rc = mqtt_read_packet(c, &packet_type, timer);//存在于mqtt_packet_handle函数内部
6、根据数据包类型处理数据
这里用Publsh数据类型处理数据做演示
case PUBLISH:
rc = mqtt_publish_packet_handle(c, timer);//存在于mqtt_read_packet函数内部
break;
7、将数据传递出去
mqtt_deliver_message(c, &topic_name, &msg);//存在于mqtt_publish_packet_handle函数内部
8、根据主题找到之前的handler处理函数
msg_handler = mqtt_get_msg_handler(c, topic_name);//存在于mqtt_deliver_message函数内部
msg_handler->handler(c, &md);
附录
分析源码:mqttclient\test\emqx\test.c
参考资料:
-
kawaii-mqtt源码:
- 作者发布源码:https://github.com/jiejieTop/mqttclient
- 大牛维护的:https://github.com/longtengmcu/kawaii-mqtt
-
博客
-
APP