物联网协议应用

news2024/11/25 10:38:44

目录

  • 前言
  • 一、NTP协议
    • 1.1 NTP简介
    • 1.2 NTP实现
  • 二、HTTP协议
    • 2.1 HTTP协议简介
    • 2.2 HTTP服务器
  • 三、MQTT协议
    • 3.1 MQTT协议简介
      • 3.1.1 MQTT通信模型
      • 3.1.2 MQTT协议实现原理
      • 3.1.3 MQTT 控制报文
    • 3.2 移植MQTT协议


前言

  本文主要介绍一下物联网协议如NTP协议HTTP协议MQTT协议的接口使用,本文需要了解一下TCP/IP协议和lwIP协议
  参考以下链接:https://blog.csdn.net/weixin_44567668/article/details/139619797


一、NTP协议

1.1 NTP简介

  NTP(Network Time Protocol)网络时间协议基于UDP,是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS 等等)做同步化,它可以提供高精准度的时间校正(LAN 上与标准间差小于 1 毫秒,WAN 上几十毫秒),且可介由加密确认的方式来防止恶毒的协议攻击。时间按 NTP 服务器的等级传播。按照离外部 UTC 源的远近把所有服务器归入不同的 Stratum(层)中。
  NTP数据报文格式,如下图所示:
在这里插入图片描述
  NTP数据报文格式的各个字段的作用,如下表所示:
在这里插入图片描述
在这里插入图片描述
  从上表可知,NTP 报文的字段非常多,这些字段并不是每一个都必须设置的,可以根据项目的需要来构建 NTP 请求报文。
在这里插入图片描述

1.2 NTP实现

  由上可以知道获取 NTP 实时时间步骤了:

① 以 UDP 协议连接阿里云 NTP 服务器
② 发送 NTP 报文到阿里云 NTP 服务器
③ 获取阿里云 NTP服务器返回的数据,取第 40 位到 43 位的十六进制数值。
④ 把 40 位到 43 位的十六进制数值转成十进制
⑤ 把十进制数值减去1900-1970 的时间差(2208988800 秒)
⑥ 数值转成年月日时分秒

在这里插入图片描述

  1. lwip_demo.h头文件
      主要创建两个结构体,一个用来获取参数,一个用来显示时间
#define NTP_DEMO_RX_BUFSIZE   2000  /* 定义udp最大接收数据长度 */
#define NTP_DEMO_PORT         123   /* 定义udp连接的本地端口号 */

typedef struct _NPTformat
{
    char    version;            /* 版本号 */
    char    leap;               /* 时钟同步 */
    char    mode;               /* 模式 */
    char    stratum;            /* 系统时钟的层数 */
    char    poll;               /* 更新间隔 */
    signed char  precision;     /* 精密度 */
    unsigned int   rootdelay;   /* 本地到主参考时钟源的往返时间 */
    unsigned int   rootdisp;    /* 统时钟相对于主参考时钟的最大误差 */
    char    refid;              /* 参考识别码 */
    unsigned long long  reftime;/* 参考时间 */
    unsigned long long  org;    /* 开始的时间戳 */
    unsigned long long  rec;    /* 收到的时间戳 */
    unsigned long long  xmt;    /* 传输时间戳 */
} NPTformat;

typedef struct _DateTime  /*此结构体定义了NTP时间同步的相关变量*/
{
    int  year;        /* 年 */
    int  month;       /* 月 */
    int  day;         /* 天 */
    int  hour;        /* 时 */
    int  minute;      /* 分 */
    int  second;      /* 秒 */
} DateTime;

#define SECS_PERDAY     86400UL         /* 一天中的几秒钟 = 60*60*24 */
#define UTC_ADJ_HRS     8               /* SEOUL : GMT+8(东八区北京)  */
#define EPOCH           1900            /* NTP 起始年  */
#define HOST_NAME  "ntp1.aliyun.com"    /*阿里云NTP服务器域名 */
  1. lwip_demo.c源文件
#define NTP_TIMESTAMP_DELTA  2208988800UL
const char g_days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
NPTformat g_ntpformat;                                                        /* NT数据包结构体 */
DateTime g_nowdate;                                                           /* 时间结构体 */
uint8_t g_ntp_message[48];                                                    /* 发送数据包的缓存区 */  
uint8_t g_ntp_demo_recvbuf[NTP_DEMO_RX_BUFSIZE];                              /* NTP接收数据缓冲区 */

uint8_t g_lwip_time_buf[100];

