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;
	}
}