SNTP サーバーとの時刻比較(その1)

IPv6 のユニークローカルユニキャストアドレス算出ツールのついでに作った SNTP サーバーとの時刻比較ツールについて書いてみます。ついでに作ったものなので、コンソールベースのアプリケーションです。使っているパソコンの時刻がどの程度合っているのかを確認するだけなので。。。まぁあんまり使う機会もないかなと 🙂

起動すると、こんな感じの画面表示になります。

プログラムは NtpMessage クラス、NtpTimeStampUtil クラス、NtpClient クラス、NtpMessageException クラスと Program クラスから構成されます。また、インターフェイスとして、INtpMessage と INtpClient も作っています。

まずは、INtpMessage から。。。列挙型の宣言も INtpMessage.cs で行なっています。

//
    /// <summary>
    /// うるう秒指示子の状態
    /// </summary>
    public enum LeapIndicatorValue
    {
        /// <summary>
        /// 警告なし 
        /// </summary>
        NoWarning = 0,
        /// <summary>
        /// 最後の分は61秒
        /// </summary>
        LastMinute61,
        /// <summary>
        /// 最後の分は59秒
        /// </summary>
        LastMinute59,
        /// <summary>
        /// 警報(時計が同期していません
        /// </summary>
        AlarmCondition
    }

    /// <summary>
    /// モードの状態
    /// </summary>
    public enum ModeValue
    {
        /// <summary>
        /// 対称能動
        /// </summary>
        SymmetricActive = 1,
        /// <summary>
        /// 対称受動
        /// </summary>
        SymmetricPassive,
        /// <summary>
        /// クライアント 
        /// </summary>
        Client,
        /// <summary>
        /// サーバー 
        /// </summary>
        Server,
        /// <summary>
        /// ブロードキャスト 
        /// </summary>
        Broadcast,
        /// <summary>
        /// 未知のモード
        /// </summary>
        Unknown = 99
    }

    /// <summary>
    /// 階級の状態
    /// </summary>
    public enum StratumState
    {
        /// <summary>
        /// 規定なし
        /// </summary>
        Unspecified = 0,
        /// <summary>
        /// 一次基準源(電波時計など)
        /// </summary>
        PrimaryReference,
        /// <summary>
        /// 従属的な基準
        /// </summary>
        SecondaryReference,
        /// <summary>
        /// 予約済み
        /// </summary>
        Reserved
    }
//
    public interface INtpMessage
    {
        /// <summary>
        /// NTP メッセージ
        /// </summary>
        byte[] NtpMessageArray { get; }
        /// <summary>
        /// NTP メッセージの既定長
        /// </summary>
        byte MessageLength { get; }
        /// <summary>
        /// Leap indicator を取得する
        /// </summary>
        LeapIndicatorValue LeapIndicator { get; }
        /// <summary>
        /// バージョンを取得する
        /// </summary>
        byte VersionNumber { get; }
        /// <summary>
        /// モードを取得する
        /// </summary>
        ModeValue Mode { get; }
        /// <summary>
        /// 階級番号を取得する
        /// </summary>
        byte StratumNumber { get; }
        /// <summary>
        /// 階級種別を取得する
        /// </summary>
        StratumState Stratum { get; }
        /// <summary>
        /// ポーリング間隔を取得する
        /// </summary>
        uint PollInterval { get; }
        /// <summary>
        /// 精度(ミリ秒)を取得する
        /// </summary>
        double Precision { get; }
        /// <summary>
        /// ルート遅延を取得する
        /// </summary>
        double RootDelay { get; }
        /// <summary>
        /// ルート分散を(ミリ秒)取得する
        /// </summary>
        double RootDispersion { get; }
        /// <summary>
        /// 参照IDを取得する
        /// </summary>
        string ReferenceIdentifier { get; }
        /// <summary>
        /// 参考タイムスタンプを取得する
        /// </summary>
        ulong ReferenceTimestamp { get; }
        /// <summary>
        /// 発信タイムスタンプを取得する
        /// </summary>
        ulong OriginateTimestamp { get; }
        /// <summary>
        /// 受信タイムスタンプを取得する
        /// </summary>
        ulong ReceiveTimestamp { get; }
        /// <summary>
        /// 送信タイムスタンプを取得/設定する
        /// </summary>
        ulong TransmitTimestamp { get; set; }
        /// <summary>
        /// NTP サーバーへ送るメッセージを初期化する
        /// </summary>
        /// <returns></returns>
        void InitializeMessage();
    }

次は、NtpMessage クラスですが、全体をいっきに書くには長いので、分割します。
また、Nunit でテストをするコードも書いているので、それ前提のコードになっています(テストプロジェクトのほうまでは長くなるので書きませんけど)。

using System;
using System.Text;
using System.Runtime.CompilerServices;

// TestTagManage からのアクセスだけは受け入れる
[assembly: InternalsVisibleTo("NtpGetTimestamp.Test")]

namespace Sntp
{
    public class NtpMessage : INtpMessage
    {
        internal static readonly int NTP_VERSION = 4;
        internal static readonly byte MESSAGE_LENGTH = 48;

