Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

An SNTP Client for C# and VB.NET

4.93/5 (29 votes)
22 Jul 2009CPOL11 min read 168.1K   9K  
A complete overview and implementation of SNTP from a client perspective.
Demo application screenshot

Table of Contents

Overview

In my last article DateValidator using SNTP, I showed how to use the Simple Network Time Protocol (SNTP) to get the date and time from a server rather than relying on the local system time to check that the expiry date of an application hadn't been exceeded. The partial implementation of the protocol in that article was sufficient because of the low precision needed, but I felt I had 'cheated' a little, so this article puts that right by presenting a full implementation of an SNTP client, with the exception of the optional (and apart from in special circumstances, not needed) Key Identifier and Message Digest fields, plus I'm only considering unicast mode of operation (not anycast or multicast).

I have attached source code in both C# and VB. The C# code has been pretty extensively tested and should be bug free - let me know if it's not!

I'm not a VBer, so the VB code should be treated as a starting point only, but it seems to work OK and hasn't crashed for me yet. The Components project must be compiled with Remove integer overflow checks: On. If you find any problems and have a fix, let me know and I'll update the code accordingly, but please understand I am not supporting the VB version. It is here for your convenience only!

What is SNTP, and what can it do for me?

SNTP, as its name implies, is a protocol for transferring date and time information. The main purpose is for time synchronization. For example, Windows uses this (occasionally!) to keep your computer's clock updated, but it could be used on a LAN with one machine acting as a server, to make sure all client machines are perfectly 'synced' with the server, and therefore each other, in time critical applications. It can also be used for validating times as I did in my aforementioned article. In fact, any application that uses dates and/or times could find a use for SNTP. It uses UTC (Coordinated Universal Time) for all its data, and .NET conveniently provides methods to easily convert between UTC and local time. It uses UDP on port 123, but there are some (non standard servers) that operate using TCP/HTTP on different ports. As they are non standard, they are ignored here.

If you're not interested in the nitty-gritty, deep and dirty of SNTP, skip the rest of this section and move on.

SNTP is a simple system (in the normal unicast mode) that consists of one packet of bytes being sent by a client, and one packet then being received. Each packet consists of 48 bytes (68 if Key Identifier and Message Digest are used). The list below explains each byte's meaning.

