目录
前言
本文主要介绍一下物联网协议如NTP协议、HTTP协议和MQTT协议的接口使用。在了解物联网协议之前需要了解一下TCP/IP协议和lwIP协议,参考以下链接:https://blog.youkuaiyun.com/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 秒)
⑥ 数值转成年月日时分秒
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服务器域名 */
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 secondsUTC 世界标准时间
*@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)&