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.
- The Leap Indicator which is contained in bits 7 and 6. This indicates whether there is to be a leap second added or removed.
- 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.
- 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.
private QueryServerCompletedEventArgs QueryServer()
{
QueryServerCompletedEventArgs result =
new QueryServerCompletedEventArgs();
Initialize();
UdpClient client = null;
try
{
client = new UdpClient();
IPEndPoint ipEndPoint = RemoteSNTPServer.GetIPEndPoint();
client.Client.SendTimeout = Timeout;
client.Client.ReceiveTimeout = Timeout;
client.Connect(ipEndPoint);
SNTPData request = SNTPData.GetClientRequestPacket(VersionNumber);
client.Send(request, request.Length);
result.Data = client.Receive(ref ipEndPoint);
result.Data.DestinationDateTime = DateTime.Now.ToUniversalTime();
if (result.Data.Mode == Mode.Server)
{
result.Succeeded = true;
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
{
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:
Private Function QueryServer() As QueryServerCompletedEventArgs
Dim result As QueryServerCompletedEventArgs = _
New QueryServerCompletedEventArgs()
Initialize()
Dim client As UdpClient = Nothing
Try
client = New UdpClient()
Dim ipEndPoint As IPEndPoint = RemoteSNTPServer.GetIPEndPoint()
client.Client.SendTimeout = Timeout
client.Client.ReceiveTimeout = Timeout
client.Connect(ipEndPoint)
Dim request As SNTPData = SNTPData.GetClientRequestPacket(VersionNumber)
client.Send(request, request.Length)
result.Data = client.Receive(ipEndPoint)
result.Data.DestinationDateTime = DateTime.Now.ToUniversalTime()
If result.Data.Mode = Mode.Server Then
result.Succeeded = True
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
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.
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.
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).
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