Click here to Skip to main content
15,880,956 members
Articles / Desktop Programming / Windows Forms

An SNTP Client for C# and VB.NET

Rate me:
Please Sign up or sign in to vote.
4.93/5 (29 votes)
22 Jul 2009CPOL11 min read 162.3K   9K   66  
A complete overview and implementation of SNTP from a client perspective.
using System;
using System.Collections.Generic;

namespace DaveyM69.Components.SNTP
{
    /// <summary>
    /// A class that represents a SNTP packet.
    /// See http://www.faqs.org/rfcs/rfc2030.html for full details of protocol.
    /// </summary>
    public class SNTPData
    {
		#region Fields

        private byte[] data;
        private static readonly DateTime Epoch = new DateTime(1900, 1, 1);
        private const int LeapIndicatorLength = 4;
        private const byte LeapIndicatorMask = 0xC0;
        private const byte LeapIndicatorOffset = 6;
        /// <summary>
        /// The maximum number of bytes in a SNTP packet.
        /// </summary>
        public const int MaximumLength = 68;
        /// <summary>
        /// The minimum number of bytes in a SNTP packet.
        /// </summary>
        public const int MinimumLength = 48;
        private const byte ModeComplementMask = 0xF8;
        private const int ModeLength = 8;
        private const byte ModeMask = 0x07;
        private const int originateIndex = 24;
        private const int receiveIndex = 32;
        private const int referenceIdentifierOffset = 12;
        private const int referenceIndex = 16;
        private const int StratumLength = 16;
        /// <summary>
        /// Represents the number of ticks in 1 second.
        /// </summary>
        public const long TicksPerSecond = TimeSpan.TicksPerSecond;
        private const int transmitIndex = 40;
        private const byte VersionNumberComplementMask = 0xC7;
        private const int VersionNumberLength = 8;
        private const byte VersionNumberMask = 0x38;
        private const byte VersionNumberOffset = 3;

		#endregion Fields 

		#region Constructors 

        internal SNTPData(byte[] bytearray)
        {
            if (bytearray.Length >= MinimumLength && bytearray.Length <= MaximumLength)
                data = bytearray;
            else
                throw new ArgumentOutOfRangeException(
                    "Byte Array",
                    string.Format(
                    "Byte array must have a length between {0} and {1}.",
                    MinimumLength, MaximumLength));
        }

        internal SNTPData()
            : this(new byte[48])
        { }

		#endregion Constructors 

		#region Properties

        /// <summary>
        /// Gets the DateTime (UTC) when the data arrived from the server.
        /// </summary>
        public DateTime DestinationDateTime
        {
            get;
            internal set;
        }

        /// <summary>
        /// Gets a warning of an impending leap second to be inserted/deleted in the last minute of the current day.
        /// </summary>
        public LeapIndicator LeapIndicator
        {
            get { return (LeapIndicator)LeapIndicatorValue; }
        }

        /// <summary>
        /// Gets a textual representation of the LeapIndicator property.
        /// </summary>
        public string LeapIndicatorText
        {
            get
            {
                string result;
                LeapIndicatorDictionary.TryGetValue(LeapIndicator, out result);
                return result;
            }
        }

        private byte LeapIndicatorValue
        {
            get { return (byte)((data[0] & LeapIndicatorMask) >> LeapIndicatorOffset); }
        }

        /// <summary>
        /// Gets the number of bytes in the packet.
        /// </summary>
        public int Length
        {
            get { return data.Length; }
        }

        /// <summary>
        /// Gets the difference in seconds between the local time and the time retrieved from the server.
        /// </summary>
        public double LocalClockOffset
        {
            get
            {
                return ((double)((ReceiveDateTime.Ticks - OriginateDateTime.Ticks) + 
                    (TransmitDateTime.Ticks - DestinationDateTime.Ticks)) / 2) / TicksPerSecond;
            }
        }

        /// <summary>
        /// Gets the operating mode of whatever last altered the packet.
        /// </summary>
        public Mode Mode
        {
            get { return (Mode)ModeValue; }
            private set { ModeValue = (byte)value; }
        }

        /// <summary>
        /// Gets a textual representation of the Mode property.
        /// </summary>
        public string ModeText
        {
            get
            {
                string result;
                ModeDictionary.TryGetValue(Mode, out result);
                return result;
            }
        }

        private byte ModeValue
        {
            get { return (byte)(data[0] & ModeMask); }
            set { data[0] = (byte)((data[0] & ModeComplementMask) | value); }
        }

        /// <summary>
        /// Gets the DateTime (UTC) at which the request departed the client for the server.
        /// </summary>
        public DateTime OriginateDateTime
        {
            get { return TimestampToDateTime(originateIndex); }
        }