NB: The RFCs list these in Big Endian format, whereas I'm using Little Endian as they are when we read them from .NET.

  • Byte 0: This contains three values.
    1. The Leap Indicator which is contained in bits 7 and 6. This indicates whether there is to be a leap second added or removed.
    2. The protocol Version Number to use in bits 5, 4, and 3. Version 3 and version 4 are in common usage although NTP version 4 has yet to get an RFC. Previous versions are now commonly considered obsolete.
    3. The Mode in the remaining bits 2, 1, and 0. In unicast mode (which is all I'm considering in this article), we set this to 3 to indicate that we are a client, and check that it is 4 on receipt to make sure the data has come from a server.
  • Byte 1:
  • The Stratum, or how far we are away from the primary reference source.

    The value 0 is unspecified (this is the actual clock source). 16 to 255 are reserved for future use. A stratum of 1 is considered a primary source such as an atomic clock, GPS, radio etc. If a server synchronizes itself with a stratum 1 server, it is a stratum 2 as it's one step more away. This carries on all the way up to 15.

  • Byte 2:
  • Poll Interval, the time in seconds between the server re-syncing with its source.

    To stop servers being overrun with constant requests, polling is recommended to be carried out infrequently. We need to remember this for our own clients and should probably not check the same server more than every 64 seconds (the recommended 'default' value). The actual time is calculated by 2 ^ value.

  • Byte 3:
  • Precision, the precision of the server's clock. This is calculated by 2 ^ value.

  • Bytes 4 - 7:
  • Root Delay, the round trip delay to the primary reference source (in stratum 1, if it's not already a stratum 1 server) from the server, and back again. This is a 32 bit fixed point value, 16 for the integer part and 16 for the fractional part giving fine precision.

  • Bytes 8 - 11:
  • Root Dispersion, the nominal error relative to the primary reference source. 32 bits as in Root Delay above.

  • Bytes 12 - 15:
  • Reference Identifier, this identifies the reference in a variety of ways depending on the version being used and the stratum. If it's a stratum 1 source, this is 4 characters identifying the type of clock. If it's a stratum 2 to 15 (secondary), then:

    • If version 3: Each byte represents an octet of the IP address of the server's reference source.
    • If version 4: This should be the integer 32 bits of the latest transmit timestamp of the reference source, although in all my tests, the IP address was here as in version 3!
  • Bytes 16 - 23:
  • Reference Timestamp, the time at which the server's clock was last corrected. This is a 64 bit fixed point value, 32 for the integer part and 32 for the fractional part giving extremely fine precision. In fact, this precision is far greater than can be handled in .NET which can be accurate to 10 nanoseconds at best.

  • Bytes 24 - 31:
  • Originate Timestamp, the time at which the request departed the client for the server. We don't set this in the client. Instead, we use the transmit timestamp, and the server copies this into the originate timestamp in its reply. 64 bits as in reference timestamp.

  • Bytes 32 - 39:
  • Receive Timestamp, the time at which the request arrived at the server. 64 bits as in reference timestamp.

  • Bytes 40 - 47:
  • Transmit Timestamp, the time at which the reply departed the server for the client, or the request departed the client for the server. 64 bits as in reference timestamp.

Here is a graphical version of all that!

|    Byte + 3   |    Byte + 2   |    Byte + 1   |    Byte + 0   |
 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Precision   |      Poll     |    Stratum    |LI | VN  |Mode |  0 - 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           Root Delay                          |  4 - 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Root Dispersion                        |  8 - 11
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Reference Identifier                     | 12 - 15
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                    Reference Timestamp (64)                   | 16 - 23
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                    Originate Timestamp (64)                   | 24 - 31
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                     Receive Timestamp (64)                    | 32 - 39
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                     Transmit Timestamp (64)                   | 40 - 47
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  Key Identifier (optional) (32)               | 48 - 51
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                                                               |
|                  Message Digest (optional) (128)              | 52 - 68
|                                                               |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

There is one further timestamp that is relevant but not stored in the packet, although I have created a property for this in the SNTPData class.

  • Destination Timestamp, the time at which the response packet is received by the client.

Delay calculation

The RFCs give the formulae for delay calculation as:

  • Roundtrip Delay = (Destination - Originate) - (Receive - Transmit)
  • Local Clock Offset = ((Receive - Originate) + (Transmit - Destination)) / 2

There are also properties for these in the SNTPData class.

The Code

The SNTPClient class/component is in the DaveyM69.Components namespace, and all the related classes are in DaveyM69.Components.SNTP. This compiles to Components.dll, which you will find in the Release directory in the download if you don't want to build it yourself. It uses .NET framework v2.

SNTPClient

The SNTPClient has a few properties to control how it behaves. You can set whether to update the local time, the NTP/SNTP version to use, and of course, the remote server to query along with a timeout value. The QueryServerAsync method is what starts the ball rolling. This creates a new worker thread which calls the private QueryServer method where the real work gets done.

C#
private QueryServerCompletedEventArgs QueryServer()
{
    QueryServerCompletedEventArgs result = 
              new QueryServerCompletedEventArgs();
    Initialize();
    UdpClient client = null;
    try
    {
        // Configure and connect the socket.
        client = new UdpClient();
        IPEndPoint ipEndPoint = RemoteSNTPServer.GetIPEndPoint();
        client.Client.SendTimeout = Timeout;
        client.Client.ReceiveTimeout = Timeout;
        client.Connect(ipEndPoint);

        // Send and receive the data, and save the completion DateTime.
        SNTPData request = SNTPData.GetClientRequestPacket(VersionNumber);
        client.Send(request, request.Length);
        result.Data = client.Receive(ref ipEndPoint);
        result.Data.DestinationDateTime = DateTime.Now.ToUniversalTime();

        // Check the data
        if (result.Data.Mode == Mode.Server)
        {
            result.Succeeded = true;

            // Call other method(s) if needed
            if (UpdateLocalDateTime)
            {
                UpdateTime(result.Data.LocalClockOffset);
                result.LocalDateTimeUpdated = true;
            }
        }
        else
        {
            result.ErrorData = new ErrorData(
                "The response from the server was invalid.");
        }
        return result;
    }
    catch (Exception ex)
    {
        result.ErrorData = new ErrorData(ex);
        return result;
    }
    finally
    {
        // Close the socket
        if (client != null)
            client.Close();
    }
}

First of all, we initialize the client, then connect to the server. We then send a request packet and wait for a response and save the time at which it was received. We then validate the data by simply checking the 3 bits of byte 0 to make sure it's set to server (4). If all is OK and the local date and time are to be updated, we call the necessary method. As you can see, all the results of the query, including any errors/exceptions (apart from threading exceptions which are allowed to escalate to the host application), are stored in a QueryServerCompletedEventArgs instance. This is passed back to the original thread and the QueryServerCompleted event is raised along with these arguments.

Here is the same method in VB:

VB.NET
Private Function QueryServer() As QueryServerCompletedEventArgs
    Dim result As QueryServerCompletedEventArgs = _
               New QueryServerCompletedEventArgs()
    Initialize()
    Dim client As UdpClient = Nothing
    Try
        ' Configure and connect the socket.
        client = New UdpClient()
        Dim ipEndPoint As IPEndPoint = RemoteSNTPServer.GetIPEndPoint()
        client.Client.SendTimeout = Timeout
        client.Client.ReceiveTimeout = Timeout
        client.Connect(ipEndPoint)

        ' Send and receive the data, and save the completion DateTime.
        Dim request As SNTPData = SNTPData.GetClientRequestPacket(VersionNumber)
        client.Send(request, request.Length)
        result.Data = client.Receive(ipEndPoint)
        result.Data.DestinationDateTime = DateTime.Now.ToUniversalTime()

        ' Check the data
        If result.Data.Mode = Mode.Server Then
            result.Succeeded = True

            ' Call other method(s) if needed
            If (UpdateLocalDateTime) Then
                UpdateTime(result.Data.LocalClockOffset)
                result.LocalDateTimeUpdated = True
            End If
        Else
            result.ErrorData = _
              New ErrorData("The response from the server was invalid.")
        End If
        Return result
    Catch ex As Exception
        result.ErrorData = New ErrorData(ex)
        Return result
    Finally
        If client IsNot Nothing Then
            ' Close the socket
            client.Close()
        End If
    End Try
End Function

In addition to the above, there is a static property Now (and the overloaded GetNow methods) which retrieves the current date and time from the server synchronously.

RemoteSNTPServer

This class is very simple and essentially just holds the host name and port of a server. I have included many servers in there as static readonly so they can easily be used in your code. You should ideally pick a server that is geographically close to you, and preferably stratum 1 or 2, although as all timestamps along the path are logged and delays are calculated accordingly to produce an offset, it shouldn't really make much difference.

SNTPData

This class represents the 48 (possibly 68) byte packet I covered above. Because it is really just a byte array, I have implemented conversion operators accordingly. Most of this class's operations are self explanatory. The only slightly complicated part was converting the 64 bit fixed point timestamps to System.DateTime and back again. I have to thank Luc Pattyn for his assistance with this! Once the problem was solved, the resulting methods are, in reality, pretty trivial. The code for these two methods is below, and should keep roughly 1 tick (0.00000001 second) accuracy, but obviously rounding errors, and the time it takes the system to perform time updates etc., make this precision unachievable, but it should be OK to within a few microseconds.

C#
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);
}

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

