2015年5月18日月曜日

C#でNTPを使ってみる

C#のUDPでNTPにアクセスしてみました
今回使ったサーバーは ntp.nict.jp です(つまりStratum2以上のサーバーでの動作確認はしていません)

実行するとこんな感じになります

nictサーバーでStratumは1 クロックソース識別子は"NICT"です

5個の時間は上から
・サーバーがクロックソースと最後に同期した時間(サーバー時間)
・クライアントがリクエストを送信した時間(クライアント時間)
・サーバーがリクエストを受信した時間(サーバー時間)
・サーバーがレスポンスを送信した時間(サーバー時間)
・クライアントがレスポンスを受信した時間(クライアント時間)
となります
NTPでは2種類の時間系が存在することに注意してください

最初の1つを除いた4個の時間がそれぞれ ts, Tr, Ts, tr となり、サーバーから見たクライアントの遅延は ((Ts + Tr) / 2) + ((ts + tr) / 2) となります
クライアントの時間が進んでいる場合は負の値になります

NTP時間は整数32bit 小数点以下32bitの固定小数点形式です
C#で時間を計算する場合は、Math.Pow(2, -32)をNTPtime(UInt64)に掛けた積が秒となり、1900年1月1日0時0分0秒UTC±0からの経過時間です

C#ではシステム時間を変更することができるので、クライアント時間の遅延量がわかれば適切に修正できますが、気軽にシステム時間を変更するべきではありません
一気に時間を飛ばすと不都合が発生する可能性があるからです(もっとも、Windowsの標準機能でも一気に時間を飛ばしていますが。。。)

***

NTPと通信する場合は、リクエストを送信する直前に送信時間を記録し、レスポンスを受信した直後に受信時間を記録します
送信時間と受信時間の処理はNTPの精度に大きく影響しますが、その前後は精度にはさほど影響しません(もちろんレスポンスを受信してから何日もほっといてしまえば精度は劣化しますが、常識的な処理速度であれば問題ないでしょう)

今回のプログラムは送信前に時間を記録しておき、レスポンスが一定時間なければタイムアウトの例外を投げます
タイムアウトは初期値で2秒で、その間はブロッキングです
一応200ミリ秒程度でレスポンスが帰ってきますが、回線状態によってはもう少し必要な場合もあります

NTPのエンディアンはビッグエンディアンですから、C#のBitConverterのような簡単な処理方法は使えません そのためいちいちビットシフト等で計算しています

***

なぜシステム時間を変更するわけでもないのにNTPの時間を使うかというと、クライアントの遅延時間がわかれば、C#のAddSecondsなどで簡単に補正することが可能だからです

例えば人工衛星の位置を計算したい場合、相手は秒速8km前後で移動しているわけで、システム時間が30秒もズレていたら相手はおよそ250kmも移動してしまいます ISSなどは高度400km程度ですから、250kmというのは無視できません(35度くらい移動してしまいます)

ただNTPは常に使えるわけではなく、最低限何らかのインターネット接続できる環境が必要なので、できればPCの時間は常に構成しておくほうが望ましいです(Windowsって定期的にNTPと同期するはずなのにかなりズレてますよね。。。)

WindowsでNTPと同期した直後に以下のプログラムを走らせても数十ミリ秒位のズレがあるので、あんまり高精度は期待しない方がいいです

それと、最近のNTPでは「64bitNTPtimeの最上位bitが0の場合は2036年以降として処理する」みたいな約束がありますが、以下のプログラムでは未対応なので、あと20年くらいで使えなくなります
ま、20年後にこのコードを使う人もいないと思いますが、注意してください

using System;
using System.Text;

