【プログラム】はじめてNTPから時間同期した

はじめに

訳あってNTPサーバから時間を取得して、端末と同期する事をしました。
もう古代からある技術だと思うので、今更やるのかと感じもするのですが、
知らないことを知る良い機会でした。

NTPクライアント

NTPサーバへ通信して、1900/01/01 00:00:00 からの経過時間を取得する処理を調べました。
最初は「httpで取得して、レスポンスから時間を拾って、おしまい」なんて思っていたら、だいぶ違っていて躓きました。。。

NTPサーバと通信するには、UDPを使うのが一般的なんですね。
C#にはUdpClientクラスがあるので、それを使いました。
UdpClientもリファレンスを斜め読みしていたら、
Send/Recieveはスレッドをブロックするとな。。。

非同期用のasync/awaitや、終了時にコールバックさせるメソッドもありましたが、使ってみたところ面倒くさいので、スレッドプール内で処理するようにしました。

さて、NTPサーバへ送るパケット、受け取るパケットのフォーマットですが、調べてみるとどの先駆者たちも48バイトのデータを送受信していました。
NTPの公式な仕様やドキュメントを調べてみたのですけど、
RFCという資料しか見つけられず、NTPのバージョン別の正式な資料なのかよくわからず。。。素人には判断がつかずで、また躓いていました。。。

見様見真似で先駆者たちのサンプルを頼りにだいぶ理解がすすみました。
送信パケットに設定するデータには、NTPのバージョン、モード、うるう秒の警告、パケットを送信する端末の時間を設定する必要があるようです。
受信パケットからは3つの時間が64ビットで受け取れるので、パケットを受信した端末の時間とその3つの時間を使って、端末がNTPサーバとどれくらいずれているのかを判定するようです。
なるほどね~。。。知らないことを知るのは良い刺激ですね。

なのですが、また問題にぶつかるのです。。。

2036年問題

もう2023年です。
あと10年ちょっとでその問題の年です。
コンピュータ界隈でたびたび問題にあがるオーバーフローの問題です。
NTPだって解決されてるんでしょ、と高を括っていました。
RFCにもNTPDateFormatとかいう128ビットのフォーマットがあるようなので、それを受信できれば問題なしでしょ、と。

しかし、どうしても、そのNTPDateFormatを受信できる方法がわからない。。。みんな使ってないのかな。。。?
本当に困ってしまったので、ChatGPTにも頼ってしまいました涙。
ChatGPTも128ビットの時間を返すには、カスタムしたNTPサーバを用意する、など言われました。そうですか!

NTPクライアントのOSSなども調べてみましたが、
48バイトのパケットに含まれる64ビットの時間を使って、
クライアント側で現在時間が特定の136年間に含まれている前提で同期を取っているのですね。

こちらの期限の都合もあるので、128ビットのNTPDateFormatなるものは諦めました。

まとめ

2036年問題は無視+小数点以下の処理が雑ですが、実装例です。

public class NtpClient
{
    public DateTime Now()
    {
        var endpoint = new IPEndPoint(IPAddress.Any, 0);
        using (var udp = new System.Net.Sockets.UdpClient(endpoint))
        {
            var ntpEpoch = new System.DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            //  送信
            {
                var request = new byte[48];
                
                var li = 0; // うるう秒の警告の扱い
                var versinoNumber = 4; // NTPのバージョン、3でも動くっぽい
                var mode = 3; // クライアントを示す3にする
                request[0] = (byte)((li << 6) | (versinoNumber << 3) | (mode)); //  この設定が必須
                // 現在時間をいれる(受信時のoriginTimestampに相当する)
                System.Buffers.Binary.BinaryPrimitives.WriteInt64BigEndian(
                    request.AsSpan(40..),
                    ((long)(System.DateTime.UtcNow - ntpEpoch).TotalSeconds) << 32
                );

                udp.Send(request, request.Length, "time.google.com", 123);
            }

            //  受信
            {
                var response = udp.Receive(ref endpoint);
                var originTimestamp = System.Buffers.Binary.BinaryPrimitives.ReadUInt64BigEndian(
                    response.AsSpan(24..)
                );
                var originDateTime = ntpEpoch.AddSeconds(originTimestamp / (1L << 32));

                var receiveTimestamp = System.Buffers.Binary.BinaryPrimitives.ReadUInt64BigEndian(
                    response.AsSpan(32..)
                );
                var receiveDateTime = ntpEpoch.AddSeconds(receiveTimestamp / (1L << 32));

                var transmitTimestamp = System.Buffers.Binary.BinaryPrimitives.ReadUInt64BigEndian(
                    response.AsSpan(40..)
                );
                var transmitDateTime = ntpEpoch.AddSeconds(transmitTimestamp / (1L << 32));

                var destinationDateTime = System.DateTime.UtcNow;

                var offset = ((receiveDateTime - originDateTime) - (destinationDateTime - transmitDateTime)).TotalSeconds * 0.5;

                return destinationDateTime.AddSeconds(offset);
            }
        }
    }
}

あとは、別スレッドに逃がすなり、無駄を省くなどして
ひとまず、事なきを得そうなので安心です。



NTPでnoteを検索してみたら、
INTPだとかENTPとかでてきて、
知らないプロトコルだと思いましたわ。。。

ちなみに私は、ISFJ-Aってやつでした。
よくわからん。。。


この記事が気に入ったらサポートをしてみませんか?