        /// <summary>
        /// Gets the maximum interval between successive messages, in seconds.
        /// </summary>
        public double PollInterval
        {
            get { return Math.Pow(2, (sbyte)data[2]); }
        }

        /// <summary>
        /// Gets the precision of the clock, in seconds.
        /// </summary>
        public double Precision
        {
            get { return Math.Pow(2, (sbyte)data[3]); }
        }

        /// <summary>
        /// Gets the DateTime (UTC) at which the request arrived at the server.
        /// </summary>
        public DateTime ReceiveDateTime
        {
            get { return TimestampToDateTime(receiveIndex); }
        }

        /// <summary>
        /// Gets the DateTime (UTC) at which the clock was last set or corrected.
        /// </summary>
        public DateTime ReferenceDateTime
        {
            get { return TimestampToDateTime(referenceIndex); }
        }

        /// <summary>
        /// Gets the identifier of the reference source.
        /// </summary>
        public string ReferenceIdentifier
        {
            get
            {
                string result = null;
                switch (Stratum)
                {
                    case Stratum.Unspecified:
                    case Stratum.Primary:
                        UInt32 id = 0;
                        for (int i = 0; i <= 3; i++)
                            id = (id << 8) | data[referenceIdentifierOffset + i];
                        if (!RefererenceIdentifierDictionary.TryGetValue(((ReferenceIdentifier)id), out result))
                        {
                            result = string.Format("{0}{1}{2}{3}",
                                (char)data[referenceIdentifierOffset],
                                (char)data[referenceIdentifierOffset + 1],
                                (char)data[referenceIdentifierOffset + 2],
                                (char)data[referenceIdentifierOffset + 3]);
                        }
                        break;
                    case Stratum.Secondary:
                    case Stratum.Secondary3:
                    case Stratum.Secondary4:
                    case Stratum.Secondary5:
                    case Stratum.Secondary6:
                    case Stratum.Secondary7:
                    case Stratum.Secondary8:
                    case Stratum.Secondary9:
                    case Stratum.Secondary10:
                    case Stratum.Secondary11:
                    case Stratum.Secondary12:
                    case Stratum.Secondary13:
                    case Stratum.Secondary14:
                    case Stratum.Secondary15:
                        switch (VersionNumber)
                        {
                            case VersionNumber.Version3:
                                result = string.Format("{0}.{1}.{2}.{3}",
                                    data[referenceIdentifierOffset],
                                    data[referenceIdentifierOffset + 1],
                                    data[referenceIdentifierOffset + 2],
                                    data[referenceIdentifierOffset + 3]);
                                break;
                            // The code below works with the Version 4 spec, but many servers respond as v4 but fill this as v3.
                            case VersionNumber.Version4:
                                // result = Timestamp32ToDateTime(referenceIdentifierOffset).ToString();
                                break;
                            default:
                                if (VersionNumber < VersionNumber.Version3)
                                {
                                    result = string.Format("{0}.{1}.{2}.{3}",
                                    data[referenceIdentifierOffset],
                                    data[referenceIdentifierOffset + 1],
                                    data[referenceIdentifierOffset + 2],
                                    data[referenceIdentifierOffset + 3]);
                                }
                                else
                                {
                                    // For future
                                }
                                break;
                        }
                        break;
                    default:
                        break;
                }
                return result;
            }
        }

        /// <summary>
        /// Gets the total delay to the primary reference source, in seconds.
        /// </summary>
        public double RootDelay
        {
            get { return SecondsStampToSeconds(4); }
        }

        /// <summary>
        /// Gets the nominal error relative to the primary reference source, in seconds.
        /// </summary>
        public double RootDispersion
        {
            get { return SecondsStampToSeconds(8); }
        }

        /// <summary>
        /// Gets the total roundtrip delay, in seconds.
        /// </summary>
        public double RoundTripDelay
        {
            get
            {
                return (double)((DestinationDateTime.Ticks - OriginateDateTime.Ticks)
                    - (ReceiveDateTime.Ticks - TransmitDateTime.Ticks)) / TicksPerSecond;
            }
        }

        /// <summary>
        /// Gets the stratum level of the clock.
        /// </summary>
        public Stratum Stratum
        {
            get { return (Stratum)StratumValue; }
        }

        /// <summary>
        /// Gets a textual representation of the Stratum property.
        /// </summary>
        public string StratumText
        {
            get
            {
                string result;
                if (!StratumDictionary.TryGetValue(Stratum, out result))
                    result = "Reserved";
                return result;
            }
        }

