C#
ntp

[C#]NTPやってみる

NTPとは

Network Time Protocol(ネットワーク・タイム・プロトコル)の略称で、ネットワークに接続される機器の持つ時計を正しい時刻に同期するための通信プロトコルのこと。
PC、スマホなどネットワークに接続される機器の時刻がずれないのは、この仕組みを利用して定期的に時刻の同期を行い修正しているため。

概要

NTPサーバは、世界中に存在し階層構造(最大16階層)で構成され負荷分散を行う仕組みになっている。
最上位にGPSや標準電波、原子時計などの正確な時刻源を配置し、下位のサーバーは通常複数の上位/同位サーバーを利用して時刻を取得している。
上位サーバーの方がより正確な時刻保持しているが、サーバとのネットワーク的な近さのほうが時刻精度に大きく影響する。
また、高負荷によりサーバの応答性を下げ、配信される時刻の正確性が失われる場合があり、一般的にWindowsOSやMacOSなどで初期設定されているNTPサーバーは混雑しているため、他のNTPサーバーに変更することでより正確な時刻取得が可能になる。
また、組織内(LAN)のクライアント台数が存在する場合、内部NTPサーバーを利用し同期を行った方が、外部サーバーとの同期と比べ各機器同士の時刻誤差が発生しにくくなる。

プロトコルの説明

NTPは、OSI基本参照モデルの第7層(アプリケーション層)に位置し、UDPポート123番を使用し、下図のパケットで送受信を行っている。
NTP_Packet.png

LI(Leap Indicator):閏秒指示子

LI 意味
00 警告無し
01 最後の1分が61秒
10 最後の1分が59秒
11 警告

VN(VersionNumber):バージョン番号

VN 意味
000 -
001 バージョン1
010 バージョン2
011 バージョン3(NTPサーバ)
100 バージョン4(SNTPサーバ)

Mode:動作モード

Mode 意味
000 予約
001 対称アクティブモード
010 対称パッシブモード
011 クライアント
100 サーバ
101 ブロードキャスト
110 NTP制御メッセージのため予約
111 私的使用のため予約

Stratum:階層

Stratum 意味
00000000 不明または有効ではない
00000001 1次参照(セシウム原子時計、GPS時計を参照している)
00000010 ~ 00001111 2~15次参照(NTP、SNTPを経由して参照している)
00010000 ~ 11111111 予約

Pool:ポーリング間隔

8ビットの符号付き整数で、連続するメッセージの最大間隔を、秒単位で2のべき乗で表される。

Pool 意味
00000000 0秒間隔
00000001 1秒間隔
00000010 2秒間隔
00000011 4秒間隔
00000100 8秒間隔
00000101 16秒間隔
00000110 32秒間隔
00000111 64秒間隔(1分4秒)
00001000 128秒間隔(2分8秒)
00001001 256秒間隔(4分16秒)
00001010 512秒間隔(8分32秒)
00001011 1024秒間隔(17分4秒)
00001100 2048秒間隔(34分8秒)

Precision:精度

8ビットの符号付き整数でローカル時計の精度を、秒単位で2のべき乗で表される。

ルート遅延(Root Delay)

32ビット符号付き固定小数点で、1次参照源までの往復遅延の合計を表す。

ルート分散(Root Dispersion)

32ビット符号付き固定小数点で、1次参照源までの相対的な誤差を表す。

参照識別子(Reference Identifier)

32ビットのビット列で階層0,1のサーバでは4文字のアスキーで左寄せされている。残りは0で埋められている。
それ以外のサーバでは時刻源のIPv4のIPアドレスがセットされる。

参照タイムスタンプ(Reference Timestamp)

ローカル時計が最後に設定、修正された時刻を表す。64ビットのタイムスタンプフォーマットで表される。

開始タイムスタンプ(Originate Timestamp)

クライアントからサーバへリクエストを発信した時間を表す。64ビットのタイムスタンプフォーマットで表される。

受信タイムスタンプ(Receive Timestamp)

サーバへリクエストが到着した時間を表す。64ビットのタイムスタンプフォーマットで表される。

送信タイムスタンプ(Transmit Timestamp)

サーバからクライアントに応答が発信された時間を表す。64ビットのタイムスタンプフォーマットで表される。

鍵識別子(Key Identifier)(任意)

メッセージダイジェスト(Message Digest)(任意)

関連RFC

RFC 1128
RFC 1129
RFC 1305
RFC 2030
RFC 4330
RFC 5905
RFC 5906
RFC 5907
RFC 5908
RFC 868 - Time Protocol

C#による実装

NtpPacket.cs
    using System;
    using System.Net;
    using System.Text;


    public class NtpPacket
    {

        const long COMPENSATING_RATE_32 = 0x100000000L;
        const double COMPENSATING_RATE_16 = 0x10000d;

        static readonly DateTime COMPENSATING_DATETIME = new DateTime(1900, 1, 1);
        static readonly DateTime PASSED_COMPENSATING_DATETIME = COMPENSATING_DATETIME.AddSeconds(uint.MaxValue);

        const int CLIENT_VERSION = 3;

        public byte[] PacketData { get; private set; }

        public DateTime NtpPacketCreatedTime { get; private set; }

        public TimeSpan DifferentTimeSpan
        {
            get
            {
                long offsetTick = ((ReceiveTimestamp - OriginateTimestamp) + (TransmitTimestamp - NtpPacketCreatedTime)).Ticks / 2;
                return new TimeSpan(offsetTick);
            }
        }

        public TimeSpan NetworkDelay
        {
            get { return ((NtpPacketCreatedTime - OriginateTimestamp) + (TransmitTimestamp - ReceiveTimestamp)); }
        }

        public NtpPacket(byte[] packetData)
        {
            PacketData = packetData;
            NtpPacketCreatedTime = DateTime.Now;
        }

        static DateTime GetCompensatingDatetime(uint seconds)
        {
            return (seconds & 0x80000000) == 0 ? PASSED_COMPENSATING_DATETIME : COMPENSATING_DATETIME;
        }

        static DateTime GetCompensatingDatetime(DateTime dateTime)
        {
            return PASSED_COMPENSATING_DATETIME <= dateTime ? PASSED_COMPENSATING_DATETIME : COMPENSATING_DATETIME;
        }

        static public NtpPacket CreateSendPacket()
        {
            byte[] packet = new byte[48];
            FillHeader(packet);
            FillTransmitTimestamp(packet);
            return new NtpPacket(packet);
        }

        static private void FillHeader(byte[] ntpPacket)
        {
            const byte li = 0x00;
            const byte mode = 0x03;

            ntpPacket[0] = (byte)(li | CLIENT_VERSION << 3 | mode);
        }

        static private void FillTransmitTimestamp(byte[] ntpPacket)
        {
            byte[] time = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(DateTimeToNtpTimeStamp(DateTime.UtcNow)));
            Array.Copy(time, 0, ntpPacket, 40, 8);
        }

        static private double SignedFixedPointToDouble(int signedFixedPoint)
        {
            short number = (short)(signedFixedPoint >> 16);
            ushort fraction = (ushort)(signedFixedPoint & short.MaxValue);

            return number + (double)fraction / COMPENSATING_RATE_16;
        }

        static private DateTime NtpTimeStampToDateTime(long ntpTimeStamp)
        {
            uint seconds = (uint)(ntpTimeStamp >> 32);
            uint secondsFraction = (uint)(ntpTimeStamp & uint.MaxValue);

            long milliseconds = (long)seconds * 1000 + (secondsFraction * 1000) / COMPENSATING_RATE_32;
            return GetCompensatingDatetime(seconds) + TimeSpan.FromMilliseconds(milliseconds);
        }

        static private long DateTimeToNtpTimeStamp(DateTime dateTime)
        {
            DateTime compensatingDatetime = GetCompensatingDatetime(dateTime);
            double ntpStandardTick = (dateTime - compensatingDatetime).TotalMilliseconds;

            uint seconds = (uint)((dateTime - compensatingDatetime).TotalSeconds);
            uint secondsFraction = (uint)((ntpStandardTick % 1000) * COMPENSATING_RATE_32 / 1000);

            return (long)(((ulong)seconds << 32) | secondsFraction);
        }

        public int LeapIndicator
        {
            get { return PacketData[0] >> 6 & 0x03; }
        }

        public int Version
        {
            get { return PacketData[0] >> 3 & 0x03; }
        }

        public int Mode
        {
            get { return PacketData[0] & 0x03; }
        }

        public int Stratum
        {
            get { return PacketData[1]; }
        }

        public int PollInterval
        {
            get
            {
                int interval = (SByte)PacketData[2];
                switch (interval)
                {
                    case (0): return 0;
                    case (1): return 1;
                    default: return (int)Math.Pow(2, interval);
                }
            }
        }

        public double Precision
        {
            get { return Math.Pow(2, (SByte)PacketData[3]); }
        }

        public double RootDelay
        {
            get { return SignedFixedPointToDouble(IPAddress.NetworkToHostOrder(BitConverter.ToInt32(PacketData, 4))); }
        }

        public double RootDispersion
        {
            get { return SignedFixedPointToDouble(IPAddress.NetworkToHostOrder(BitConverter.ToInt32(PacketData, 8))); }
        }

        public string ReferenceIdentifier
        {
            get
            {
                if (Stratum <= 1)
                {
                    return Encoding.ASCII.GetString(PacketData, 12, 4).TrimEnd(new char());
                }
                else
                {
                    IPAddress ip = new IPAddress(new byte[] { PacketData[12], PacketData[13], PacketData[14], PacketData[15] });
                    return ip.ToString();
                }
            }
        }

        public DateTime ReferenceTimestamp
        {
            get { return NtpTimeStampToDateTime(IPAddress.NetworkToHostOrder(BitConverter.ToInt64(PacketData, 16))).ToLocalTime(); }
        }

        public DateTime OriginateTimestamp
        {
            get { return NtpTimeStampToDateTime(IPAddress.NetworkToHostOrder(BitConverter.ToInt64(PacketData, 24))).ToLocalTime(); }
        }

        public DateTime ReceiveTimestamp
        {
            get { return NtpTimeStampToDateTime(IPAddress.NetworkToHostOrder(BitConverter.ToInt64(PacketData, 32))).ToLocalTime(); }
        }

        public DateTime TransmitTimestamp
        {
            get { return NtpTimeStampToDateTime(IPAddress.NetworkToHostOrder(BitConverter.ToInt64(PacketData, 40))).ToLocalTime(); }
        }

        public string KeyIdentifier
        {
            get {
                if (PacketData.Length <= 48) { return string.Empty; }
                return Encoding.ASCII.GetString(PacketData, 48, 4).TrimEnd(new char()); 
            }
        }

        public string MessageDigest
        {
            get
            {
                if (PacketData.Length <= 52) { return string.Empty; }
                return Encoding.ASCII.GetString(PacketData, 52, 16).TrimEnd(new char()) ?? string.Empty;
            }
        }
    }
