NTP
什么是 NTP?这个我想大家都不陌生,NTP 是同步网络中各个计算机时间的一种协议。在 IotHub 中,保证设备和服务端的时间同步是非常重要的,比如指令的有效期设置就非常依赖于设备和服务器间的时间同步,如果设备时间不准确,就有可能导致过期的指令仍然被执行。
通常情况下,设备上都应该运行一个 NTP 的服务,定时地和 NTP 服务器进行时间同步(IotHub 服务器端也使用同样的 NTP 服务器进行时间同步),这样在绝大部分情况下,都可以保证设备和 IotHub 服务器端的时间是同步的,除非设备掉电或者断网。
IotHub 的 NTP 服务
某些嵌入式设备上,系统可能没有自带 NTP 服务,或者因为设备资源有限,无法运行 NTP 服务,这个时候 IotHub 需要基于现有的数据通道,实现一个类似于 NTP 服务器的时间同步功能,来满足上述情景下的设备与 IotHub 的时间同步。
IotHub 的 NTP 服务实现流程如下:
- 设备发起数据请求,请求 NTP 对时,请求中包含当前的设备时间 deviceSendTime。
- IotHub 收到 NTP 对时的请求下,通过下发指令的方式将收到 NTP 对时请求的时间 IotHubRecvTime、IotHub 发送指令的时间 IotHubSendTime 以及 deviceTime 发送到设备。
- 设备收到 NTP 对时指令后,记录当前时间 deviceRecvTime,然后通过公式(IotHubRecvTime + IotHubSendTime + deviceRecvTime - deviceSendTime)/ 2,来获取当前的精确时间。时间的单位都为毫秒。
这个流程没有涉及业务系统,这里的数据请求和指令下发都只存在于 IotHub 和设备之间,我们把这样的数据请求和指令都定义为 IotHub 的内部请求和指令,他们有以下的一些特点:
- 数据请求的 resource 以 $ 开头;
- 指令下发的指令名以 $ 开头;
- Payload 格式统一为 JSON 格式。
这也就意味着业务系统不能发送以
$
开头的指令;设备应用代码也不能通过 sendDataRequest 接口来发送$
开头的请求;在调用时需要对输入参数进行校验,本课程为了演示起见,跳过了输入参数校验的部分,在实际项目中是不能漏掉的。
设备端实现
在 DeviceSDK 端,首先要实现发送 NTP 对时请求的功能,NTP 对时请求用数据请求的接口实现,这里我们约定 NTP 对时请求的 Resource 为 $ntp:
//IotHub_Device/sdk/iot_device.js
sendNTPRequest() {
this.sendDataRequest("$ntp", JSON.stringify({device_time: Date.now()}))
}
然后在收到 IotHub 下发的 NTP 对时指令时进行正确计算,这里我们约定 NTP 对时的下发指令名为 $set_ntp:
//IotHub_Device/sdk/iot_device.js
handleCommand({commandName, requestID, encoding, payload, expiresAt, commandType = "cmd"}) {
if (expiresAt == null || expiresAt > Math.floor(Date.now() / 1000)) {
...
if (commandName.startsWith("$")){
if(commandName == "$set_ntp"){
this.handleNTP(payload)
}
} else {
this.emit("command", commandName, data, respondCommand)
}
}
}
在处理内部指令时,DeviceSDK 不会通过”command”事件将内部指令的信息传递给设备应用代码。可能有的读者会觉得 if (commandName.startsWith("$"))
这个判断是多余的,毕竟后面还要按照指令名去对比。这个是有必要的,如果说 IotHub 的功能升级了,增加了新的内部命令,不做这个判断的话,当新的内部命令发给还未升级的 DeviceSDK 设备时,就会把内部命令暴露给设备应用代码。
最后需要计算当前的准确时间,再传递给设备应用代码:
//IotHub_Device/sdk/iot_device.js
handleNTP(payload) {
var time = Math.floor((payload.iothub_recv + payload.iothub_send + Date.now() - payload.device_time) / 2)
this.emit("ntp_set", time)
}
设备应用代码再捕获 “ntp_set” 事件来设置系统时间。
服务端实现
服务端的实现很简单,收到 NTP 数据请求以后,将公式中需要的几个时间用指令的方式下发给设备:
static handleDataRequest({productName, deviceName, resource, payload, ts}) {
if(resource.startsWith("$")){
if(resource == "$ntp"){
this.handleNTP(payload, ts)
}
}else {
NotifyService.notifyDataRequest(...)
}
}
这里的检查if(resource.startsWith("$"))
和 DeviceSDK 中类似,当 IotHub 弃用了某个内部数据请求时,如果不检查的话,使用还未升级的 DeviceSDK 设备可能会导致这个弃用的数据请求被转发业务系统。
因为 NTP 要使用收到消息的时间,所以这里添加了 ts 参数。
接下来将 NTP 对时指令下发给设备:
//IotHub_Server/services/message_service.js
static handleNTP({payload, ts, productName, deviceName}) {
var data = {
device_time: payload.device_time,
iothub_recv: ts * 1000,
iothub_send: Date.now()
}
Device.sendCommand({
productName: productName,
deviceName: deviceName,
data: JSON.stringify(data),
commandName: "$set_ntp"
})
}
由于 EMQ X WebHook 传递过来的 ts 单位是秒,所以这里的计算会有误差,我们在后面再来解决这个问题。
代码联调
接下来我们写一段代码来验证一下:
//IotHub_Device/samples/ntp.js
...
device.on("online", function () {
console.log("device is online")
})
device.on("ntp_set", function (time) {
console.log(`going to set time ${time}`)
})
device.connect()
device.sendNTPRequest()
运行 ntp.js
, 可以看到以下输出:
device is online
going to set time 1559569382108
那么 IotHub 的 NTP 服务功能就完成了。
这一节,我们完成了 IotHub 的 NTP 服务,接下来的几节课,我们来设计和实现设备的分组功能。