概要
NTP(Network Time Protocol)は、ネットワークに接続されたコンピュータやデバイスの時刻をUTC(協定世界時)に同期するためのプロトコルです。1985年にDavid L. Millsが設計し、現在はRFC 5905(NTPv4)として標準化されています。
IoT・組み込み機器にとって正確な時刻は以下の理由から不可欠です。
- タイムスタンプの一貫性:異なるデバイスから送られたセンサーデータを正しい時系列で結合する
- 証明書の有効期限検証:TLS/HTTPSによる安全通信には現在時刻が必要
- ログの正確性:障害解析にはミリ秒単位の正確なログ時刻が必要
- スケジューリング:定時実行タスク(毎日0時にデータ集計など)
- JWTトークンの有効期限:認証トークンの期限切れ検知
マイコン(ESP32、STM32等)は電源断すると内蔵RTC(リアルタイムクロック)の時刻がリセットされる製品が多く、起動時にNTPで時刻を取得するのが標準的なパターンです。
歴史・背景
NTPの歴史はインターネットの黎明期にさかのぼります。1985年、David L. Millsによって最初のバージョン(NTPv0)が実装され、実験的なインターネット上で使われ始めました。
| バージョン | RFC | 年 | 主な変更 |
|---|---|---|---|
| NTPv1 | RFC 1059 | 1988 | 最初の公式RFC |
| NTPv2 | RFC 1119 | 1989 | 認証機能追加 |
| NTPv3 | RFC 1305 | 1992 | 分散タイムサーバー対応 |
| NTPv4 | RFC 5905 | 2010 | IPv6対応、アルゴリズム改善 |
| NTS | RFC 8915 | 2020 | TLSによる認証付き時刻同期 |
IoT分野での重要性が高まった背景として、2016年のMirai等のIoTマルウェア感染事例で「証明書期限切れで通信できない → 時刻が合っていない」という問題が多発し、適切な時刻同期の重要性が再認識されました。
SNTP(Simple NTP)はNTPv4の簡易サブセットで、精度要件が緩い組み込み機器に向いています。ESP-IDFやLwIPが実装しているのは実質的にSNTPです。
技術仕様
階層構造(Stratum)
NTPサーバーはStratumという階層で管理されます。
Stratum 0: 基準信号源(原子時計、GNSS、GPS)
↓
Stratum 1: 基準時刻サーバー(Stratum 0に直接接続)
↓
Stratum 2: Stratum 1と同期するサーバー
↓
Stratum 3: Stratum 2と同期するサーバー
↓ ...
Stratum 15: 最下層(Stratum 16は未同期/無効)
一般的なIoTデバイスはStratum 2〜3のサーバー(pool.ntp.org等)と同期します。
パケットフォーマット
NTPパケットは48バイト固定長(基本部分)です。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN |Mode | Stratum | Poll | Precision |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Delay |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Dispersion |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reference ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Reference Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Originate Timestamp (64) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Receive Timestamp (64) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transmit Timestamp (64) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| フィールド | 説明 |
|---|---|
| LI | うるう秒警告(Leap Indicator) |
| VN | バージョン番号(現在4) |
| Mode | 3=クライアント、4=サーバー |
| Stratum | 階層レベル |
| Poll | ポーリング間隔(2^n秒) |
| Precision | クロック精度(2^n秒) |
| Root Delay | 基準時刻源までの往復遅延 |
| Transmit Timestamp | 送信時刻(64ビット、1/2^32秒精度) |
NTPタイムスタンプ形式
NTPタイムスタンプは1900年1月1日00:00:00 UTCを起点とする64ビット固定小数点数です。
上位32ビット: 秒(整数部)
下位32ビット: 秒未満(小数部、1/2^32秒 ≈ 0.23ナノ秒精度)
Unixタイムへの変換:
NTP epoch (1900-01-01) から Unix epoch (1970-01-01) は 70年 = 2208988800秒
unix_time = ntp_seconds - 2208988800
import struct
import socket
def get_ntp_time(server="pool.ntp.org"):
"""NTPサーバーから時刻を取得"""
NTP_PORT = 123
REFERENCE_TIME_1900 = 2208988800 # NTP epochからUnix epochへのオフセット
# NTPリクエストパケット(48バイト)
packet = b'\x1b' + b'\x00' * 47
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
sock.sendto(packet, (server, NTP_PORT))
data, _ = sock.recvfrom(1024)
sock.close()
if data:
# Transmit Timestamp(40バイト目から8バイト)
ntp_time = struct.unpack('!I', data[40:44])[0]
unix_time = ntp_time - REFERENCE_TIME_1900
return unix_time
return None
import datetime
t = get_ntp_time()
print(datetime.datetime.utcfromtimestamp(t))
時刻オフセット計算
NTPクライアントはネットワーク遅延を考慮して時刻オフセットを計算します。
T1: クライアントがリクエストを送信した時刻
T2: サーバーがリクエストを受信した時刻
T3: サーバーがレスポンスを送信した時刻
T4: クライアントがレスポンスを受信した時刻
往復遅延(RTT): δ = (T4 - T1) - (T3 - T2)
時刻オフセット: θ = ((T2 - T1) + (T3 - T4)) / 2
クライアントの正確な時刻 = クライアント現在時刻 + θ
動作原理
ESP32でのSNTP実装
#include "esp_sntp.h"
#include "esp_log.h"
#include <time.h>
static const char *TAG = "sntp";
// 時刻同期完了コールバック
void time_sync_notification_cb(struct timeval *tv) {
ESP_LOGI(TAG, "時刻同期完了: %s", ctime(&tv->tv_sec));
}
void sntp_init_and_sync(void) {
ESP_LOGI(TAG, "SNTPクライアント初期化");
// NTPサーバー設定(日本向けにJP NTPプールを使用)
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "ntp.nict.jp"); // 日本標準時研究機構
sntp_setservername(1, "0.jp.pool.ntp.org"); // 日本プール
sntp_setservername(2, "pool.ntp.org"); // グローバルフォールバック
sntp_set_time_sync_notification_cb(time_sync_notification_cb);
// 滑らか補正(slewモード)- 急な時刻ジャンプを避ける
sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH);
sntp_init();
// 同期完了を待つ(最大10秒)
int retry = 0;
const int max_retries = 20;
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET &&
++retry < max_retries) {
ESP_LOGI(TAG, "時刻同期待ち... (%d/%d)", retry, max_retries);
vTaskDelay(pdMS_TO_TICKS(500));
}
if (retry == max_retries) {
ESP_LOGW(TAG, "NTP同期タイムアウト");
return;
}
// タイムゾーン設定(日本標準時 UTC+9)
setenv("TZ", "JST-9", 1);
tzset();
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
char strtime[64];
strftime(strtime, sizeof(strtime), "%Y-%m-%d %H:%M:%S", &timeinfo);
ESP_LOGI(TAG, "現在時刻(JST): %s", strtime);
}
Raspberry PiでのNTP(systemd-timesyncd)
# systemd-timesyncd の設定確認
timedatectl status
# NTPサーバーをJP NTPプールに設定
cat > /etc/systemd/timesyncd.conf << EOF
[Time]
NTP=ntp.nict.jp 0.jp.pool.ntp.org 1.jp.pool.ntp.org
FallbackNTP=pool.ntp.org
EOF
systemctl restart systemd-timesyncd
# 同期状態を確認
timedatectl show-timesync --all
# 詳細ログ
journalctl -u systemd-timesyncd -f
# ntpd(従来型)の設定
cat >> /etc/ntp.conf << EOF
server ntp.nict.jp iburst
server 0.jp.pool.ntp.org iburst
server 1.jp.pool.ntp.org iburst
EOF
systemctl restart ntp
# 同期状態確認
ntpq -pn
NTSによるセキュア時刻同期
NTS(Network Time Security、RFC 8915)はTLSによりNTP通信を保護します。
# chrony(NTSサポート)の設定
cat > /etc/chrony.conf << EOF
# NTSサーバー(Cloudflare)
server time.cloudflare.com iburst nts
# 証明書検証
ntsdumpdir /var/lib/chrony/nts-cookies
# 許容誤差
makestep 1.0 3
rtcsync
EOF
systemctl restart chronyd
# NTS接続確認
chronyc sources -v
# * time.cloudflare.com ... nts
用途・ユースケース
センサーデータのタイムスタンプ付与
// NTP同期済みの時刻でセンサーデータを記録
#include <time.h>
#include "cJSON.h"
char* create_timestamped_data(float temperature, float humidity) {
time_t now;
struct tm *tm_info;
char iso8601[30];
time(&now);
tm_info = gmtime(&now);
// ISO 8601形式(UTC)
strftime(iso8601, sizeof(iso8601),
"%Y-%m-%dT%H:%M:%SZ", tm_info);
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "timestamp", iso8601);
cJSON_AddNumberToObject(root, "temperature", temperature);
cJSON_AddNumberToObject(root, "humidity", humidity);
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return json;
}
TLS証明書検証への活用
HTTPSで通信する前に時刻が正確であることを確認します。証明書の有効期限チェックに現在時刻が使われるため、時刻が大幅にずれているとすべてのHTTPS通信が失敗します。
// TLS接続前の時刻チェック
bool is_time_synchronized(void) {
time_t now;
time(&now);
// 2020年以降なら同期済みとみなす(未同期だと1970年になる)
struct tm *tm_info = gmtime(&now);
return (tm_info->tm_year + 1900) >= 2020;
}
void connect_to_api(void) {
if (!is_time_synchronized()) {
ESP_LOGE(TAG, "時刻未同期:TLS接続を中止");
sync_time_from_ntp();
return;
}
// HTTPS接続を実行
http_connect_to_api();
}
定時タスクのスケジューリング
# Raspberry Pi: 毎日0時にデータ集計
import schedule
import time
from datetime import datetime
def daily_aggregation():
print(f"日次集計開始: {datetime.now().isoformat()}")
# データ集計処理
aggregate_daily_data()
print(f"日次集計完了: {datetime.now().isoformat()}")
# スケジュール登録(ntpで時刻が正確であることが前提)
schedule.every().day.at("00:00").do(daily_aggregation)
schedule.every(5).minutes.do(send_heartbeat)
while True:
schedule.run_pending()
time.sleep(1)
LoRaWAN との時刻同期
LoRaWANのClass Bモードでは、ゲートウェイのビーコン信号とデバイスの時刻同期が必要です。ゲートウェイはGPS/NTPで時刻を同期し、ビーコンに含めます。
GPS(Stratum 0)
↓
LoRaWANゲートウェイ(NTP/GPS時刻同期済み)
↓ ビーコン信号(128秒周期)
LoRaWANデバイス(Class B)
ビーコン時刻に合わせてRTCを校正
→ 決まった時間スロットで受信待ち
実装・開発のポイント
NTPサーバーの選択
| サーバー | 対象 | 精度 | 備考 |
|---|---|---|---|
| ntp.nict.jp | 日本国内 | <1ms | 国立研究開発法人情報通信研究機構 |
| pool.ntp.org | グローバル | 数ms | 自動的に近いサーバーを選択 |
| time.cloudflare.com | グローバル | 数ms | NTS対応 |
| time.google.com | グローバル | <1ms | Googleのタイムサーバー |
| 169.254.169.123 | AWS EC2 | <1ms | AWSインスタンスローカル |
ポーリング間隔の設定
// ESP-IDFでのポーリング間隔設定(sntp_component.h)
// デフォルトは15分(900秒)
// 最小値は15秒
// menuconfig 設定(sdkconfig.defaults)
// CONFIG_LWIP_SNTP_UPDATE_DELAY=15000 // 15秒(最小値)
// CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 // 1時間(推奨)
時刻同期失敗時の対処
// RTCに時刻をバックアップして、NTP失敗時に使う(ESP32の例)
#include "driver/rtc_io.h"
// RTC メモリに最後の同期時刻を保存(ディープスリープをまたいで保持)
RTC_DATA_ATTR static time_t last_synced_time = 0;
RTC_DATA_ATTR static uint32_t last_synced_uptime = 0;
void backup_time_to_rtc(void) {
time(&last_synced_time);
last_synced_uptime = esp_log_timestamp() / 1000;
}
void restore_time_from_rtc(void) {
if (last_synced_time > 0) {
// ディープスリープ中の経過時間を加算
uint32_t sleep_duration_sec =
(esp_log_timestamp() / 1000) - last_synced_uptime;
time_t estimated_time = last_synced_time + sleep_duration_sec;
struct timeval tv = {.tv_sec = estimated_time};
settimeofday(&tv, NULL);
ESP_LOGI(TAG, "RTC時刻を復元: %s", ctime(&estimated_time));
}
}
うるう秒の考慮
NTPはうるう秒を「LIフラグ」で予告します。サーバーがうるう秒調整を行う前日の23:59から翌日0:01のセンサーデータのタイムスタンプには注意が必要です。
# うるう秒を持たないUNIX時間を使う場合の注意
# MQTTやREST APIでは ISO 8601 (UTC) 文字列が推奨
# "2015-06-30T23:59:59Z" → 最後のうるう秒の例
from datetime import datetime, timezone
# UTC時刻で記録(うるう秒は記録されない点に注意)
ts = datetime.now(timezone.utc).isoformat()
他技術との比較
| 項目 | NTP | GNSS/GPS時刻 | PTP(IEEE 1588) | RTC(内蔵) |
|---|---|---|---|---|
| 精度 | ミリ秒〜マイクロ秒 | ナノ秒 | ナノ秒〜マイクロ秒 | 秒〜分/月 |
| ネットワーク要件 | インターネット接続 | GPSアンテナ | LAN内専用 | 不要 |
| コスト | 無料 | GPSモジュール代 | 専用HW | 内蔵 |
| IoT適合 | ◎ | ○(屋外・農業) | ◎(産業) | △(長期ずれ) |
| セキュリティ | △(NTSで改善) | ○ | ○ | — |
GPS/GNSSと組み合わせることで、インターネット接続がないオフライン環境でも高精度な時刻同期が可能です。産業IoT分野ではPTP(IEEE 1588)がマイクロ秒精度を実現し、モーション制御など同期精度が重要な用途に使われます。ESP32やRaspberry PiではNTPが時刻同期の標準手段であり、Wi-Fi接続後の初期化シーケンスに組み込むことが推奨されます。