NtpClient.cs
    using System.Net;
    using System.Net.Sockets;

    public class NtpClient
    {

        private UdpClient _connection = new UdpClient();

        public IPAddress IP { get { return _receiveEndPoint.Address; } }
        private IPEndPoint _receiveEndPoint = new IPEndPoint(System.Net.IPAddress.Any, 0);

        public string Host { get; private set; }

        public int Port { get; private set; }

        public int SendTimeout { get { return _connection.Client.SendTimeout; } set { _connection.Client.SendTimeout = value; } }

        public int ReceiveTimeout { get { return _connection.Client.ReceiveTimeout; } set { _connection.Client.ReceiveTimeout = value; } }

        public void Connect(string host)
        {
            Connect(host, 123);
        }

        public void Connect(string host, int port)
        {
            this.Host = host;
            this.Port = port;
            _connection.Connect(this.Host, this.Port);
        }

        public NtpPacket GetData()
        {
            Send();
            return Receive();
        }

        private void Send()
        {
            NtpPacket packet = NtpPacket.CreateSendPacket();
            _connection.Send(packet.PacketData, packet.PacketData.GetLength(0));
        }

        private NtpPacket Receive()
        {
            byte[] receiveData = _connection.Receive(ref _receiveEndPoint);
            return new NtpPacket(receiveData);
        }
    }