        private byte StratumValue
        {
            get { return data[1]; }
        }

        /// <summary>
        /// Gets the DateTime (UTC) at which the reply departed the server for the client.
        /// </summary>
        public DateTime TransmitDateTime
        {
            get { return TimestampToDateTime(transmitIndex); }
            private set { DateTimeToTimestamp(value, transmitIndex); }
        }

        /// <summary>
        /// Gets the NTP/SNTP version number.
        /// </summary>
        public VersionNumber VersionNumber
        {
            get { return (VersionNumber)VersionNumberValue; }
            private set { VersionNumberValue = (byte)value; }
        }

        /// <summary>
        /// Gets a textual representation of the VersionNumber property.
        /// </summary>
        public string VersionNumberText
        {
            get
            {
                string result;
                if (!VersionNumberDictionary.TryGetValue(VersionNumber, out result))
                    result = "Unknown";
                return result;
            }
        }

        private byte VersionNumberValue
        {
            get { return (byte)((data[0] & VersionNumberMask) >> VersionNumberOffset); }
            set { data[0] = (byte)((data[0] & VersionNumberComplementMask) | (value << VersionNumberOffset)); }
        }

		#endregion Properties 

		#region Methods

		// Private Methods 

        /// <summary>
        /// Converts a DateTime into a byte array and stores it starting at the position specifed.
        /// </summary>
        /// <param name="dateTime">The DateTime to convert.</param>
        /// <param name="startIndex">The index in the data at which to start.</param>
        private void DateTimeToTimestamp(DateTime dateTime, int startIndex)
        {
            UInt64 ticks = (UInt64)(dateTime - Epoch).Ticks;
            UInt64 seconds = ticks / TicksPerSecond;
            UInt64 fractions = ((ticks % TicksPerSecond) * 0x100000000L) / TicksPerSecond;
            for (int i = 3; i >= 0; i--)
            {
                data[startIndex + i] = (byte)seconds;
                seconds = seconds >> 8;
            }
            for (int i = 7; i >= 4; i--)
            {
                data[startIndex + i] = (byte)fractions;
                fractions = fractions >> 8;
            }
        }

        /// <summary>
        /// Converts a 32bit seconds (16 integer part, 16 fractional part) into a double that represents the value in seconds.
        /// </summary>
        /// <param name="startIndex">The index in the data at which to start.</param>
        /// <returns>A double that represents the value in seconds</returns>
        private double SecondsStampToSeconds(int startIndex)
        {
            UInt64 seconds = 0;
            for (int i = 0; i <= 1; i++)
                seconds = (seconds << 8) | data[startIndex + i];
            UInt64 fractions = 0;
            for (int i = 2; i <= 3; i++)
                fractions = (fractions << 8) | data[startIndex + i];
            UInt64 ticks = (seconds * TicksPerSecond) + ((fractions * TicksPerSecond) / 0x10000L);
            return (double)ticks / TicksPerSecond;
        }

        private DateTime Timestamp32ToDateTime(int startIndex)
        {
            UInt64 seconds = 0;
            for (int i = 0; i <= 3; i++)
                seconds = (seconds << 8) | data[startIndex + i];
            UInt64 ticks = (seconds * TicksPerSecond);
            return Epoch + TimeSpan.FromTicks((Int64)ticks);
        }

        /// <summary>
        /// Converts a byte array starting at the position specified into a DateTime.
        /// </summary>
        /// <param name="startIndex">The index in the data at which to start.</param>
        /// <returns>A DateTime converted from a byte array starting at the position specified.</returns>
        private DateTime TimestampToDateTime(int startIndex)
        {
            UInt64 seconds = 0;
            for (int i = 0; i <= 3; i++)
                seconds = (seconds << 8) | data[startIndex + i];
            UInt64 fractions = 0;
            for (int i = 4; i <= 7; i++)
                fractions = (fractions << 8) | data[startIndex + i];
            UInt64 ticks = (seconds * TicksPerSecond) + ((fractions * TicksPerSecond) / 0x100000000L);
            return Epoch + TimeSpan.FromTicks((Int64)ticks);
        }
		// Internal Methods 

        /// <summary>
        /// A SNTPData that is used by a client to send to a server to request data.
        /// </summary>
        internal static SNTPData GetClientRequestPacket(VersionNumber versionNumber)
        {
            SNTPData packet = new SNTPData();
            packet.Mode = Mode.Client;
            packet.VersionNumber = versionNumber;
            packet.TransmitDateTime = DateTime.Now.ToUniversalTime();
            return packet;
        }

		#endregion Methods 

        #region Conversion Operators