/**
 *@brief     计算日期时间
 *@param     secondsUTC 世界标准时间
 *@retval    无
*/
void lwip_calc_date_time(unsigned long long time)
{
   unsigned int Pass4year;
    int hours_per_year;
    if (time <= 0)
    {
        time = 0;
    }

    g_nowdate.second = (int)(time % 60);   /* 取秒时间 */
    time /= 60;

    g_nowdate.minute = (int)(time % 60);   /* 取分钟时间 */
    time /= 60;
    
    g_nowdate.hour = (int)(time % 24);     /* 小时数 */

    Pass4year = ((unsigned int)time / (1461L * 24L));/* 取过去多少个四年,每四年有 1461*24 小时 */

    g_nowdate.year = (Pass4year << 2) + 1970;    /* 计算年份 */

    time %= 1461 * 24;     /* 四年中剩下的小时数 */

    for (;;)               /* 校正闰年影响的年份,计算一年中剩下的小时数 */
    {
        hours_per_year = 365 * 24;         /* 一年的小时数 */

        if ((g_nowdate.year & 3) == 0) /* 判断闰年 */
        {

            hours_per_year += 24;          /* 是闰年,一年则多24小时,即一天 */
        }

        if (time < hours_per_year)
        {
            break;
        }

        g_nowdate.year++;
        time -= hours_per_year;
    }

    time /= 24;   /* 一年中剩下的天数 */

    time++;       /* 假定为闰年 */

    if ((g_nowdate.year & 3) == 0)      /* 校正闰年的误差,计算月份,日期 */
    {
        if (time > 60)
        {
            time--;
        }
        else
        {
            if (time == 60)
            {
                g_nowdate.month = 1;
                g_nowdate.day = 29;
                return ;
            }
        }
    }

    for (g_nowdate.month = 0; g_days[g_nowdate.month] < time; g_nowdate.month++)   /* 计算月日 */
    {
        time -= g_days[g_nowdate.month];
    }

    g_nowdate.day = (int)(time);

    return;

}

/**
 *@brief     从NTP服务器获取时间
 *@param     buf:存放缓存
 *@param     idx:定义存放数据起始位置
 *@retval    无
*/
void lwip_get_seconds_from_ntp_server(uint8_t *buf, uint16_t idx)
{
    unsigned long long atk_seconds = 0; 
    uint8_t i = 0;

    for (i = 0; i < 4; i++)  /* 获取40~43位的数据 */
    {
        atk_seconds = (atk_seconds << 8) | buf[idx + i]; /* 把40~43位转成16进制再转成十进制 */
    }

    atk_seconds -= NTP_TIMESTAMP_DELTA;/* 减去减去1900-1970的时间差(2208988800秒) */
    lwip_calc_date_time(atk_seconds);       /* 由UTC时间计算日期 */
}

/**
 *@brief     初始化NTP Client信息
 *@param     无
 *@retval    无
*/
void lwip_ntp_client_init(void)
{
    uint8_t flag;

    g_ntpformat.leap = 0;           /* leap indicator */
    g_ntpformat.version = 3;        /* version number */
    g_ntpformat.mode = 3;           /* mode */
    g_ntpformat.stratum = 0;        /* stratum */
    g_ntpformat.poll = 0;           /* poll interval */
    g_ntpformat.precision = 0;      /* precision */
    g_ntpformat.rootdelay = 0;      /* root delay */
    g_ntpformat.rootdisp = 0;       /* root dispersion */
    g_ntpformat.refid = 0;          /* reference ID */
    g_ntpformat.reftime = 0;        /* reference time */
    g_ntpformat.org = 0;            /* origin timestamp */
    g_ntpformat.rec = 0;            /* receive timestamp */
    g_ntpformat.xmt = 0;            /* transmit timestamp */

    flag = (g_ntpformat.version << 3) + g_ntpformat.mode; /* one byte Flag */
    memcpy(g_ntp_message, (void const *)(&flag), 1);
}

