Friday, July 1, 2011

Internet Synchronized clock using C#. Part 1

Task – show clock that is synchronized with some internet time server.

In this part I’ll just try to get UTC time from the server and in Part 2 I’ll use this code to create simple clock.
Some search pointed me to the http://tf.nist.gov/tf-cgi/servers.cgi page. It contains list of servers placed in US which provide UTC time on demand. Another page from that site (http://www.nist.gov/pml/div688/grp40/its.cfm) gives information about supported protocols. One of the is Network Time Protocol (RFC-1305) and another one is Daytime Protocol (RFC-867). Network Time Protocol (NTP) is a little bit more complicated and it provides more features. Daytime Protocol is simpler, but it should be just enough for this task.

This protocol is extremely simple – you should connect to the server using TCP (or UDP) protocol and port 13 and it will give you time string and connection get closed. All of the listed servers return date and time in the following format


JJJJJ YR-MO-DA HH:MM:SS TT L H msADV UTC(NIST) OTM

We are interested in following members from this string:
YR-MO-DA – current date
HH:MM:SS – current time
H – indicating server status/health. 0 – means that server is healthy, the rest will introduce some error (for more details visit http://www.nist.gov/pml/div688/grp40/its.cfm)

This information is enough to get UTC time, so lets start.

First thing we need is to connect to the server and get results from it. Everything we need is to connect to port 13 and then read data that server return and convert it using ASCII encoding. The method which does this may look like this:

  1. private static string GetTimeString( string serverName )
  2. {
  3.   string result = string.Empty;
  4.  
  5.   try
  6.   {
  7.     TcpClient client = new TcpClient();
  8.     client.Connect( serverName, 13 );
  9.  
  10.     Stream stream = client.GetStream();
  11.     int BufferSize = 100;
  12.     byte[] buffer = new byte[ BufferSize ];
  13.     int iReadCount;
  14.  
  15.     while( ( iReadCount = stream.Read( buffer, 0, BufferSize ) ) > 0 )
  16.     {
  17.       result += Encoding.ASCII.GetString( buffer, 0, iReadCount );
  18.     }
  19.  
  20.     stream.Dispose();
  21.     client.Close();
  22.  
  23.   }
  24.   catch
  25.   {
  26.     result = null;
  27.   }
  28.  
  29.   return result;
  30. }

I put everything inside try-catch block because we don’t need it to crash if server is unavailable or any other reason.

Now we are able to get time string from the server and next step is to parse it. The simplest way to do this is just get date and time part from the server and make DateTime.ParseExact method with appropriate format string to parse it. And don’t forget to check health status. Let’s do it:

  1. private static DateTime ParseTimeString(string dateTimeString)
  2. {
  3.   try
  4.   {
  5.     //55741 11-06-29 11:58:55 50 0 0 629.4 UTC
  6.     dateTimeString = dateTimeString.Trim();
  7.     string[] dateTimeParts = dateTimeString.Split(' ');
  8.  
  9.     DateTime result = ( dateTimeParts[ 5 ] == "0" ) ?
  10.       DateTime.ParseExact(dateTimeParts[1] + ' ' + dateTimeParts[2], "yy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) :
  11.       DateTime.MinValue;
  12.     return result;
  13.   }
  14.   catch
  15.   {
  16.     return DateTime.MinValue;
  17.   }
  18. }

Now we can get current UTC time from one server, but who said that it is always available? Of course no, we are in the internet and everything could happen. So we should try to use at least several servers. To do this we will just have to iterate through servers list and try to get time from at least someone. First of all we need some definitions:

  1. /// <summary>
  2. /// Number of allowed iterations
  3. /// </summary>
  4. private const int IterationCount = 1000;
  5. /// <summary>
  6. /// List of time servers taken from http://tf.nist.gov/tf-cgi/servers.cgi
  7. /// </summary>
  8. private static readonly string[] ServersList = new string[]
  9. {
  10.   "64.90.182.55",
  11.   "96.47.67.105",
  12.   "206.246.122.250",
  13. };

IterationCount stands for how many times we will try to connect to all servers in total.
I’ve put just three servers in the server list just for sample, you can get all servers from http://tf.nist.gov/tf-cgi/servers.cgi.

  1. public static DateTime GetCurrentUtcTime()
  2. {
  3.   string dateTime = null;
  4.   int index = 0;
  5.   int iteration = 0;
  6.   DateTime typedDateTime = DateTime.MinValue;
  7.  
  8.   do
  9.   {
  10.     do
  11.     {
  12.       dateTime = GetTimeString(ServersList[index]);
  13.       index++;
  14.       iteration++;
  15.  
  16.       if (index >= ServersList.Length)
  17.         index = 0;
  18.     }
  19.     while (string.IsNullOrEmpty(dateTime) && iteration < IterationCount);
  20.  
  21.     typedDateTime = ParseTimeString(dateTime);
  22.   }
  23.   while (typedDateTime == DateTime.MinValue && iteration < IterationCount);
  24.  
  25.   return typedDateTime;
  26. }

We are simply iterating through all servers one after another until we get correct value from any of them. When we have it, we will return it. If 1000 of those attempts will not succeed then we will return DateTime.MinValue as final result.

Generally, I think that it is good idea to make those calls to the servers asynchronous, and use first returned result, this could speed up this operation significantly in case of first sever (or some servers at the list top) overload or unavailable. Also we can use random number to get server which we want to try, in this case we decrease probability of overloading some server by large number of requests of our application (in case it will have millions or billions of running copies :) ). Also we may count average time returned by several servers, this could increase accuracy of our time. And we may count time between request start and getting the result to count this when getting final result. But we have the base where we can start doing something more appropriate to our needs, so feel free to add any of those or any other changes if necessary.
And, by the way, don’t call it too often, one of those references says that there should be at least 4 seconds between accessing some server.

No comments:

Post a Comment