> 文章列表 > 【Mongoose笔记】SNTP 客户端

【Mongoose笔记】SNTP 客户端

【Mongoose笔记】SNTP 客户端

【Mongoose笔记】SNTP 客户端

简介

Mongoose 笔记系列用于记录学习 Mongoose 的一些内容。

Mongoose 是一个 C/C++ 的网络库。它为 TCP、UDP、HTTP、WebSocket、MQTT 实现了事件驱动的、非阻塞的 API。

项目地址:

https://github.com/cesanta/mongoose

学习

下面学习一下 Mongoose 项目代码中的 sntp-time-sync 示例程序 ,这个示例程序实现了一个简易的 SNTP 客户端,实现与远程 SNTP 服务器同步时间

示例程序代码如下:

// Copyright (c) 2022 Cesanta Software Limited
// All rights reserved
//
// Synchronize time with remote SNTP server
// A working time() call is required by TLS, so an embedded device without
// a clock that wants to use TLS, must sync time via SNTP.#include "mongoose.h"// The UNIX epoch of the boot time. Initially, we set it to 0.  But then after
// SNTP response, we update it to the correct value, which will allow us to
// use time(). Uptime in milliseconds is returned by mg_millis().
static time_t s_boot_timestamp = 0;// SNTP client connection
static struct mg_connection *s_sntp_conn = NULL;// On embedded systems, rename to time()
time_t my_time(time_t *tp) {time_t t = s_boot_timestamp + mg_millis() / 1000;if (tp != NULL) *tp = t;return t;
}// SNTP client callback
static void sfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {if (ev == MG_EV_SNTP_TIME) {int64_t t = *(int64_t *) ev_data;MG_INFO(("Got SNTP time: %lld ms from epoch", t));s_boot_timestamp = (time_t) ((t - mg_millis()) / 1000);} else if (ev == MG_EV_CLOSE) {s_sntp_conn = NULL;}(void) fn_data, (void) c;
}// Called every 5 seconds. Increase that for production case.
static void timer_fn(void *arg) {struct mg_mgr *mgr = (struct mg_mgr *) arg;if (s_sntp_conn == NULL) s_sntp_conn = mg_sntp_connect(mgr, NULL, sfn, NULL);if (s_sntp_conn != NULL) mg_sntp_request(s_sntp_conn);
}int main(void) {struct mg_mgr mgr;        // Event managermg_mgr_init(&mgr);        // Initialise event managermg_log_set(MG_LL_DEBUG);  // Set log levelmg_timer_add(&mgr, 5000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn, &mgr);for (;;) mg_mgr_poll(&mgr, 300);  // Infinite event loopmg_mgr_free(&mgr);                // Free manager resourcesreturn 0;
}

下面从main函数开始分析代码。

定义变量,struct mg_mgr是用于保存所有活动连接的事件管理器。

  struct mg_mgr mgr;        // Event manager

初始化一个事件管理器,也就是将上面定义的struct mg_mgr变量 mgr 中的数据进行初始化。

  mg_mgr_init(&mgr);        // Initialise event manager

设置 Mongoose 日志记录级别,设置等级为 MG_LL_DEBUG

  mg_log_set(MG_LL_DEBUG);  // Set log level

调用mg_timer_add设置一个定时器,这会将其添加到事件管理器的内部定时器列表中。其中的参数5000表示 5000 毫秒,MG_TIMER_REPEAT表示定时重复调用函数,MG_TIMER_RUN_NOW表示设置定时器后立即调用,timer_fn是要调用的函数,&mgr是要传递的参数。事件管理器将以参数 5000 毫秒的时间间隔调用 timer_fn 函数,并将参数 &mgr 传递给它。

  mg_timer_add(&mgr, 5000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn, &mgr);

然后是事件循环,mg_mgr_poll 遍历所有连接,接受新连接,发送和接收数据,关闭连接,并为各个事件调用事件处理函数。

  for (;;) mg_mgr_poll(&mgr, 300);  // Infinite event loop