/**
 * @brief  lwip_demo程序入口
 * @param  无
 * @retval 无
*/
void lwip_demo(void)
{
    err_t err;
    static struct netconn *udpconn;
    static struct netbuf  *recvbuf;
    static struct netbuf  *sentbuf;
    ip_addr_t destipaddr;
    uint32_t data_len = 0;
    struct pbuf *q;
    lwip_ntp_client_init();
    /* 第一步:创建udp控制块 */
    udpconn = netconn_new(NETCONN_UDP);
    /* 定义接收超时时间 */
    udpconn->recv_timeout = 10;

    if (udpconn != NULL) /* 判断创建控制块释放成功 */
    {
        /* 第二步:绑定控制块、本地IP和端口 */
        err = netconn_bind(udpconn, IP_ADDR_ANY, NTP_DEMO_PORT);
        /* 域名解析 */
        netconn_gethostbyname((char *)(HOST_NAME), &(destipaddr));
        /* 第三步:连接或者建立对话框 */
        netconn_connect(udpconn, &destipaddr, NTP_DEMO_PORT); /* 连接到远端主机 */

        if (err == ERR_OK) /* 绑定完成 */
        {
            while (1)
            {
                sentbuf = netbuf_new();
                netbuf_alloc(sentbuf, 48);
                memcpy(sentbuf->p->payload, (void *)g_ntp_message, sizeof(g_ntp_message));
                err = netconn_send(udpconn, sentbuf); /* 将sentbuf中的数据发送出去 */
                if (err != ERR_OK)
                {
                    printf("发送失败\r\n");
                    netbuf_delete(sentbuf); /* 删除buf */
                }
                netbuf_delete(sentbuf);      /* 删除buf */

                /* 第五步:接收数据 */
                netconn_recv(udpconn, &recvbuf);
                vTaskDelay(1000);     /* 延时1s */
                if (recvbuf != NULL)  /* 接收到数据 */
                {
                    memset(g_ntp_demo_recvbuf, 0, NTP_DEMO_RX_BUFSIZE); /*数据接收缓冲区清零 */

                    for (q = recvbuf->p; q != NULL; q = q->next) /*遍历完整个pbuf链表 */
                    {
                        /* 判断要拷贝到UDP_DEMO_RX_BUFSIZE中的数据是否大于UDP_DEMO_RX_BUFSIZE的剩余空间,如果大于 */
                        /* 的话就只拷贝UDP_DEMO_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据 */
                        if (q->len > (NTP_DEMO_RX_BUFSIZE - data_len)) memcpy(g_ntp_demo_recvbuf + data_len, q->payload, (NTP_DEMO_RX_BUFSIZE - data_len)); /* 拷贝数据 */
                        else memcpy(g_ntp_demo_recvbuf + data_len, q->payload, q->len);

                        data_len += q->len;

                        if (data_len > NTP_DEMO_RX_BUFSIZE) break;          /* 超出TCP客户端接收数组,跳出 */
                    }

                    data_len = 0;                                           /* 复制完成后data_len要清零 */
                    lwip_get_seconds_from_ntp_server(g_ntp_demo_recvbuf,40);   /* 从NTP服务器获取时间 */
                    printf("北京时间:%02d-%02d-%02d %02d:%02d:%02d\r\n",  
                           g_nowdate.year, 
                           g_nowdate.month + 1,
                           g_nowdate.day,
                           g_nowdate.hour + 8,
                           g_nowdate.minute,
                           g_nowdate.second);
                    sprintf((char*)g_lwip_time_buf,"BJ time:%02d-%02d-%02d %02d:%02d:%02d", g_nowdate.year, 
                                                                                               g_nowdate.month + 1,
                                                                                               g_nowdate.day,
                                                                                               g_nowdate.hour + 8,
                                                                                               g_nowdate.minute,
                                                                                               g_nowdate.second);
                    lcd_show_string(5, 170, lcddev.width, 16, 16, (char*)g_lwip_time_buf, RED);
                    
                    netbuf_delete(recvbuf);             /* 删除buf */
                }
                else vTaskDelay(5);                     /* 延时5ms */
            }
        }
        else printf("NTP绑定失败\r\n");
    }
    else printf("NTP连接创建失败\r\n");
}

  在此文件下定义了四个函数,这些函数的作用如下表所示:

函数描述
lwip_demo()实现UDP连接,使用NETCONN接口
lwip_ntp_client_init()构建NTP请求报文
lwip_get_seconds_from_ntp_server()获取NTP服务器的数据
lwip_calc_date_time()计算日期时间

二、HTTP协议

2.1 HTTP协议简介

  HTTP(Hypertext Transfer Protocol)协议,即超文本传输协议,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。HTTP 协议是基于TCP/IP 协议的网络应用层协议。默认端口为80端口。HTTP 协议是一种请求/响应式的协议。一个客户端与服务器建立连接之后,发送一个请求给服务器。服务器接收到请求之后,通过接收到的信息判断响应方式,并且给予客户端相应的响应,完成整个 HTTP数据交互流程。
在这里插入图片描述
  HTTP定义了与服务器交互的不同方法,其最基本的方法是 GET、PORT 和 HEAD。如下图所示。
在这里插入图片描述

  • GET:从服务端获取数据。
  • PORT:向服务器传送数据。
  • HEAD:检测一个对象是否存在。

  互联网通过URL来定位,URL全称是 Uniform Resource Locator,是互联网上用来标识某一处资源的绝对地址,大部分 URL 都会遵循 URL 的语法,一个 URL 的组成有多个不同的组件,一个 URL的通用格式如下:

<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>

在这里插入图片描述
在这里插入图片描述

  HTTP 报文是由 3 个部分组成,分别是:对报文进行描述的“起始行”,包含属性的“首部”,以及可选的“数据主体”,对于请求报文与应答报文,只有“起始行”的格式是不一样的。起始行和首部就是由行分隔的 ASCII 文本组成,每行都以由两个字符组成的行终止序列作为结束,其中包括一个回车符(ASCII 码 13)和一个换行符(ASCII 码 10), 这个行终止序列可以写做 CRLF。

# HTTP请求报文
<method> <request-URL> <version> //起始行
<headers> 						//首部
 
<entity-body> 					//数据主体

# HTTP应答报文
<version> <status> <reason-phrase> //起始行
<headers> 							//首部

<entity-body> 						//数据主体

  下面就对这两种 HTTP 报文的各个部分简单描述一下:

  • 方法(method):HTTP 请求报文的起始行以方法作为开始,方法用来告知服务器要做些什么,常见的方法有 GET、POST、HEAD 等,比如“GET /forum.php HTTP/1.1” 使用的就是 GET 方法。
  • 请求 URL(request-URL):指定了所请求的资源。
  • 版本(version):指定报文所使用的 HTTP 协议版本,其中指定了主要版本号, 指定了次要版本号,它们都是整数,其格式如下:
