## 心跳机制
* 边缘主机通过心跳机制来判断主机是否存活(解决弱网环境下websocket链接会经常断开的问题)
* 边缘主机定期发送 UDP 心跳包，服务端按照 `iot_host` 的心跳检测周期判断是否有收到心跳包。
* 如果 UDP 心跳丢失但 SSL channel 仍然存在，服务端不会把 host 标记为离线。
* 如果 UDP 心跳丢失且 SSL channel 也不存在，服务端会把 host 标记为离线。

### udp服务器
* 端口: 18080

### 心跳包格式
旧格式 `<<Len:16, HostUUID/binary>>` 已不再接受。

当前心跳包格式：

```erlang
<<
    Version:8,
    UuidLen:16,
    UUID:UuidLen/binary,
    Timestamp:64/unsigned-big,
    Nonce:16/binary,
    Mac:32/binary
>>
```

字段说明：

| 字段 | 说明 |
| --- | --- |
| `Version` | 当前固定为 `1`。 |
| `UuidLen` | `UUID` 的字节数。 |
| `UUID` | efka 对应的 host UUID。 |
| `Timestamp` | 秒级 Unix 时间戳。服务端只接受 120 秒时间窗内的心跳。 |
| `Nonce` | 16 字节随机数，必须加入 HMAC payload，使同一秒内多次心跳的 Mac 不同。当前服务端不保存 nonce。 |
| `Mac` | HMAC-SHA256 结果，32 字节。 |

`Mac` 的计算内容为前面所有字段：

```erlang
Payload = <<Version:8, UuidLen:16, UUID:UuidLen/binary, Timestamp:64/unsigned-big, Nonce:16/binary>>,
Mac = crypto:mac(hmac, sha256, HeartbeatSecret, Payload)
```

服务端在 `efka_client_store:verify_heartbeat/4` 中校验时间窗和 HMAC。`HeartbeatSecret` 使用 `SHA256(Token)`，其中 `Token` 和 TLS 鉴权使用同一个 auth token。`iot` 侧只保存派生后的 `heartbeat_secret`，不保存明文 token。