调用 mg_mgr_free 关闭所有连接,释放所有资源。

  mg_mgr_free(&mgr);                // Free manager resources

下面我们先看下timer_fn的实现:

// Called every 5 seconds. Increase that for production case.
static void timer_fn(void *arg) {

传入的参数是在main函数中初始化的事件管理器,用于下面连接 SNTP 服务器。

  struct mg_mgr *mgr = (struct mg_mgr *) arg;

如果s_sntp_connNULL,表示目前还没连接到远程 SNTP 服务器,调用mg_sntp_connect连接 SNTP 服务器。其中第一个参数是事件管理器,第二个参数用于指定远程 URL,但这里为NULL,默认 URL 为udp://time.google.com:123。第三个参数sfn是事件处理函数。第四个参数是要传递给sfn的参数,这里没有就为NULL

  if (s_sntp_conn == NULL) s_sntp_conn = mg_sntp_connect(mgr, NULL, sfn, NULL);

如果s_sntp_conn不为NULL,表示目前已连接到远程 SNTP 服务器,调用mg_sntp_request向 SNTP 服务器发送时间请求。

  if (s_sntp_conn != NULL) mg_sntp_request(s_sntp_conn);
}

下面我们看下事件处理函数sfn的实现:

// SNTP client callback
static void sfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {

判断是否收到MG_EV_SNTP_TIME事件,如果是则表示收到 SNTP 时间。将 SNTP 时间打印出来,然后使用从服务器获取当前时间t减去我们的正常运行时间来得到我们的启动时间戳s_boot_timestamp。其中mg_millis函数以毫秒为单位返回当前正常运行时间。这里获取的时间t是 UNIX 纪元时间,也就是记录自 1970 年 1 月 1 日起已经过多少毫秒。

  if (ev == MG_EV_SNTP_TIME) {int64_t t = *(int64_t *) ev_data;MG_INFO(("Got SNTP time: %lld ms from epoch", t));s_boot_timestamp = (time_t) ((t - mg_millis()) / 1000);} 

s_boot_timestamp是一个全局变量,定义如下:

// The UNIX epoch of the boot time. Initially, we set it to 0.  But then after
// SNTP response, we update it to the correct value, which will allow us to
// use time(). Uptime in milliseconds is returned by mg_millis().
static time_t s_boot_timestamp = 0;

s_boot_timestamp用于自己实现的time函数,这个my_time函数的使用方式与time函数一致。s_boot_timestamp实际上就是保存时间戳的误差值,在获取当前时间的时候将误差值加上得到正确的时间。

// On embedded systems, rename to time()
time_t my_time(time_t *tp) {time_t t = s_boot_timestamp + mg_millis() / 1000;if (tp != NULL) *tp = t;return t;
}

回到刚才的事件处理函数sfn

判断是否收到MG_EV_CLOSE事件,表示连接关闭,将s_sntp_connNULL

  } else if (ev == MG_EV_CLOSE) {s_sntp_conn = NULL;}(void) fn_data, (void) c;
}

sntp-time-sync 的示例程序代码就都解析完了,下面实际运行一下 sntp-time-sync 程序进行测试验证。

打开示例程序,编译并运行:

pi@raspberrypi:~ $ cd Desktop/study/mongoose/examples/sntp-time-sync/
pi@raspberrypi:~/Desktop/study/mongoose/examples/sntp-time-sync$ make
cc ../../mongoose.c -I../.. -W -Wall -DMG_ENABLE_LINES=1  -o example main.c
./example
6b9aeb9 3 net.c:159:mg_connect          1 0xffffffffffffffff udp://time.google.com:123
6b9aebb 3 net.c:159:mg_connect          2 0xffffffffffffffff udp://8.8.8.8:53
6b9aebc 3 sock.c:146:mg_send            2 0x4 0:0 33 err 0
6b9aebe 1 sntp.c:62:mg_sntp_request     1 wait until resolved
6b9af07 3 sock.c:273:read_conn          2 0x4 snd 0/0 rcv 0/2048 n=97 err=0
6b9af08 3 dns.c:165:dns_cb              1 time.google.com is 216.239.35.12
6b9af0a 3 sock.c:146:mg_send            1 0x5 0:0 48 err 0