HTTP/<major>.<minor>
  • 状态码(status):这是在 HTTP 应答报文中使用的,状态码是在每条响应报文的起始行中返回的一个数字码,描述了请求过程中所发送的情况,比如成功、失败等,不同的状态码有不同的含义,具体见表格
    在这里插入图片描述
  • 原因短语(reason-phrase):这其实是给我们看的原因短语,因为数字是不够直观,它只是状态码的一个文本形式表达而已。
  • 首部(header):HTTP 报文可以有 0 个、1 个或者多个首部,HTTP 首部字段向请求和响应报文中添加了一些附加信息,从本质上来说,它们是一个<名字:值>对,每个首部都包含一个名字,紧跟着一个冒号“:”,然后是一个可选的空格,接着是一个值,最后以 CRLF 结束,比如“Host: www.firebbs.cn”就是一个首部。
  • 数据主体(entity-body):这部分包含一个由任意数据组成的数据块,其实这与我们前面所讲的报文数据区域是一样的,用于携带数据,HTTP 报文可以承载很多类型的数字数据:图片、视频、音频、HTML 文档、软件应用程 序等。

2.2 HTTP服务器

  HTTP协议可以应用在客户端,也可以在服务器端,在客户端可以用来获取服务器数据,比如从服务器下载固件进行升级。也可以用在服务器端,那样我们可以做一个简单网页来访问控制单片机。同上一个例程一样,新建一个任务调用函数lwip_demo()

/* HTTP报头总是以响应码开头(例如HTTP/1.1 200 OK)和一个内容类型,以便客户端知道接下来是什么,然后是一个空行: */
/* 浏览器响应数据类型为文本数据 */
static const char http_html_hdr[] = "HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n";
static const char http_index_html[] = 
                                    "<!DOCTYPE html>"\
                                    "<html>"\
                                    "<head>"\
                                    "<title> Webserver实验 </title>"\
                                    "<meta http-equiv='Content-Type' content='text/html; charset=GB2312'/>"\
                                    "</head>"\
                                    "<body>"\
                                    "<h1>http server</h1>"\
                                    "<div class='label' >"\
                                    "<label>LED State:</label>"\
                                    "</div>"\
                                    "<div class='checkboxes'>"\
                                    "<input type='checkbox' name='led1' value='1' />打开&nbsp;&nbsp;&nbsp;<input type='checkbox' name='led1' value='2' />关闭"\
                                    "</div>"\
                                    "<br>"\
                                    "<br>"\
                                    "<div class='label'>"\
                                    "<label>BEEP State:</label>"\
                                    "</div>"\
                                    "<div class='checkboxes'>"\
                                    "<input type='checkbox' name='led1' value='1' />打开&nbsp;&nbsp;&nbsp;<input type='checkbox' name='led1' value='2' />关闭"\
                                    "</div>"\
                                    "<br>"\
                                    "<br>"\
                                    "<input type='submit' class='sendbtn' value='发送'>"\
                                    "<br>"\
                                    "</body>"\
                                    "</html>";

/**
 * @brief       寻找指定字符位置
 * @param       buf   缓冲区指针
 * @param       name  寻找字符
 * @retval      返回字符的地址
 */
char *lwip_data_locate(char *buf, char *name)
{
    char *p;
    p = strstr((char *)buf, name);

    if (p == NULL)
    {
        return NULL;
    }

    p += strlen(name);
    return p;
}

/**
 * @brief       服务HTTP线程中接受的一个HTTP连接
 * @param       conn   netconn控制块
 * @retval      无
 */
static void lwip_server_netconn_serve(struct netconn *conn)
{
    struct netbuf *inbuf;
    char *buf;
    u16_t buflen;
    err_t err;
    char *ptemp;
    /* 从端口读取数据,如果那里还没有数据,则阻塞。
      我们假设请求(我们关心的部分)在一个netbuf中 */
    err = netconn_recv(conn, &inbuf);

    if (err == ERR_OK)
    {
        netbuf_data(inbuf, (void **)&buf, &buflen);

        /* 这是一个HTTP GET命令吗?只检查前5个字符,因为
           GET还有其他格式,我们保持简单)*/
        if (buflen >= 5 &&
                buf[0] == 'G' &&
                buf[1] == 'E' &&
                buf[2] == 'T' &&
                buf[3] == ' ' &&
                buf[4] == '/' )
        {

start_html:
            /* 发送HTML标题
            从大小中减去1,因为我们没有在字符串中发送\0
            NETCONN_NOCOPY:我们的数据是常量静态的,所以不需要复制它 */
            netconn_write(conn, http_html_hdr, sizeof(http_html_hdr) - 1, NETCONN_NOCOPY);

            /* 发送我们的HTML页面 */
            netconn_write(conn, http_index_html, sizeof(http_index_html) - 1, NETCONN_NOCOPY);
        }
        else if(buflen>=8&&buf[0]=='P'&&buf[1]=='O'&&buf[2]=='S'&&buf[3]=='T')
        {
            ptemp = lwip_data_locate((char *)buf, "led1=");

            if (ptemp != NULL)
            {
                if (*ptemp == '1')    /* 查看led1的值。为1则灯亮,为2则灭,此值与HTML网页中设置有关 */
                {
                    LED0(0);  /* 点亮LED1 */
                }
                else
                {
                    LED0(1);    /* 熄灭LED1 */
                }

            }

            ptemp = lwip_data_locate((char *)buf, "beep=");    /* 查看beep的值。为3则灯亮,为4则灭,此值与HTML网页中设置有关 */

            if (ptemp != NULL )
            {
                if (*ptemp == '3')
                {
                    /* 打开蜂鸣器 */
                }
                else
                {
                    /* 关闭蜂鸣器 */
                }
            }
            goto start_html;
        }
    }

    /* 关闭连接(服务器在HTTP中关闭) */
    netconn_close(conn);

    /* 删除缓冲区(netconn_recv给我们所有权,
       所以我们必须确保释放缓冲区) */
    netbuf_delete(inbuf);
}

