MQTT
简介
MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,为此,它需要一个消息中间件 。
MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。
MQTT应用
IBM和St. Jude医疗中心通过MQTT开发了一套Merlin系统,该系统使用了用于家庭保健的传感器。St. Jude医疗中心设计了一个叫做Merlin@home的心脏装置,这种无线发射器可以用来监控那些已经植入复律-除颤器和起搏器(两者都是基本的传感器)的心脏病人。
该产品利用MQTT把病人的即时更新信息传给医生/医院,然后医院进行保存。这样的话,病人就不用亲自去医院检查心脏仪器了,医生可以随时查看病人的数据,给出建议,病人在家里就可以自行检查。
IBM称该发射器包括一个大型触摸屏,一个嵌入式键盘平台,以及一个Linux操作系统。
在未来几年,MQTT的应用会越来越广,值得关注。
通过MQTT协议,目前已经扩展出了数十个MQTT服务器端程序,可以通过PHP,JAVA,Python,C,C#等系统语言来向MQTT发送相关消息。
此外,国内很多企业都广泛使用MQTT作为Android手机客户端与服务器端推送消息的协议。其中Sohu,Cmstop手机客户端中均有使用到MQTT作为消息推送消息。据Cmstop主要负责消息推送的高级研发工程师李文凯称,随着移动互联网的发展,MQTT由于开放源代码,耗电量小等特点,将会在移动消息推送领域会有更多的贡献,在物联网领域,传感器与服务器的通信,信息的收集,MQTT都可以作为考虑的方案之一。在未来MQTT会进入到我们生活的各各方面。
如果需要下载MQTT服务器端,可以直接去MQTT官方网站点击software进行下载MQTT协议衍生出来的各个不同版本。
MQTT特点
MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议,它具有以下主要的几项特性:
1、使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合;
2、对负载内容屏蔽的消息传输;
3、使用 TCP/IP 提供网络连接;
4、有三种消息发布服务质量:
5、小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量;
6、使用 Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。
MQTT客户端系列函数
(1) 创建 MQTT 管理客户端
struct mosquitto *mosquitto_new(const char *id, bool clean_session, void *obj);
(2) 连接 MQTT 服务器
成功返回0,参数为地址和端口,例如127.0.0.1、1883。
int mosquitto_connect(struct mosquitto *mosq, const char *host, int port, int keepalive);
(3) 订阅 MQTT 消息
可以多次订阅。
int mosquitto_subscribe(struct mosquitto *mosq, int *mid, const char *sub, int qos);
(4) 接收MQTT订阅消息回调函数(包括被请求的和请求回复的)
void mosquitto_message_callback_set(struct mosquitto *mosq, void (*on_message)(struct mosquitto *, void *, const struct mosquitto_message *));
(5) 开新线程进行循环处理
int mosquitto_loop_start(struct mosquitto *mosq);
(6) 发布消息
int mosquitto_publish(struct mosquitto *mosq, int *mid, const char *topic, int payloadlen, const void *payload, int qos, bool retain);
消息队列的模板基础类CQueMsg
实例化队列数据类型、以及数据类型使用的容器类型(默认list容器),主要是list线程不安全,因此需要我们自己保证多线程的list安全。
主要包含5个函数,分别是:
-
是否为空
-
返回链表指针
-
元素压栈
-
弹出第一个元素
-
弹出指定的元素
template <typename T, typename CONT = std::list<T> >
class CQueMsg
{
private:
CONT elems;
uv_sem_t m_queSem; //队列信号量
public:
CQueMsg(); virtual ~CQueMsg();
bool empty(void) const;
CONT *getElems();//返回链表指针
void push(T const &elem);//元素压栈
bool popFront(T &elem, DWORD dwTimeOut=0);//弹出第一个元素
bool popElem(T const &elem);//弹出指定的元素
};
消息队列的实例类CMmQueMsg
这里没有继续使用模板,因为函数中我们用到了元素的内部数据结构,需要根据实际修改。
class CMmQueMsg
{
private:
CQueMsg<uc_node> m_ReqRcvQos0;//被请求队列0
CQueMsg<uc_node> m_ReqRcvQos1;//被请求队列1
CQueMsg<uc_node> m_ReqRcvQos2;//被请求队列2
CQueMsg<uc_node> m_AskSend;//请求队列
uv_sem_t m_AskQueSem; //请求队列信号量
public:
CMmQueMsg();virtual ~CMmQueMsg();
bool reqRcvEmpty() const;//被请求队列是否为空
void reqPush(uc_node const &elem);//被请求队列压栈
bool reqRcvPop(uc_node &elem);//被请求队列出栈
void askSendPush(uc_node const &elem);//请求队列压栈
bool postAskReq(uc_node const &elem);//请求队列回复元素匹配
uc_node askSendPop(uc_node const &elem);//请求队列元素出栈
void askRmOverTime(DWORD dwSavetime); //请求队列超时处理
};
元素实例uc_code
typedef struct st_uniform_comm{
int qos; //消息质量
BYTE bNeedTime = 0;//处理消息是否耗时,耗时的新建线程处理,非0-耗时
BYTE bJson = 0;//100-json格式,0-ASN.1格式
std::string sSource; /* 源名称,需要和接收方名称相同(等待类的消息固定匹配) */
std::string sDestination; /* 接收方名称,需要和源名称相同(等待类的消息不匹配) */
//匹配特征-JSON格式
DWORD dwToken = 0; //token
//std::string sOperate; /* 操作类型,等待类的消息固定匹配) */
std::string sCommand; /* 命令类型,"request", "response",等待类的消息固定匹配) */
std::string sInterface; /* 接口类型,等待类的消息固定匹配) */
//匹配特征-ASN.1格式
BYTE bPrm; /* 启动标志位,必定相反(0、1,等待类的消息固定匹配) */
WORD wIndex; /* 帧序号(从 0 递增),需要相同(等待类的消息不匹配) */
WORD wLabel; /* 消息标签,需要相同(等待类的消息不匹配) */
DWORD dwTag; /* 接口标识,需要相同或者匹配错误(等待类的消息固定匹配) */
//节点类型、结果、标志
BYTE bType; /* 0 同步 1 异步 255 不发送但等待消息 */
int ret; /* 0 正常 -1 发送错误 -2 超时 -3 其他错误 */
BOOL fOutdate = false; /* 操作过就过时 */
//计时器
DWORD dwSaveTime = 0; /* 当前节点存在的时间,单位 ms */
DWORD dwOvertime = 0; /* 设定的超时时间,单位 ms,0 为最大超时 600s */
//接收数据
BYTE pbRecBuf[MSG_NUM]; /* 帧接收缓冲指针 */
WORD wRecBufLen = 0; /* 帧接收缓冲区长度 */
//同步操作
uv_sem_t *sem = NULL; /* 无名信号量,uv_sem_init(&sem, n) 使初始值为n *///改为指针,linux调用需要指针
//异步操作
void (*async_cb)(int ret, BYTE *pbRecBuf, WORD wRecBufLen) = NULL; /* 回调函数 */
//重载操作符==,因为后面删除链表中元素需要比较元素
bool operator==(const struct st_uniform_comm &node) const
{
if (!node.bJson && !this->bJson){
if (node.wIndex == this->wIndex && node.wLabel == this->wLabel && node.dwTag == this->dwTag)
return true;
}
else if (node.bJson == 100 && this->bJson == 100){
if (node.dwToken == this->dwToken &&
((node.sSource == this->sSource && node.sDestination == this->sDestination && node.sInterface == this->sInterface) ||
(node.sSource == this->sDestination && node.sDestination == this->sSource && node.sInterface == this->sInterface)))
return true;
}
return false;
}
struct st_uniform_comm *next = NULL;/* 链表指针 */
} uc_node;
消息分类及队列
MQTT客户端的消息共2类,第一类是订阅的,第二类是发布的。
但是订阅又分为被请求和请求回复,发布分为请求和被请求回复。
因为被请求的不需要同步异步处理,按顺序处理即可,因此我们归为一类,放在一个队列中。
主动请求的消息分为同步和异步,我们归为一类,放在一个对列中。
如类 CMmQueMsg所示。
两个类的源码
QueMgr.h
#pragma once
#include <string>
#include <iostream>
#include <list>
#include <vector>
#include <uv.h>
//实例化队列数据类型、以及数据类型使用的容器类型(默认list容器),主要是list线程不安全,因此需要我们自己保证多线程的list安全
template <typename T, typename CONT = std::list<T> >
class CQueMsg
{
private:
CONT elems;
uv_sem_t m_queSem; //队列信号量
public:
CQueMsg();
virtual ~CQueMsg();
bool empty(void) const;
void push(T const &elem);//元素压栈
CONT *getElems();//返回链表指针
bool popFront(T &elem, DWORD dwTimeOut=0);//弹出第一个元素
bool popElem(T const &elem);//弹出指定的元素
};
//描述:构造
//参数:@iSemVal 信号量初始化值
//返回:无
template <typename T, typename CONT >
CQueMsg<T, CONT>::CQueMsg()
{
uv_sem_init(&m_queSem, 1);
}
//描述:析构
//参数:无
//返回:无
template <typename T, typename CONT >
CQueMsg<T, CONT>::~CQueMsg()
{
uv_sem_destroy(&m_queSem);
}
//描述:队列是否为空
//参数:无
//返回:为空返回true,反之为false
template <typename T, typename CONT >
bool CQueMsg<T, CONT>::empty(void) const
{
bool fEmpty;
//uv_sem_wait(&m_queSem);
fEmpty = elems.empty();
//uv_sem_post(&m_queSem);
return fEmpty;
}
//描述:队列压栈
//参数:@elem 队列元素
//返回:无
template <typename T, typename CONT >
void CQueMsg<T, CONT>::push(T const &elem)
{
uv_sem_wait(&m_queSem);
//延缓消息处理
WORD wSize = elems.size();
if (wSize > 100)
{
uv_sem_post(&m_queSem);
uv_sleep(1000);
uv_sem_wait(&m_queSem);
}
else if (wSize > 50)
{
uv_sem_post(&m_queSem);
uv_sleep(500);
uv_sem_wait(&m_queSem);
}
else if (wSize > 10)
{
uv_sem_post(&m_queSem);
uv_sleep(50);
uv_sem_wait(&m_queSem);
}
elems.push_back(elem);
uv_sem_post(&m_queSem);
}
//描述:返回list容器的指针
//参数:@elem 队列元素
//返回:list容器的指针
template <typename T, typename CONT >
CONT *CQueMsg<T, CONT>::getElems()
{
return &elems;
}
//描述:队列头出栈
//参数:@elem 队列元素
// @dwTimeOut 超时毫秒,未实现,调用者自己实现延时
//返回:成功为ture,反之为false
template <typename T, typename CONT >
bool CQueMsg<T, CONT>::popFront(T &elem, DWORD dwTimeOut)
{
uv_sem_wait(&m_queSem);
if (!elems.empty())
{
elem = elems.front(); //返回第一个元素
elems.pop_front(); //删除第一个元素
uv_sem_post(&m_queSem);
return true;
}
uv_sem_post(&m_queSem);
return false;
}
//描述:请求特定元素出栈
//参数:@elem 队列元素
//返回:成功为ture,反之为false
template <typename T, typename CONT >
bool CQueMsg<T, CONT>::popElem(T const &elem)
{
uv_sem_wait(&m_queSem);
if (!elems.empty())
{
elems.remove(elem); //删除指定元素
uv_sem_post(&m_queSem);
return true;
}
uv_sem_post(&m_queSem);
return false;
}
mm_QueMgr.h
#pragma once
#include <string>
#include <iostream>
#include <list>
#include <vector>
#include <uv.h>
#include "QueMsg.h"
#include "mm_uniform_api.h"
//实例化队列数据类型、以及数据类型使用的容器类型(默认list容器)
class CMmQueMsg
{
private:
CQueMsg<uc_node> m_ReqRcvQos0;//被请求队列0
CQueMsg<uc_node> m_ReqRcvQos1;//被请求队列1
CQueMsg<uc_node> m_ReqRcvQos2;//被请求队列2
CQueMsg<uc_node> m_AskSend;//请求队列
uv_sem_t m_AskQueSem; //请求队列信号量
public:
CMmQueMsg();
virtual ~CMmQueMsg();
bool reqRcvEmpty() const;
void reqPush(uc_node const &elem);
bool reqRcvPop(uc_node &elem);
void askSendPush(uc_node const &elem);
bool postAskReq(uc_node const &elem);
uc_node askSendPop(uc_node const &elem);
void askRmOverTime(DWORD dwSavetime);
};
//描述:构造
//参数:@iSemVal 信号量初始化值
//返回:无
CMmQueMsg::CMmQueMsg()
{
uv_sem_init(&m_AskQueSem, 1);
}
//描述:析构
//参数:无
//返回:无
CMmQueMsg::~CMmQueMsg()
{
uv_sem_destroy(&m_AskQueSem);
}
//描述:队列是否为空
//参数:无
//返回:为空返回true,反之为false
bool CMmQueMsg::reqRcvEmpty(void) const
{
return m_ReqRcvQos0.empty() && m_ReqRcvQos1.empty() && m_ReqRcvQos2.empty();
}
//描述:队列压栈
//参数:@elem 队列元素
//返回:无
void CMmQueMsg::reqPush(uc_node const &elem)
{
switch (elem.qos){
case 0:
m_ReqRcvQos0.push(elem); break;
case 1:
m_ReqRcvQos1.push(elem); break;
case 2:
m_ReqRcvQos2.push(elem); break;
default: break;
}
}
//描述:被请求消息队列出栈
//参数:@elem 队列元素
// @dwTimeOut 超时毫秒
//返回:成功为ture,反之为false
bool CMmQueMsg::reqRcvPop(uc_node &elem)
{
if (!m_ReqRcvQos0.empty())
return m_ReqRcvQos0.popFront(elem);
else if (!m_ReqRcvQos1.empty())
return m_ReqRcvQos1.popFront(elem);
else
return m_ReqRcvQos2.popFront(elem);
}
//描述:请求队列压栈
//参数:@elem 队列元素
//返回:无
void CMmQueMsg::askSendPush(uc_node const &elem)
{
uv_sem_wait(&m_AskQueSem);
m_AskSend.push(elem);
uv_sem_post(&m_AskQueSem);
}
//描述:释放匹配同步异步请求消息队列
//参数:@elem 队列元素
// @dwTimeOut 超时毫秒
//返回:成功为ture,反之为false
bool CMmQueMsg::postAskReq(uc_node const &elem)
{
uv_sem_wait(&m_AskQueSem);
std::list<uc_node> *elems = m_AskSend.getElems();
if (!elems->empty())
for (std::list<uc_node>::iterator node=elems->begin(); node!=elems->end(); node++){
if(*node == elem){
node->wRecBufLen = elem.wRecBufLen;
memcpy(node->pbRecBuf, elem.pbRecBuf, elem.wRecBufLen);
node->ret = 0;//正常返回
if (node->async_cb){//异步直接调用回调函数
node->async_cb(node->ret, node->pbRecBuf, node->wRecBufLen);
m_AskSend.popElem(*node);
uv_sem_post(&m_AskQueSem);
return true;
}
if (node->sem){
uv_sem_post(node->sem);//同步释放信号量
uv_sem_post(&m_AskQueSem);
return true;
}
}
}
uv_sem_post(&m_AskQueSem);
return false;
}
//描述:同步请求消息队列出栈
//参数:@elem 队列元素
//返回:成功为出栈元素,反之为空元素
uc_node CMmQueMsg::askSendPop(uc_node const &elem)
{
uc_node matchNode;
uv_sem_wait(&m_AskQueSem);
std::list<uc_node>* elems = m_AskSend.getElems();
if (!elems->empty())
for (std::list<uc_node>::iterator node=elems->begin(); node!=elems->end(); node++){
if(*node == elem){
matchNode = *node;
m_AskSend.popElem(matchNode);
uv_sem_post(&m_AskQueSem);
return matchNode;
}
}
uv_sem_post(&m_AskQueSem);
return matchNode;
}
//描述:同步异步请求消息超时处理
//参数:@每次增加的走时,毫秒
//返回:无
void CMmQueMsg::askRmOverTime(DWORD dwSavetime)
{
uv_sem_wait(&m_AskQueSem);
std::list<uc_node> *elems = m_AskSend.getElems();
if (!elems->empty()){
for (std::list<uc_node>::iterator node=elems->begin(); node!=elems->end();){
node->dwSaveTime += dwSavetime;//增加走时
/* 超时分类节点 */
if((0 == node->dwOvertime && node->dwSaveTime >= 1000*60*1) || (0 != node->dwOvertime && node->dwSaveTime >= node->dwOvertime)){
node->ret = -2;//超时
if (node->async_cb){//异步直接调用回调函数
node->async_cb(node->ret, node->pbRecBuf, node->wRecBufLen);
m_AskSend.popElem(*node++);
continue;
}
uv_sem_post(node->sem);//同步释放信号量
}
node++;
}
}
uv_sem_post(&m_AskQueSem);
}
实例
static CMmQueMsg *s_QueMsg = new CMmQueMsg();
订阅消息入栈
**注意:**收到消息之后需要转换为对应的元素节点然后入栈。
static void DoMqttMsg(const struct mosquitto_message *message)
{
/* 根据 MQTT 主题名获取一些信息 *///:app/mapManager/JSON/get/request/slotInfo
std::string sTopic = message->topic;
std::string sTopicSrcName = sTopic.substr(0, sTopic.find_first_of('/'));//app
std::string sTmp = sTopic.substr(sTopic.find_first_of('/') + 1);
std::string sTopicDestName = sTmp.substr(0, sTmp.find_first_of('/'));//mapManager
std::string sInterface = sTopic.substr(sTopic.find_last_of('/') + 1);//slotInfo
sTmp = sTopic.substr(0, sTopic.find_last_of('/'));
std::string sCommand = sTmp.substr(sTmp.find_last_of('/') + 1);//request
//s_pLog->info("{} {} {} {} {}", sTopic, sTopicSrcName, sTopicDestName, sCommand, sInterface);
if (s_sCurAppName == sTopicSrcName) return;//收到自己发出的消息,丢弃
if (message->payloadlen >= MSG_NUM){
s_pLog->info(" topic: {} payload: {} too long!!!!", message->topic, byte2string((BYTE *)(message->payload), message->payloadlen), message->payloadlen);
return;
}
CUniformMkRec rec;
if (sInterface == "A-XDR"){
if(0 < rec.init((BYTE *)message->payload, message->payloadlen)){
/* 打印接收消息帧 */
s_pLog->info(">> IID: {:04x} IOP: {:04x} Content: {}", rec.getTAG_IID(), rec.getTAG_IOP(), GetUniformMsgDesc(rec.getPRM(), rec.getTAG_IID(), rec.getTAG_IOP()));
s_pLog->info(" topic: {} payload: {}", message->topic, byte2string((BYTE *)(message->payload), message->payloadlen));
uc_node node;
node.qos = message->qos; node.bJson = 0; node.bPrm = rec.getPRM();
node.wIndex = rec.getINDEX();node.wLabel = rec.getLABEL();
node.sSource = rec.getSOURCE();node.sDestination = rec.getDESTINATION();
node.dwTag = rec.getTAG();
memcpy(node.pbRecBuf, (BYTE *)message->payload, message->payloadlen);
node.wRecBufLen = message->payloadlen;
if (node.dwTag == 0x00040013 || node.dwTag == 0x00040014 || node.dwTag == 0x00040015)
node.bNeedTime = 1;
if (node.bPrm) s_QueMsg->reqPush(node);
else s_QueMsg->postAskReq(node);
return;
}
}
else{
if (!rec.initJson((BYTE *)message->payload)){
s_pLog->info(" topic: {} payload: {}", message->topic, byte2string((BYTE *)(message->payload), message->payloadlen));
uc_node node;
node.qos = message->qos;
node.bJson = 100;
node.sSource = sTopicSrcName;
node.sDestination = sTopicDestName;
node.dwToken = rec.getToken();
node.sInterface = sInterface;
node.sCommand = sCommand;
memcpy(node.pbRecBuf, (BYTE *)message->payload, message->payloadlen);
node.wRecBufLen = message->payloadlen;
if (node.sInterface == "modRestart" || node.sInterface == "slotReset"
|| node.sInterface == "fileTrans" || node.sInterface == "dataTrans")
node.bNeedTime = 1;
if (sTopicDestName == s_sCurAppName && sCommand == "request") s_QueMsg->reqPush(node);
else if (sTopicDestName == s_sCurAppName && sCommand == "response") s_QueMsg->postAskReq(node);
return;
}
}
/* 打印错误消息帧 */
s_pLog->error(">> Frame format is incomplete! topic: {} payload: {}", message->topic, byte2string((BYTE *)(message->payload), message->payloadlen));
}
异步同步请求消息入栈
同步:
int CUniformCommList::syncSendJson(std::string sDestination, std::string sInterface, DWORD dwToken, BYTE *pbMsg, BYTE *pbDestBuf, int *BufLen, int BufSize, DWORD dwOvertime)
{
std::string send_topic = makeTopicJson(1, sDestination, sInterface);
uv_sem_t sem;//信号量需要在栈上申请,与windows不同,linux的键应该是这个值的地址,值为这个值,需要全局和唯一;windows键就是这个值,所以复制一个就可以用
uv_sem_init(&sem, 0);
/* 新建通信节点 */
uc_node node;
node.sem = &sem;
makeNodeJson(node, sDestination, sInterface, dwToken, dwOvertime, NULL);
/* 尾插节点 */
s_QueMsg->askSendPush(node);
/* 打印发送消息帧 */
mqttPublish(0xffff, 0xffff, send_topic.c_str(), pbMsg, strlen((char *)pbMsg));
/* 等待信号量,会阻塞 */
uv_sem_wait(&sem);
uv_sem_destroy(&sem);
node = s_QueMsg->askSendPop(node);
if (!node.ret){
memcpy(pbDestBuf, node.pbRecBuf, node.wRecBufLen);
*BufLen = node.wRecBufLen;
}
return node.ret;
}
异步:
int CUniformCommList::asyncSendJson(std::string sDestination, std::string sInterface, DWORD dwToken, BYTE *pbMsg, void (*recv_cb)(int ret, BYTE *pbRecBuf, WORD wRecBufLen))
{
std::string send_topic = makeTopicJson(1, sDestination, sInterface);
/* 新建通信节点 */
uc_node node;
makeNodeJson(node, sDestination, sInterface, dwToken, 0, recv_cb);
/* 尾插节点 */
s_QueMsg->askSendPush(node);
/* 打印发送消息帧 */
mqttPublish(0xffff, 0xffff, send_topic.c_str(), pbMsg, strlen((char *)pbMsg));
return 0;
}
被请求消息处理线程
static void reqPopThread(void *_para)
{
while(1){
if(!s_QueMsg->reqRcvEmpty())
reqThread(NULL);//处理消息
else
uv_sleep(100);
}
}
static void reqThread(void *_para)
{
uc_node *reqNode = new uc_node();//在处理函数内部需要释放
s_QueMsg->reqRcvPop(*reqNode);
if (reqNode->bNeedTime){//需要时间处理,新建线程
uv_thread_t thread;
uv_thread_create(&thread, common_do_recv, (void *)reqNode);
#ifndef WIN32
pthread_detach(thread);
#endif // !WIN32
}
else{
common_do_recv((void *)reqNode);
}
return;
}
过时节点消息处理线程
static void askRmOverTimeThread(void *_para)
{
DWORD dwSavetime = 1000;/* 大约 1s 循环一次 */
while (1){
uv_sleep(dwSavetime);
s_QueMsg->askRmOverTime(dwSavetime);
}
}