然后一直没接收到回复,发现是连不上time.google.com,估计是在国内连不上。

6c2fa42 3 sock.c:146:mg_send            1 0x5 0:0 48 err 0
6c30e32 3 sock.c:146:mg_send            1 0x5 0:0 48 err 0
6c32223 3 sock.c:146:mg_send            1 0x5 0:0 48 err 0
6c334e7 3 sock.c:146:mg_send            1 0x5 0:0 48 err 0
6c348d8 3 sock.c:146:mg_send            1 0x5 0:0 48 err 0

修改 URL ,改成国内的服务器"udp://cn.pool.ntp.org:123"。注意,分配给 NTP 的UDP 端口号为 123。

  if (s_sntp_conn == NULL) s_sntp_conn = mg_sntp_connect(mgr, "udp://cn.pool.ntp.org:123", sfn, NULL);

重新编译运行:

pi@raspberrypi:~/Desktop/study/mongoose/examples/sntp-time-sync$ make
cc ../../mongoose.c -I../.. -W -Wall -DMG_ENABLE_LINES=1  -o example main.c
./example
72b1970 3 net.c:159:mg_connect          1 0xffffffffffffffff udp://cn.pool.ntp.org:123
72b1970 3 net.c:159:mg_connect          2 0xffffffffffffffff udp://8.8.8.8:53
72b1970 3 sock.c:146:mg_send            2 0x4 0:0 33 err 0
72b1970 1 sntp.c:62:mg_sntp_request     1 wait until resolved
72b19c1 3 sock.c:273:read_conn          2 0x4 snd 0/0 rcv 0/2048 n=97 err=0
72b19c2 3 dns.c:165:dns_cb              1 cn.pool.ntp.org is 162.159.200.1
72b19c2 3 sock.c:146:mg_send            1 0x5 0:0 48 err 0
72b1a5f 3 sock.c:273:read_conn          1 0x5 snd 0/0 rcv 0/2048 n=48 err=0
72b1a60 2 sntp.c:46:sntp_cb             1 got time: 1673005400198 ms from epoch
72b1a62 2 main.c:39:sfn                 Got SNTP time: 1673005400198 ms from epoch

可以看到成功获取到了时间并且打印了出来。

还可以在程序中添加如下代码,使用my_time函数获取当前的时间,再使用localtime函数将时间转换成年月日时分秒的形式:

void convertime(void) {time_t current_time;struct tm* now_time;my_time(&current_time);now_time = localtime(&current_time);printf("%4.4d年%2.2d月%2.2d日,%2.2d:%2.2d:%2.2d\\n", now_time->tm_year+1900, now_time->tm_mon+1, now_time->tm_mday, now_time->tm_hour, now_time->tm_min, now_time->tm_sec);
}

在程序中就会将转换后的时间打印出来:

72b1a60 2 sntp.c:46:sntp_cb             1 got time: 1673005400198 ms from epoch
72b1a62 2 main.c:39:sfn                 Got SNTP time: 1673005400198 ms from epoch
2023年01月06日,19:43:19
72b2d2a 3 sock.c:146:mg_send            1 0x5 0:0 48 err 0
72b2dc4 3 sock.c:273:read_conn          1 0x5 snd 0/0 rcv 0/2048 n=48 err=0
72b2dc5 2 sntp.c:46:sntp_cb             1 got time: 1673005405165 ms from epoch
72b2dc7 2 main.c:39:sfn                 Got SNTP time: 1673005405165 ms from epoch
2023年01月06日,19:43:24

【参考资料】

examples/sntp-time-sync

Documentation

rfc2030


本文链接:https://blog.csdn.net/u012028275/article/details/130036376