/**
* @brief  lwip_demo程序入口
* @param  无
* @retval 无
*/
void lwip_demo(void)
{
    struct netconn *conn, *newconn;
    err_t err;

    /* 创建一个新的TCP连接句柄 */
    /* 使用默认IP地址绑定到端口80 (HTTP) */

    conn = netconn_new(NETCONN_TCP);
    netconn_bind(conn, IP_ADDR_ANY, 80);

    /* 将连接置于侦听状态 */
    netconn_listen(conn);

    do
    {
        err = netconn_accept(conn, &newconn);

        if (err == ERR_OK)
        {
            lwip_server_netconn_serve(newconn);//调用HTTP服务器子程序
            netconn_delete(newconn);
        }
    }
    while (err == ERR_OK);

    netconn_close(conn);
    netconn_delete(conn);
}
  • lwip_demo():建立 TCP 连接
  • lwip_data_locate():寻找指定字符位置
  • lwip_server_netconn_serve():服务 HTTP 线程中接受的一个HTTP连接,主要分为三步:
    • 当浏览器输入IP地址并且回车确认时,程序调用函数netconn_write把网页数据发送到浏览器当中
    • 当网页发送一个PORT命令时,程序调用函数lwip_data_locate判断触发源,判断完成之后根据触发源来执行相应的动作
    • 程序执行goto语句重新发送网页字符串到网页当中,这个步骤相当于更新网页,网页样式如下:

在这里插入图片描述

  其中网页格式为HTML,具体语法可参考:

  • HTML5超文本标记语言:https://blog.csdn.net/weixin_44567668/article/details/125626370
  • CSS3层叠样式表:https://blog.csdn.net/weixin_44567668/article/details/132521477

三、MQTT协议

3.1 MQTT协议简介

  MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一个基于客户端-服务器的消息发布/订阅(publish/subscribe)传输协议,该协议构建于 TCP/IP 协议上,由 IBM 在 1999 年发布。

3.1.1 MQTT通信模型

  实现 MQTT 协议需要:客户端和服务器端 MQTT 协议中有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者,如下图所示
在这里插入图片描述
  MQTT 传输的消息分为:主题(Topic)和消息的内容(payload)两部分

  • Topic:可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload)。
  • Payload:可以理解为消息的内容,是指订阅者具体要使用的内容。

3.1.2 MQTT协议实现原理

  要在客户端与代理服务端建立一个 TCP 连接,建立连接的过程是由客户端主动发起的,代理服务一直是处于指定端口的监听状态,当监听到有客户端要接入的时候,就会立刻去处理。客户端在发起连接请求时,携带客户端 ID、账号、密码、心跳间隔时间等数据。代理服务收到后检查自己的连接权限配置中是否允许该账号密码连接,如果允许则建立会话标识并保存,绑定客户端 ID 与会话,并记录心跳间隔时间(判断是否掉线和启动遗嘱时用)和遗嘱消息等,然后回发连接成功确认消息给客户端,客户端收到连接成功的确认消息后,进入下一步(通常是开始订阅主题,如果不需要订阅则跳过)。如下图所示:
在这里插入图片描述
  客户端将需要订阅的主题经过 SUBSCRIBE 报文发送给代理服务,代理服务则将这个主题记录到该客户端 ID 下(以后有这个主题发布就会发送给该客户端),然后回复确认消息SUBACK 报文,客户端接到 SUBACK 报文后知道已经订阅成功,则处于等待监听代理服务推送的消息,也可以继续订阅其他主题或发布主题,如下图所示:
在这里插入图片描述
  当某一客户端发布一个主题到代理服务后,代理服务先回复该客户端收到主题的确认消息,该客户端收到确认后就可以继续自己的逻辑了。但这时主题消息还没有发给订阅了这个主题的客户端,代理要根据质量级别(QoS)来决定怎样处理这个主题。所以这里充分体现了是MQTT 协议是异步通信模式,不是立即端到端反应的,如下图所示:
在这里插入图片描述

  • 如果发布和订阅时的质量级别 QoS 都是至多一次,那代理服务则检查当前订阅这个主题的客户端是否在线,在线则转发一次,收到与否不再做任何处理。这种质量对系统压力最小。
  • 如果发布和订阅时的质量级别 QoS 都是至少一次,那要保证代理服务和订阅的客户端都有成功收到才可以,否则会尝试补充发送(具体机制后面讨论)。这也可能会出现同一主题多次重复发送的情况。这种质量对系统压力较大。
  • 如果发布和订阅时的质量级别 QoS 都是只有一次,那要保证代理服务和订阅的客户端都有成功收到,并只收到一次不会重复发送(具体机制后面讨论)。这种质量对系统压力最大。