One method that is important in this class is the static GetClientRequestPacket. This method creates a new SNTPData instance, sets the mode and version number bits, and places the current system time (converted to UTC) in the transmit timestamp.

C#
internal static SNTPData GetClientRequestPacket(VersionNumber versionNumber)
{
    SNTPData packet = new SNTPData();
    packet.Mode = Mode.Client;
    packet.VersionNumber = versionNumber;
    packet.TransmitDateTime = DateTime.Now.ToUniversalTime();
    return packet;
}

Class Diagram

Here is a class diagram showing the public parts of the classes above that would typically be used by your application (to keep this compact, not everything is shown here).

Class diagram of main classes

In Use

I've tried to make sure this is as easy to use as possible. Instantiate a SNTPClient in your code (or drop one onto a Form as it also derives from System.Component), subscribe to the QueryServerCompleted event if you want to make sure it succeeds, or examine any of the data, and call QueryServerAsync. That is all you need to query the default server and update your system's date and time! I've included a demonstration application to show what I've included in the article, and a few other things that I haven't.

Conclusion

I think I've covered SNTP from the client's perspective, and I hope you find the SNTPClient useful. For my next article, I plan to create an NTP/SNTP server to complement this client.

References

Other Implementations

Valer BOCAN has already done an article, SNTP Client in C#. Although his article has high ratings, I felt that there was little by way of explanation in his article, and I found a few problems in his code. Therefore, in my opinion, a more complete/in depth article and code was in order. His code was used to help me understand some of the vagueness in the RFCs by studying his implementation, and obviously there are some similarities as we are implementing the same protocol, but there is no plagiarism!

Credits

  • Luc Pattyn for assisting me in solving the issues I had in converting the timestamps, his help with a couple of problems I had porting this to VB.NET, and his continued presence on the forums.
  • Valer BOCAN for his existing article.
  • Sarah (my 'significant other') for losing me for a week whilst I studied the subject and wrote this article/code, and also for doing the boring job of proof reading to make sure it was intelligible!

History

  • 19th July, 2009: Initial version
  • 21th July, 2009: Article updated

License

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