namespace NTPtest {
    class Program {
        static void Main(string[] args) {
            NTPpacket ntpc = new NTPpacket();

            try {
                ntpc.GetTime();
            } catch (TimeoutException) {
                Console.WriteLine("タイムアウトしました");
                Console.ReadLine();
                return;
            }

            DateTimeOffset ReferenceTimestamp = NTPpacket.Offset.AddSeconds(ntpc.ReferenceTimestamp * Math.Pow(2, -32));
            DateTimeOffset OriginateTimestamp = NTPpacket.Offset.AddSeconds(ntpc.OriginateTimestamp * Math.Pow(2, -32));
            DateTimeOffset ReceiveTimestamp = NTPpacket.Offset.AddSeconds(ntpc.ReceiveTimestamp * Math.Pow(2, -32));
            DateTimeOffset TransmitTimestamp = NTPpacket.Offset.AddSeconds(ntpc.TransmitTimestamp * Math.Pow(2, -32));
            DateTimeOffset ClientReceiveTimestamp = NTPpacket.Offset.AddSeconds(ntpc.ClientReceiveTimestamp * Math.Pow(2, -32));

            Console.WriteLine();
            Console.WriteLine("Stratum                 " + ntpc.Stratum);
            if (ntpc.Stratum == 1) {
                Console.WriteLine("ReferenceIdentifierCode " + ntpc.ReferenceIdentifierCode);
            }
            Console.WriteLine("ReferenceTimestamp      " + ReferenceTimestamp.UtcDateTime.ToString("yyyy/MM/dd HH:mm:ss.ffff"));
            Console.WriteLine("OriginateTimestamp      " + OriginateTimestamp.UtcDateTime.ToString("yyyy/MM/dd HH:mm:ss.ffff"));
            Console.WriteLine("ReceiveTimestamp        " + ReceiveTimestamp.UtcDateTime.ToString("yyyy/MM/dd HH:mm:ss.ffff"));
            Console.WriteLine("TransmitTimestamp       " + TransmitTimestamp.UtcDateTime.ToString("yyyy/MM/dd HH:mm:ss.ffff"));
            Console.WriteLine("ClientReceiveTimestamp  " + ClientReceiveTimestamp.UtcDateTime.ToString("yyyy/MM/dd HH:mm:ss.ffff"));


            double theta = ntpc.ClientOffset();
            Console.WriteLine("遅延時間 " + theta + "sec");


            Console.ReadLine();
        }

        class NTPpacket {
            public double Timeout {
                get;
                set;
            }

            public enum ELI {
                NoWarning = 0,
                MinuteLast61Second = 1,
                MinuteLast59Second = 2,
                Warning = 3
            }

            public enum EMode {
                /// <summary>1</summary>
                SymmetricActive = 1,

                /// <summary>2</summary>
                SymmetricPassive = 2,

                /// <summary>3</summary>
                Client = 3,

                /// <summary>4</summary>
                Server = 4,

                /// <summary>5</summary>
                Broadcast = 5,
            }

            private byte[] Packet {
                get;
                set;
            }

            public ELI LI {
                get {
                    return ((ELI)((Packet[0] >> 6) & 0x03));
                }
            }

            /// <summary>Protocol Version Number</summary>
            public byte VN {
                get {
                    return ((byte)((Packet[0] >> 3) & 0x07));
                }
                set {
                    Packet[0] = (byte)(
                        (Packet[0] & 0xCF) |
                        ((value & 0x07) << 3));
                }
            }

            public EMode Mode {
                get {
                    return ((EMode)((Packet[0] >> 0) & 0x07));
                }
                private set {
                    Packet[0] = (byte)(
                        (Packet[0] & 0xF8) |
                        (((int)value & 0x07) << 0));
                }
            }

            public byte Stratum {
                get {
                    return (Packet[1]);
                }
            }

            public sbyte Precision {
                get {
                    return ((sbyte)Packet[3]);
                }
            }

            public UInt32 ReferenceIdentifier {
                get {
                    return (Get32(12));
                }
            }

            /// <summary>
            /// サーバーに直結した上位クロックソース
            /// Stratum==1の場合のみ有効
            /// Stratum!=1の場合はString.Emptyを返す
            /// </summary>
            public string ReferenceIdentifierCode {
                get {
                    if (Stratum != 1) {
                        return (string.Empty);
                    }
                    return (Encoding.ASCII.GetString(Packet, 12, 4));
                }
            }

            /// <summary>
            /// 上位クロックと同期した時間(サーバー時間)
            /// </summary>
            public UInt64 ReferenceTimestamp {
                get {
                    return (Get64(16));
                }
            }

            /// <summary>
            /// リクエストを送信した時間(クライアント時間)
            /// </summary>
            public UInt64 OriginateTimestamp {
                get {
                    return (Get64(24));
                }
            }