3.1.3 MQTT 控制报文

  1. 固定报头
      MQTT 协议工作在 TCP 协议之上,因为客户端和服务器都是应用层,那么必然需要一种协议在两者之间进行通信,那么随之而来的就是 MQTT 控制报文, MQTT 控制报文有3个部分组成,分别是固定报头(fixed header)、可变报头(variable header)、有效荷载(数据区域 payload)。固定报头,所有的 MQTT 控制报文都包含,可变报头与有效载荷是部分 MQTT 控制报文包含。固定报头占据两字节的空间,具体见图
    在这里插入图片描述
      固定报头的第一个字节分为控制报文的类型(4bit),以及控制报文类型的标志位,控制类型共有 14 种,其中0与15被系统保留出来,其他的类型具体见表格
    在这里插入图片描述
      固定报头的 bit0-bit3 为标志位,依照报文类型有不同的含义,事实上,除了 PUBLISH类型报文以外,其他报文的标志位均为系统保留,PUBLISH 报文的第一字节 bit3 是控制报文的重复分发标志(DUP),bit1-bit2 是服务质量等级,bit0 是 PUBLISH 报文的保留标志,用于标识 PUBLISH 是否保留,当客户端发送一个 PUBLISH 消息到服务器,如果保留标识位置 1,那么服务器应该保留这条消息,当一个新的订阅者订阅这个主题的时候,最后保留的主题消息应被发送到新订阅的用户。
      固定报头的第二个字节开始是剩余长度字段,是用于记录剩余报文长度的,表示当前的消息剩余的字节数,包括可变报头和有效载荷区域(如果存在),但剩余长度不包括用于编码剩余长度字段本身的字节数。
      剩余长度字段使用一个变长度编码方案,对小于 128 的值它使用单字节编码,而对于更大的数值则按下面的方式处理:每个字节的低 7 位用于编码数据长度,最高位(bit7)用于标识剩余长度字段是否有更多的字节,且按照大端模式进行编码,因此每个字节可以编码 128 个数值和一个延续位,剩余长度字段最大可拥有 4 个字节。

  2. 可变报头
      可变报头并不是所有的 MQTT 报文都带有的(比如 PINGREQ 心跳请求与 PINGRESP心跳响应报文就没有可变报头),只有某些报文才拥有可变报头,它在固定报头和有效负载之间,可变报头的内容会根据报文类型的不同而有所不同,但可变报头的报文标识符(Packet Identifier)字段存在于在多个类型的报文里,而有一些报文又没有报文标识符字段,具体见表格
    在这里插入图片描述
      报文标识符结构具体见图
    在这里插入图片描述
      因为对于不同的报文,可变报头是不一样的,下面就简单讲解几个报文的可变报头

  • CONNECT
      在一个会话中,客户端只能发送一次 CONNECT 报文,它是客户端用于请求连接服务器的报文,常称之为连接报文,如果客户端发送多次连接报文,那么服务端必须将客户端发送的第二个 CONNECT 报文当作协议违规处理并断开客户端的连接。CONNECT 报文的可变报头包含四个字段:协议名(Protocol Name)、协议级别(Protocol Level)、连接标志(Connect Flags)以及保持连接(Keep Alive)字段。协议名是 MQTT 的 UTF-8 编码的字符串,其中还包含用于记录协议名长度的两字节字段 MSB 与 LSB。
      在协议名之后的是协议级别,MQTT 协议使用 8 位的无符号值表示协议的修订版本,对于 MQTT3.1 版的协议,协议级别字段的值是 3(0x03),而对于 MQTT3.1.1 版的协议,协议级别字段的值是 4(0x04)。如果服务器发现连接报文中的协议级别字段是不支持的协议级别,服务端必须给发送一个返回码为 0x01(不支持的协议级别)的 CONNACK 响应连接报文,然后终止客户端的连接请求。连接标志字段涉及的内容比较多,它在协议级别之后使用一个字节表示,但分成很多个标志位,具体见图
    在这里插入图片描述
    • bit0:是 MQTT 保留的标志位,在连接过程中,服务器会检测连接标志的 bit0 是否为 0,如果不为 0 则服务器任务这个连接报文是不合法的,会终止连接请求。
    • bit1:是清除会话标志 Clean Session,如果清除会话标志设置为 1,那么客户端不会收到旧的应用消息,清除会话标志设置为 0 的客户端在重新连接后会收到所有在它连接断开期间(其他发布者)发布的 QoS1 和 QoS2 级别的消息。因此,要确保不丢失连接断开期间的消息,需要使用 QoS1 或 QoS2 级别,同时将清除会话标志设置为 0。
    • bit2:是遗嘱标志 Will Flag,如果该位被设置为 1,表示如果客户端与服务器建立了会话,遗嘱消息(Will Message)将必须被存储在服务器中,当这个客户端断开连接的时候,遗嘱消息将被发送到订阅这个会话主题的所有订阅者,这个消息是很有用的,我们可以知道这个设备的状况,它是否已经掉线了,以备启动备用方案,当然,想要不发送遗嘱消息也是可以的,只需要让服务器端收到 DISCONNECT 报文时删除这个遗嘱消息即可。
    • bit3-bit4:用于指定发布遗嘱消息时使用的服务质量等级,与其他消息的服务质量是一样的,遗嘱 QoS 的值可以等于 0(0x00),1(0x01),2(0x02),当然,使用遗嘱消息的前提是遗嘱标志位为 1。
    • bit5:表示遗嘱保留标志位,当客户端意外断开连接时,如果 Will Retain 置一,那么服务器必须将遗嘱消息当作保留消息发布,反之则无需保留。
    • bit6:是密码标志位 Password Flag,如果密码标志被设置为 0,有效载荷中不能包含密码字段,反之则必须包含密码字段。
    • bit7:是用户名标志位 User Name Flag,如果用户名标志被设置为 0,有效载荷中不能包
      含用户名字段,反之则必须包含用户名字段。

  总的来说,整个 CONNECT 报文可变报头的内容如下:
