NTPクライアント
ソースまとめ
[クラス]
- TimeSynchronizer 時刻補正
- NtpClient NTP 通信用クライアント
- NtpPacket NTP 通信で使用するパケット
NtpPacket
/// <summary> /// NTP 通信で使用するパケットを提供します。 /// </summary> 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; /// <summary> /// パケットデータを取得します。 /// </summary> public byte[] PacketData { get; private set; } /// <summary> /// NtpPacket が作成された時刻を取得します。 /// </summary> public DateTime NtpPacketCreatedTime { get; private set; } /// <summary> /// タイムサーバーとの差異を取得します。 /// </summary> public TimeSpan DifferentTimeSpan { get { long offsetTick = ( ( ReceiveTimestamp - OriginateTimestamp ) + ( TransmitTimestamp - NtpPacketCreatedTime ) ).Ticks / 2; return new TimeSpan( offsetTick ); } } /// <summary> /// 回線の遅延時間を取得します。 /// </summary> public TimeSpan NetworkDelay { get { return ( ( NtpPacketCreatedTime - OriginateTimestamp ) + ( TransmitTimestamp - ReceiveTimestamp ) ); } } /// <summary> /// NtpPacket クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="packetData">初期化に使用するパケットデータ。</param> public NtpPacket( byte[] packetData ) { PacketData = packetData; NtpPacketCreatedTime = DateTime.Now; } /// <summary> /// NTP タイムスタンプの整数部を使用して、NTP タイムスタンプの基底秒数を取得します。 /// </summary> /// <param name="seconds">判断材料となる、NTP タイムスタンプの整数部。</param> /// <returns>NTP タイムスタンプの基底秒数。</returns> static DateTime GetCompensatingDatetime( uint seconds ) { return ( seconds & 0x80000000 ) == 0 ? PASSED_COMPENSATING_DATETIME : COMPENSATING_DATETIME; } /// <summary> /// DateTime を使用して、NTP タイムスタンプの基底秒数を取得します。 /// </summary> /// <param name="dateTime">判断材料となる、DateTime。</param> /// <returns>NTP タイムスタンプの基底秒数。</returns> static DateTime GetCompensatingDatetime( DateTime dateTime ) { return PASSED_COMPENSATING_DATETIME <= dateTime ? PASSED_COMPENSATING_DATETIME : COMPENSATING_DATETIME; } /// <summary> /// 送信用パケットを作成します。 /// </summary> /// <returns>作成された送信用 NtpPacket。</returns> static public NtpPacket CreateSendPacket() { byte[] packet = new byte[48]; FillHeader( packet ); FillTransmitTimestamp( packet ); return new NtpPacket( packet ); } /// <summary> /// パケットヘッダーを充填します。 /// </summary> /// <param name="ntpPacket">充填対象の byte 配列。</param> static private void FillHeader( byte[] ntpPacket ) { const byte li = 0x00; const byte mode = 0x03; ntpPacket[0] = (byte)( li | CLIENT_VERSION << 3 | mode ); } /// <summary> /// 送信タイムスタンプを充填します。 /// </summary> /// <param name="ntpPacket">充填対象の byte 配列。</param> static private void FillTransmitTimestamp( byte[] ntpPacket ) { byte[] time = BitConverter.GetBytes( IPAddress.HostToNetworkOrder( DateTimeToNtpTimeStamp( DateTime.UtcNow ) ) ); Array.Copy( time, 0, ntpPacket, 40, 8 ); } /// <summary> /// 符号付き 32 ビット固定小数点形式から double に変換します。 /// </summary> /// <param name="signedFixedPoint">符号付き 32 ビット固定小数点。</param> /// <returns>変換された DateTime。</returns> static private double SignedFixedPointToDouble( int signedFixedPoint ) { short number = (short)( signedFixedPoint >> 16 ); ushort fraction = (ushort)( signedFixedPoint & short.MaxValue ); return number + (double)fraction / COMPENSATING_RATE_16; } /// <summary> /// NTP 64ビットタイムスタンプ形式から DateTime に変換します。 /// </summary> /// <param name="ntpTimeStamp">NTP 64ビットタイムスタンプ。</param> /// <returns>変換された DateTime。</returns> 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 ); } /// <summary> /// DateTime 型から NTP 64ビットタイムスタンプ形式に変換します。 /// </summary> /// <param name="dateTime">変換する DateTime。</param> /// <returns>変換された NTP 64ビットタイムスタンプ。</returns> 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 ); } /// <summary> /// 閏秒指示子を取得します。 /// </summary> public int LeapIndicator { get { return PacketData[0] >> 6 & 0x03; } } /// <summary> /// バージョンを取得します。 /// </summary> public int Version { get { return PacketData[0] >> 3 & 0x03; } } /// <summary> /// Mode を取得します。 /// </summary> public int Mode { get { return PacketData[0] & 0x03; } } /// <summary> /// 階層を取得します。 /// </summary> public int Stratum { get { return PacketData[1]; } } /// <summary> /// ポーリング間隔を取得します。 /// </summary> 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 ); } } } /// <summary> /// 精度を取得します。 /// </summary> public double Precision { get { return Math.Pow(2,(SByte)PacketData[3]); } } /// <summary> /// ルート遅延を取得します。 /// </summary> public double RootDelay { get { return SignedFixedPointToDouble( IPAddress.NetworkToHostOrder( BitConverter.ToInt32( PacketData, 4 ) ) ); } } /// <summary> /// ルート分散を取得します。 /// </summary> public double RootDispersion { get { return SignedFixedPointToDouble( IPAddress.NetworkToHostOrder( BitConverter.ToInt32( PacketData, 8 ) ) ); } } /// <summary> /// 参照識別子を取得します。 /// </summary> 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(); } } } /// <summary> /// 参照タイムスタンプを取得します。 /// </summary> public DateTime ReferenceTimestamp { get { return NtpTimeStampToDateTime( IPAddress.NetworkToHostOrder( BitConverter.ToInt64( PacketData, 16 ) ) ).ToLocalTime(); } } /// <summary> /// 開始タイムスタンプを取得します。 /// </summary> public DateTime OriginateTimestamp { get { return NtpTimeStampToDateTime( IPAddress.NetworkToHostOrder( BitConverter.ToInt64( PacketData, 24 ) ) ).ToLocalTime(); } } /// <summary> /// 受信タイムスタンプを取得します。 /// </summary> public DateTime ReceiveTimestamp { get { return NtpTimeStampToDateTime( IPAddress.NetworkToHostOrder( BitConverter.ToInt64( PacketData, 32 ) ) ).ToLocalTime(); } } /// <summary> /// 送信タイムスタンプを取得します。 /// </summary> public DateTime TransmitTimestamp { get { return NtpTimeStampToDateTime( IPAddress.NetworkToHostOrder( BitConverter.ToInt64( PacketData, 40 ) ) ).ToLocalTime(); } } /// <summary> /// 鍵識別子を取得します。 /// </summary> public string KeyIdentifier { get { return Encoding.ASCII.GetString( PacketData, 48, 4 ).TrimEnd( new char() ); } } /// <summary> /// メッセージダイジェストを取得します。 /// </summary> public string MessageDigest { get { return Encoding.ASCII.GetString( PacketData, 52, 16 ).TrimEnd( new char() ); } } }
NtpClient
/// <summary> /// NTP 通信用クライアントを提供します。 /// </summary> public class NtpClient { const int _port = 123; IPEndPoint _ntpServerEndPoint; /// <summary> /// 接続サーバーの IP を指定して NtpClient クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="ipAdress">接続サーバーの IP アドレス。</param> public NtpClient( IPAddress ipAdress ) { _ntpServerEndPoint = new IPEndPoint( ipAdress, _port ); } /// <summary> /// 同期するための Ntp データを取得します。 /// </summary> /// <returns>同期するための Ntp データ。</returns> public NtpPacket GetNtpData() { Socket socket = Connect(); SendPacket( socket ); return ReceivePacket( socket ); } /// <summary> /// パケットを受信します。 /// </summary> /// <param name="socket">受信に使用する Socket。</param> /// <returns>受信した NtpPacket。</returns> private NtpPacket ReceivePacket( Socket socket ) { byte[] receiveData = new byte[48 + ( 32 + 128 ) / 8]; EndPoint endPoint = new IPEndPoint( IPAddress.Any, 0 ); int recv = socket.ReceiveFrom( receiveData, ref endPoint ); return new NtpPacket( receiveData ); } /// <summary> /// パケットを送信します。 /// </summary> /// <param name="socket">送信に使用する Socket。</param> private void SendPacket( Socket socket ) { NtpPacket sendPacket = NtpPacket.CreateSendPacket(); socket.SendTo( sendPacket.PacketData, _ntpServerEndPoint ); } /// <summary> /// サーバに接続し Socket を取得します。 /// </summary> /// <returns>接続された Socket。</returns> private Socket Connect() { Socket socket = new Socket( AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp ); socket.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 2000 ); return socket; } }
おまけ
TimeSynchronizer
/// <summary> /// 時刻補正を行う機能を提供します。 /// </summary> public class TimeSynchronizer { /// <summary> /// SYSTEMTIME 構造体は、月、日、年、曜日、時、分、秒、ミリ秒の各メンバを使用し日付と時間を表します。 /// </summary> [StructLayout( LayoutKind.Sequential )] public struct SystemTime { /// <summary> /// 現在年。 /// </summary> public ushort wYear; /// <summary> /// 現在月。1 月は 1 です。 /// </summary> public ushort wMonth; /// <summary> /// 現在の曜日。日曜が 0、月曜が 1、というように表します。 /// </summary> public ushort wDayOfWeek; /// <summary> /// 現在日。 /// </summary> public ushort wDay; /// <summary> /// 現在時。 /// </summary> public ushort wHour; /// <summary> /// 現在分。 /// </summary> public ushort wMinute; /// <summary> /// 現在秒。 /// </summary> public ushort wSecond; /// <summary> /// 現在のミリ秒。 /// </summary> public ushort wMilliseconds; } /// <summary> /// 現在のローカル日時を設定します。 /// </summary> /// <param name="sysTime">設定するべき現在のローカル日時を保持している、1 個の 構造体へのポインタを指定します。この構造体の wDayOfWeek メンバは無視されます。</param> /// <returns>関数が成功すると、0 以外の値が返ります。関数が失敗すると、0 が返ります。 /// </returns> [DllImport( "kernel32.dll" )] public static extern bool SetLocalTime( ref SystemTime lpSystemTime ); /// <summary> /// 時刻調整をするために必要な補正幅。 /// </summary> /// <remarks>この値を越えた場合は時刻が補正されます。</remarks> public int AdjustOverOffsetLength { get; set; } /// <summary> /// TimeSynchronizer クラスの新しいインスタンスを初期化します。 /// </summary> public TimeSynchronizer() { AdjustOverOffsetLength = 500; } /// <summary> /// 現在のローカル日時を設定します。 /// </summary> /// <param name="dateTime">設定する日時</param> public static void SetLocalTime( DateTime dateTime ) { SystemTime sysTime = new SystemTime(); sysTime.wYear = (ushort)dateTime.Year; sysTime.wMonth = (ushort)dateTime.Month; sysTime.wDay = (ushort)dateTime.Day; sysTime.wHour = (ushort)dateTime.Hour; sysTime.wMinute = (ushort)dateTime.Minute; sysTime.wSecond = (ushort)dateTime.Second; sysTime.wMilliseconds = (ushort)dateTime.Millisecond; if ( !SetLocalTime( ref sysTime ) ) { throw new Win32Exception( Marshal.GetLastWin32Error() ); } } /// <summary> /// 時刻を同期します。 /// </summary> /// <param name="ntpData">同期に利用する NtpPacket。</param> /// <returns>補正されれば true。それ以外は false。</returns> public bool Synchronize( NtpPacket ntpData ) { return Synchronize( ntpData, AdjustOverOffsetLength ); } /// <summary> /// 時刻を同期します。 /// </summary> /// <param name="ntpData">同期に利用する NtpPacket。</param> /// <param name="adjustOverOffsetLength">時刻調整をするために必要な補正幅。</param> /// <returns>補正されれば true。それ以外は false。</returns> static public bool Synchronize( NtpPacket ntpData, int adjustOverOffsetLength ) { if ( ntpData.LeapIndicator == 3 ) { throw new ServerException( "サーバ側で時刻が同期できていません。" ); } TimeSpan offsetSpan = ntpData.DifferentTimeSpan; return Synchronize( offsetSpan.TotalMilliseconds, adjustOverOffsetLength ); } /// <summary> /// 時刻を同期します。 /// </summary> /// <param name="offsetMilliseconds">時刻を同期するための閾値。</param> /// <param name="adjustOverOffsetLength">時刻調整をするために必要な補正幅。</param> /// <returns>補正されれば true。それ以外は false。</returns> public static bool Synchronize( double offsetMilliseconds, int adjustOverOffsetLength ) { if ( adjustOverOffsetLength < Math.Abs( offsetMilliseconds ) ) { SetLocalTime( DateTime.Now.AddMilliseconds( offsetMilliseconds ) ); return true; } return false; } }