        private byte[] ntpMessage;
        internal bool isInitialized = false;

        // NTP メッセージ中の各タイムスタンプへのオフセット値
        internal const byte offsetReferenceId = 12;
        internal const byte offsetReferenceTimestamp = 16;
        internal const byte offsetOriginateTimestamp = 24;
        internal const byte offsetReceiveTimestamp = 32;
        internal const byte offsetTransmitTimestamp = 40;

        public NtpMessage()
        {
            ntpMessage = new byte[MESSAGE_LENGTH];
        }
//
        /// <summary>
        /// NTP メッセージを取得/設定する
        /// </summary>
        public byte[] NtpMessageArray
        {
            get { return ntpMessage; }
            internal set { ntpMessage = value; }
        }

        /// <summary>
        /// NTP メッセージの既定長
        /// </summary>
        public byte MessageLength
        {
            get { return MESSAGE_LENGTH; }
        }

        /// <summary>
        /// Leap indicator を取得する
        /// </summary>
        public LeapIndicatorValue LeapIndicator
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");

                // 上位 2 bit を分離
                var val = (byte)(ntpMessage[0] >> 6);
                switch (val)
                {
                    case 0: return LeapIndicatorValue.NoWarning;
                    case 1: return LeapIndicatorValue.LastMinute61;
                    case 2: return LeapIndicatorValue.LastMinute59;
                    case 3:
                    default:
                        return LeapIndicatorValue.AlarmCondition;
                }
            }
        }
//
        /// <summary>
        /// バージョンを取得する
        /// </summary>
        public byte VersionNumber
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");

                // 3-5 bit を取り出し
                var val = (byte)((ntpMessage[0] & 0x38) >> 3);
                return val;
            }
        }

        /// <summary>
        /// モードを取得する
        /// </summary>
        public ModeValue Mode
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");

                // 0-2 bit を取り出し
                var val = (byte)(ntpMessage[0] & 0x7);
                switch (val)
                {
                    case 1: return ModeValue.SymmetricActive;
                    case 2: return ModeValue.SymmetricPassive;
                    case 3: return ModeValue.Client;
                    case 4: return ModeValue.Server;
                    case 5: return ModeValue.Broadcast;
                    case 0:
                    case 6:
                    case 7:
                    default:
                        return ModeValue.Unknown;
                }
            }
        }

        /// <summary>
        /// 階級番号を取得する
        /// </summary>
        public byte StratumNumber
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");

                return (byte)ntpMessage[1];
            }
        }

        /// <summary>
        /// 階級種別を取得する
        /// </summary>
        public StratumState Stratum
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");

                var val = (byte)ntpMessage[1];
                if (val == 0)
                    return StratumState.Unspecified;
                else if (val == 1)
                    return StratumState.PrimaryReference;
                else if (val <= 15)
                    return StratumState.SecondaryReference;
                else
                    return StratumState.Reserved;
            }
        }
//
        /// <summary>
        /// ポーリング間隔を取得する
        /// </summary>
        public uint PollInterval
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");

                return (uint)Math.Pow(2, ntpMessage[2]);
            }
        }

        /// <summary>
        /// 精度(ミリ秒)を取得する
        /// </summary>
        public double Precision
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");

                return (1000 * Math.Pow(2, (sbyte)ntpMessage[3]));
            }
        }

        /// <summary>
        /// ルート遅延(ミリ秒)を取得する
        /// </summary>
        public double RootDelay
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");

                var temp = new byte[4];
                Array.Copy(ntpMessage, 4, temp, 0, 4);
                if (BitConverter.IsLittleEndian)    // ホストバイトオーダーがリトルエンディアンだったら
                    Array.Reverse(temp);            // リトルエンディアンへ変換する

                return ((double)BitConverter.ToInt32(temp, 0) / NtpTimeStampUtil.UNIT_RATING16) * 1000;
            }
        }
//
        /// <summary>
        /// ルート分散(ミリ秒)を取得する
        /// </summary>
        public double RootDispersion
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");

                var temp = new byte[4];
                Array.Copy(ntpMessage, 8, temp, 0, 4);
                if (BitConverter.IsLittleEndian)    // ホストバイトオーダーがリトルエンディアンだったら
                    Array.Reverse(temp);            // リトルエンディアンへ変換する

                return ((double)BitConverter.ToInt32(temp, 0) / NtpTimeStampUtil.UNIT_RATING16) * 1000;
            }
        }

        /// <summary>
        /// 参照IDを取得する
        /// </summary>
        public string ReferenceIdentifier
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");

                var val = "";
                switch (Stratum)
                {
                    case StratumState.Unspecified:
                    case StratumState.PrimaryReference:
                        val = Encoding.ASCII.GetString(ntpMessage, offsetReferenceId, 4).TrimEnd('');
                        break;
                    case StratumState.SecondaryReference:
                        switch (VersionNumber)
                        {
                            case 3: // Version 3 は IPv4 アドレス
                                val = ntpMessage[offsetReferenceId].ToString() + "." +
                                      ntpMessage[offsetReferenceId + 1].ToString() + "." +
                                      ntpMessage[offsetReferenceId + 2].ToString() + "." +
                                      ntpMessage[offsetReferenceId + 3].ToString();
                                break;
                            case 4: // Version 4 は参照源の最終送信タイムスタンプの下位32ビット
                                var array = new byte[NtpTimeStampUtil.TIMESTAMP_LENGTH / 2];
                                Array.Copy(ntpMessage, offsetReferenceId, array, 0, NtpTimeStampUtil.TIMESTAMP_LENGTH / 2);
                                var temp = new StringBuilder();
                                foreach (var n in array)
                                {
                                    temp.Append(string.Format("{0:X2}", n));
                                }
                                val = temp.ToString();
                                break;
                            default:
                                val = "N/A";
                                break;
                        }
                        break;
                }

                return val;
            }
        }