在这里插入图片描述

  • CONNACK
      它是由连接确认标志字段(Connect Acknowledge Flags)与连接返回码字段 (Connect Return code)组成,各占用 1 个字节。它的第 1 个字节是 连接确认标志字段,bit1-bit7 是保留位且必须设置为 0, bit0 是当前会话(Session Present)标志位。它的第 2 个字节是返回码字段,如果服务器收到一个 CONNECT 报文,但出于某些原因无法处理它,服务器会返回一个包含返回码的 CONNACK 报文。如果服务器返回了一个返回码字段是非 0 的 CONNACK 报文,那么它必须关闭网络连接,返回码描述具体见表格
    在这里插入图片描述
      如果服务端收到清理会话(CleanSession)标志为 1 的连接,除了将 CONNACK报文中的返回码设置为 0 之外,还必须将 CONNACK 报文中的当前会话设置(Session Present)标志为 0。那么总的来说,CONNACK 报文的可变报头部分内容具体见图
    在这里插入图片描述

3.2 移植MQTT协议

  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1834115.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

elementPlus渲染需要权限校验的图片

技术栈&#xff1a;elementPlusvue3 html部分&#xff1a; <el-image :style"{width:width,height:height}" ref"previewimg" :src"previewSrc"></el-image> js部分 /*** 预览图片* param {String} url [图片的地址]*/const showP…

飞腾派初体验(3)

“我心光明&#xff0c;夫复何言” - 王阳明。 朋友的题词&#xff0c;念念不忘 做人要厚道&#xff0c;挖坑就要填。说了要整labview&#xff0c;现在来还愿。 国内labview还是相对小众的东西&#xff0c;而且主要是大厂设备在用&#xff0c;同时NI原厂比较佛系&#xff08;欧…

企业邮箱大附件无法上传?无法确认接收状态?这样解决就行

Outlook邮箱作为最常用的邮箱系统&#xff0c;被全世界企业采用作为内部通用沟通方式&#xff0c;但Outlook邮箱却有着明显的使用缺陷&#xff0c;如邮箱大附件上传障碍及附件接收无提示等。 1、企业邮箱大附件无法上传 Outlook企业邮箱大附件的上传上限一般是50M&#xff0c;…

opencv c++ 检测图像尺寸大小,标注轮廓

1. 项目背景 本项目旨在开发一个图像处理程序&#xff0c;通过使用计算机视觉技术&#xff0c;能够自动检测图像中物体的尺寸并进行分类。项目利用了开源的计算机视觉库 OpenCV&#xff0c;实现了图像的灰度处理、二值化、轮廓检测、边界框绘制以及尺寸分类等功能。通过这些功…

Java控制语句——break和continue

系列文章目录 文章目录 系列文章目录前言一、【break的例子】二、【continue的例子】 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂…

003 AOP介绍

文章目录 什么是AOP为什么用AOPAOP相关术语介绍AOP实现之AspectJ(了解)AOP实现之Spring AOP(了解)JDK动态代理Cglib动态代理 什么是AOP 在软件业中&#xff0c;AOP为Aspect Oriented Programming的缩写&#xff0c;意为面向切面编程作用&#xff1a;在不修改目标类代码的前提下…

Ubuntu乌班图安装VIM文本编辑器工具

系列文章目录 Ubuntu-24.04-live-server-amd64安装界面中文版 Ubuntu-24.04-live-server-amd64启用ssh Ubuntu安装qemu-guest-agent 文章目录 系列文章目录前言一、安装VIM&#xff1f;二、VIM基本设置总结 前言 从centos转到Ubuntu发现默认安装没有vi 一、安装VIM&#xff1…

服务器部署开源大模型完整教程 Ollama+Gemma+open-webui