            /// <summary>
            /// リクエストを受信した時間(サーバー時間)
            /// </summary>
            public UInt64 ReceiveTimestamp {
                get {
                    return (Get64(32));
                }
            }

            /// <summary>
            /// レスポンスを送信した時間(サーバー時間)
            /// </summary>
            public UInt64 TransmitTimestamp {
                get {
                    return (Get64(40));
                }
                set {
                    Set64(40, value);
                }
            }

            /// <summary>
            /// レスポンスを受信した時間(クライアント時間)
            /// </summary>
            public UInt64 ClientReceiveTimestamp {
                get;
                private set;
            }


            public NTPpacket() {
                Packet = new byte[48];
                for (int i = 0; i < 48; i++) {
                    Packet[i] = 0x00;
                }

                Timeout = 2;

                for (int i = 0; i < 48; i++) {
                    Packet[i] = 0x00;
                }

                VN = 3;
                Mode = EMode.Client;
            }

            public void GetTime(string Server = "ntp.nict.jp") {
                using (System.Net.Sockets.UdpClient udpc = new System.Net.Sockets.UdpClient(Server, 123)) {
                    DateTime start = DateTime.Now;
                    System.Net.IPEndPoint remoteEP = null;


                    TransmitTimestamp = (UInt64)((DateTimeOffset.UtcNow - Offset).TotalSeconds / Math.Pow(2, -32));
                    udpc.Send(Packet, 48);

                    while (udpc.Available < 48) {
                        if ((DateTime.Now - start).TotalSeconds > Timeout) {
                            throw (new TimeoutException());
                        }
                    }

                    ClientReceiveTimestamp = (UInt64)((DateTimeOffset.UtcNow - Offset).TotalSeconds / Math.Pow(2, -32));
                    byte[] rcvBytes = udpc.Receive(ref remoteEP);

                    Packet = rcvBytes;
                }
            }

            public double ClientOffset() {
                UInt64 ts, Tr, Ts, tr;

                ts = OriginateTimestamp;
                Tr = ReceiveTimestamp;
                Ts = TransmitTimestamp;
                tr = ClientReceiveTimestamp;

                Int64 theta = (Int64)((Ts + Tr) / 2) - (Int64)((ts + tr) / 2);

                return (theta * Math.Pow(2, -32));
            }


            private void Set64(int Offset, UInt64 value) {
                Packet[Offset + 0] = (byte)((value >> 56) & 0xFF);
                Packet[Offset + 1] = (byte)((value >> 48) & 0xFF);
                Packet[Offset + 2] = (byte)((value >> 40) & 0xFF);
                Packet[Offset + 3] = (byte)((value >> 32) & 0xFF);
                Packet[Offset + 4] = (byte)((value >> 24) & 0xFF);
                Packet[Offset + 5] = (byte)((value >> 16) & 0xFF);
                Packet[Offset + 6] = (byte)((value >> 8) & 0xFF);
                Packet[Offset + 7] = (byte)((value >> 0) & 0xFF);
            }

            private UInt64 Get64(int Offset) {
                UInt64 ret = 0;

                ret |= Packet[Offset + 0];

                ret <<= 8;
                ret |= Packet[Offset + 1];

                ret <<= 8;
                ret |= Packet[Offset + 2];

                ret <<= 8;
                ret |= Packet[Offset + 3];

                ret <<= 8;
                ret |= Packet[Offset + 4];

                ret <<= 8;
                ret |= Packet[Offset + 5];

                ret <<= 8;
                ret |= Packet[Offset + 6];

                ret <<= 8;
                ret |= Packet[Offset + 7];

                return (ret);
            }

            private UInt32 Get32(int Offset) {
                UInt32 ret = 0;

                ret |= Packet[Offset + 0];

                ret <<= 8;

                ret |= Packet[Offset + 1];

                ret <<= 8;

                ret |= Packet[Offset + 2];

                ret <<= 8;

                ret |= Packet[Offset + 3];

                return (ret);
            }

            static public readonly DateTimeOffset Offset = new DateTimeOffset(1900, 1, 1, 0, 0, 0, new TimeSpan(0, 0, 0, 0, 0));
        }
    }
}

0 件のコメント:

コメントを投稿