//
        /// <summary>
        /// 参考タイムスタンプを取得する
        /// </summary>
        public ulong ReferenceTimestamp
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");
                if (LeapIndicator == LeapIndicatorValue.AlarmCondition)
                    throw new NtpMessageException("NTP サーバーの時計が同期していないため、時刻を取得できませんでした。");

                return getTimeStamp(offsetReferenceTimestamp);
            }
        }

        /// <summary>
        /// 発信タイムスタンプを取得する
        /// </summary>
        public ulong OriginateTimestamp
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");
                if (LeapIndicator == LeapIndicatorValue.AlarmCondition)
                    throw new NtpMessageException("NTP サーバーの時計が同期していないため、時刻を取得できませんでした。");

                return getTimeStamp(offsetOriginateTimestamp);
            }
        }

        /// <summary>
        /// 受信タイムスタンプを取得する
        /// </summary>
        public ulong ReceiveTimestamp
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Broadcast && Mode != ModeValue.Server)
                    throw new InvalidOperationException("まだ NTP サーバーからメッセージを取得していません。");
                if (LeapIndicator == LeapIndicatorValue.AlarmCondition)
                    throw new NtpMessageException("NTP サーバーの時計が同期していないため、時刻を取得できませんでした。");

                return getTimeStamp(offsetReceiveTimestamp);
            }
        }

        /// <summary>
        /// 送信タイムスタンプを取得/設定する
        /// </summary>
        public ulong TransmitTimestamp
        {
            get
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");
                if (Mode != ModeValue.Client && LeapIndicator == LeapIndicatorValue.AlarmCondition)
                    throw new NtpMessageException("NTP サーバーの時計が同期していないため、時刻を取得できませんでした。");

                return getTimeStamp(offsetTransmitTimestamp);
            }
            set
            {
                if (!isInitialized)
                    throw new InvalidOperationException("メッセージが初期化されていません。");

                setTimeStamp(offsetTransmitTimestamp, value);
            }
        }
//
        /// <summary>
        /// NTP サーバーへ送るメッセージを初期化する
        /// </summary>
        /// <returns></returns>
        public void InitializeMessage()
        {
            byte mode = (byte)ModeValue.Client;
            ntpMessage[0] = (byte)(NTP_VERSION << 3 | mode);
            // 他のフィールドを 0 で初期化
            for (var i = 1; i < MESSAGE_LENGTH; i++)
            {
                ntpMessage[i] = 0;
            }

            isInitialized = true;
            TransmitTimestamp = NtpTimeStampUtil.Datetime2NtpTimeStamp(DateTime.UtcNow);    // 送信タイムスタンプをセット
        }

        /// <summary>
        /// 指定されたフィールドからタイムスタンプを取得する
        /// </summary>
        /// <param name="offset"></param>
        /// <returns></returns>
        private ulong getTimeStamp(byte offset)
        {
            var array = new byte[NtpTimeStampUtil.TIMESTAMP_LENGTH];
            Array.Copy(ntpMessage, offset, array, 0, NtpTimeStampUtil.TIMESTAMP_LENGTH);
            if (BitConverter.IsLittleEndian)    // ホストバイトオーダーがリトルエンディアンだったら
                Array.Reverse(array);           // リトルエンディアンへ変換する
            return BitConverter.ToUInt64(array, 0);
        }

        /// <summary>
        /// 指定されたフィールドにタイムスタンプをセットする
        /// </summary>
        /// <param name="offset"></param>
        /// <param name="timeStamp"></param>
        private void setTimeStamp(byte offset, ulong timeStamp)
        {
            var bigEndianTimeStamp = BitConverter.GetBytes(timeStamp);
            if (BitConverter.IsLittleEndian)    // ホストバイトオーダーがリトルエンディアンだったら
                Array.Reverse(bigEndianTimeStamp);  // ビッグエンディアンへ変換する

            Array.Copy(bigEndianTimeStamp, 0, ntpMessage, offset, NtpTimeStampUtil.TIMESTAMP_LENGTH);
        }
    }
}

長くなったので、(その2)へ続きます。。。


SNTP サーバーとの時刻比較(その1)」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です