现在开源的大模型其实挺多的&#xff0c;今天搭建Gemma这个谷歌开源的大模型&#xff0c;但是你想搭建别的只要你看完你都会了。 介绍 Ollama&#xff1a;一款可以让你在本地快速搭建大模型的工具 官网&#xff1a;https://ollama.com/ github&#xff1a;https://github.c…

【机器学习】智能创意工厂:机器学习驱动的AIGC,打造未来内容新生态

&#x1f680;时空传送门 &#x1f50d;机器学习在AIGC中的核心技术&#x1f4d5;深度学习&#x1f388;生成对抗网络&#xff08;GANs&#xff09; &#x1f680;机器学习在AIGC中的具体应用&#x1f340;图像生成与编辑⭐文本生成与对话系统&#x1f320;音频生成与语音合成 …

关于阿里云效流水线自动部署项目教程

1、登录阿里云效:阿里云登录 - 欢迎登录阿里云&#xff0c;安全稳定的云计算服务平台 2、点击左侧流水线&#xff1a; 3、在流水线界面&#xff0c;新建流水线 4、我的是php代码&#xff0c;因此选择php模版 5、创建之后添加流程线源&#xff0c;如下图 6、选择相应的源头。比…

【市场分析】TEMU平台美国区品牌产品市场分析Python采集爬虫数据

文章目录 一、引言二、数据概述1. 数据字段展示2. 数据统计信息展示 三、数据分析3.1 店铺托管模式3.1.1 半托管与全托管占比 3.2 品牌化最多的类目3.2.1 数据可视化展示3.2.2 各类目品牌商品占比分布 3.3 商品占比最多的Top品牌3.2.1 数据可视化展示3.2.2 品牌商品数量占比Top…

物联网技术-第6章-物联网应用案例

目录 1.共享单车 2.自动驾驶汽车 &#xff08;1&#xff09;概念 &#xff08;2&#xff09;关键技术 &#xff08;3&#xff09;典型代表 3.智能电网 4.智能交通 &#xff08;1&#xff09;车联网 &#xff08;2&#xff09;无人驾驶 5.智能物流 6.致谢 1.共享单车…

【语义分割系列】基于camvid数据集的Deeplabv3+分割算法(二)

基于camvid数据集的Deeplabv3+分割算法 前言 在前面的内容中,对比了Camvid数据集在基于不同backbone的Deeplabv3+算法上的效果。在这节内容中,本文将介绍在ghostnet的基础上,进一步优化效果,使得Miou提升。通过引入CFAC和CARAFE结构,有效地提升了模型的miou。 1.代码部…

JeecgFlow排他网关演示

排他网关概念理解 排他网关&#xff0c;也称为异或(XOR)网关&#xff0c;用于流程中实现分支决策建模。排他网关需要搭配条件顺序流使用。 当流程流转到排他网关时&#xff0c;所有流程顺序流都是会顺序求解&#xff0c; 其中第一条条件为true的顺序流会被选中(当有多条顺序流都…

每日一题——8行Python代码实现PAT乙级1029 旧键盘(举一反三+思想解读+逐步优化)五千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 我的写法 代码评析 时间复杂度 空间复杂度 我要更强 方法一&#xff1a;使用集合…

OpenGL Super Bible 7th - Drawing Our First Triangle(绘制第一个三角形)

简介 本文的原版为《OpenGL Super Bible 7th》,是同事给我的,翻译是原文+译文的形势。文章不属于机器直译,原因在于语言不存在一一对应的关系,我将尽可能的按照中国人看起来舒服的方式来翻译这些段子,如果段子让你感到身心愉悦,那还劳烦点个关注,追个更。如果我没有及时…

IT入门知识第四部分《数据库》(4/10)

目录 1. 数据库基础 1.1 数据库的定义 1.2 数据库的关键概念 数据模型 数据库架构 数据库操作语言&#xff08;DML 和 DDL&#xff09; 总结 2. 关系型数据库 2.1 MySQL MySQL 的历史和特点 MySQL 的安装和配置 MySQL 的基本操作 2.2 PostgreSQL PostgreSQL 的特…

软件测试期末复习

软件测试期末复习 Author 雨 2024年6月18日 1. 什么是软件测试 从一个通常为无限的执行域中选取合适的有限的测试用例&#xff0c;对程序所期望的行为进行动态验证的活动过程。 2. 软件测试的目的 尽早地发现软件的缺陷 3.什么是测试什么是缺陷 从软件内部看&#xff1a;软件开…

在 Stable Diffusion 中控制光线的三种方式

光线在摄影中扮演着至关重要的角色&#xff0c;并对图像的整体质量和意境产生重要影响。你可以利用光线来增强主题&#xff0c;创造深度和立体感&#xff0c;传达情感&#xff0c;并突出重要细节。 在本文中&#xff0c;你将了解通过以下方法来控制光线&#xff1a; 光线提示…

【推荐100个unity插件之21】unity实现多语言切换功能——Localization插件的使用

文章目录 前言优缺点优点缺点 安装创建配置选择语言选择默认语言创建多语言表数据创建key配置不同语言文本预加载绑定不同多语言文本数据&#xff0c;并显示语言切换自己编写按钮控制语言切换多语言图片切换在构建中使用Localization分析错误修复动态修改多语言文本内容参考推荐…