        public static implicit operator SNTPData(byte[] byteArray)
        {
            return new SNTPData(byteArray);
        }

        public static implicit operator byte[](SNTPData sntpPacket)
        {
            return sntpPacket.data;
        }

        #endregion Conversion Operators

        #region Dictionaries

        private static readonly Dictionary<LeapIndicator, string> LeapIndicatorDictionary = new Dictionary<LeapIndicator, string>()
        {
            {LeapIndicator.NoWarning, "No warning"},
            {LeapIndicator.LastMinute61Seconds, "Last minute has 61 seconds"},
            {LeapIndicator.LastMinute59Seconds, "Last minute has 59 seconds"},
            {LeapIndicator.Alarm, "Alarm condition (clock not synchronized)"},
        };

        private static readonly Dictionary<VersionNumber, string> VersionNumberDictionary = new Dictionary<VersionNumber, string>()
        {
            //{VersionNumber.Version0, "Version 0 (obselete)"},
            //{VersionNumber.Version1, "Version 1 (obselete)"},
            //{VersionNumber.Version2, "Version 2 (obselete)"},
            {VersionNumber.Version3, "Version 3 (IPv4 only)"},
            {VersionNumber.Version4, "Version 4 (IPv4, IPv6 and OSI)"}
        };

        private static readonly Dictionary<Mode, string> ModeDictionary = new Dictionary<Mode, string>()
        {
            {Mode.Reserved, "Reserved"},
            {Mode.SymmetricActive, "Symmetric active"},
            {Mode.SymmetricPassive, "Symmetric passive"},
            {Mode.Client, "Client"},
            {Mode.Server, "Server"},
            {Mode.Broadcast, "Broadcast"},
            {Mode.ReservedNTPControl, "Reserved for NTP control message"},
            {Mode.ReservedPrivate, "Reserved for private use"},
        };

        private static readonly Dictionary<Stratum, string> StratumDictionary = new Dictionary<Stratum, string>()
        {
            {Stratum.Primary, "1, Primary reference (e.g. radio clock)"},
            {Stratum.Secondary, "2, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary3, "3, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary4, "4, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary5, "5, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary6, "6, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary7, "7, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary8, "8, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary9, "9, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary10, "10, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary11, "11, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary12, "12, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary13, "13, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary14, "14, Secondary reference (via NTP or SNTP)"},
            {Stratum.Secondary15, "15, Secondary reference (via NTP or SNTP)"},
            {Stratum.Unspecified, "Unspecified or unavailable"}
        };

        private static readonly Dictionary<ReferenceIdentifier, string> RefererenceIdentifierDictionary = new Dictionary<ReferenceIdentifier, string>()
        {
            {SNTP.ReferenceIdentifier.ACTS, "NIST dialup modem service"},
            {SNTP.ReferenceIdentifier.CHU, "Ottawa (Canada) Radio 3330, 7335, 14670 kHz"},
            {SNTP.ReferenceIdentifier.DCF, "Mainflingen (Germany) Radio 77.5 kHz"},
            {SNTP.ReferenceIdentifier.GOES, "Geostationary Orbit Environment Satellite"},
            {SNTP.ReferenceIdentifier.GPS, "Global Positioning Service"},
            {SNTP.ReferenceIdentifier.LOCL, "Uncalibrated local clock used as a primary reference for a subnet without external means of synchronization"},
            {SNTP.ReferenceIdentifier.LORC, "LORAN-C radionavigation system"},
            {SNTP.ReferenceIdentifier.MSF, "Rugby (UK) Radio 60 kHz"},
            {SNTP.ReferenceIdentifier.OMEG, "OMEGA radionavigation system"},
            {SNTP.ReferenceIdentifier.PPS, "Atomic clock or other pulse-per-second source individually calibrated to national standards"},
            {SNTP.ReferenceIdentifier.PTB, "PTB (Germany) modem service"},
            {SNTP.ReferenceIdentifier.TDF, "Allouis (France) Radio 164 kHz"},
            {SNTP.ReferenceIdentifier.USNO, "U.S. Naval Observatory modem service"},
            {SNTP.ReferenceIdentifier.WWV, "Ft. Collins (US) Radio 2.5, 5, 10, 15, 20 MHz"},
            {SNTP.ReferenceIdentifier.WWVB, "Boulder (US) Radio 60 kHz"},
            {SNTP.ReferenceIdentifier.WWVH, "Kaui Hawaii (US) Radio 2.5, 5, 10, 15 MHz"},
        };

        #endregion Dictionaries
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO Dave Meadowcroft
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions