Atomic Time in C#

Sometimes knowing the exact time is important. If you are trying to sync data between GPS or other values that are synced to satellites, getting the atomic time from an NIST server is required.

I created a method of getting the current time from one of several time servers at any time. You do this by calling:

AtomicTime.Now

The good thing about the code is that once it has the exact time from a server it no longer has to contact one of them because the code keeps track of the time since we originally got the time by using a Stopwatch.

First I created a static class so we can call it anywhere and at anytime.

public static class AtomicTime

Then we added a few private variables to help with getting the current time:

 private static DateTime _currentAtomicTime;
 private static bool _canConnectToServer = true;
 private static Stopwatch _timeSinceLastValue = new Stopwatch();
 private static readonly object Locker = new object();
 private static Countdown _countdown; //used to help ensure we get the fastest server

You'll see that I created a Countdown object to help us get the fastest server. Currently we are using the .NET 3.5 platform so we don't have access to a CountdownEvent from .NET 4.0 and higher. I got help for the following code from Joseph Albahari great instruction on Threading in C#.

public class Countdown
{
  readonly object _locker = new object();
  int _value;

  public Countdown() { }
  public Countdown(int initialCount) { _value = initialCount; }
  public void Signal() { AddCount(-1); }
  public void PulseAll()
  {
    lock (_locker)
    {
      _value = 0;
      Monitor.PulseAll(_locker);
    }
  }

  public void AddCount(int amount)
  {
    lock (_locker)
    {
      _value += amount;
      if (_value <= 0) Monitor.PulseAll(_locker);
    }
  }
  public void Wait()
  {
    lock (_locker)
      while (_value > 0)
        Monitor.Wait(_locker);
  }
}

The Countdown object helps us connect to multiple time servers at once and when we finally get the time, we can pulse the Countdown object and stop all the other threads. Now we create a way to get the current time from a server by passing in the name of the server. If the server is currently up and working, the value it returns needs to be parsed. There is some information about the return value which is RFC-867 format. I've commented in the code some of the places where I found examples on parsing the information. What wasn't clear however is how to handle the milliseconds portion of the time. I know we get the date and time down to the seconds. There seems to be a milliseconds portion that I handle by subtracting from the received date and time to help ensure we have the exact atomic time. As you can see in the following code, once we have a valid time, we can pulse the other threads and stop attempting to contact the time servers.

private static void GetDateTimeFromServer(string server)
  {
    if (_currentAtomicTime == DateTime.MinValue)
    {
      try
      {
        // Connect to the server (at port 13) and get the response
        string serverResponse;
        using (var reader = new StreamReader(new System.Net.Sockets.TcpClient(server, 13).GetStream()))
          serverResponse = reader.ReadToEnd();

        // If a response was received
        if (!string.IsNullOrEmpty(serverResponse) || _currentAtomicTime != DateTime.MinValue)
        {
          // Split the response string ("55596 11-02-14 13:54:11 00 0 0 478.1 UTC(NIST) *")
          //format is RFC-867, see example here: http://www.kloth.net/software/timesrv1.php
          //some other examples of how to parse can be found in this: http://cosinekitty.com/nist/
          string[] tokens = serverResponse.Replace("n", "").Split(' ');

          // Check the number of tokens
          if (tokens.Length >= 6)
          {
            // Check the health status
            string health = tokens[5];
            if (health == "0")
            {
              // Get date and time parts from the server response
              string[] dateParts = tokens[1].Split('-');
              string[] timeParts = tokens[2].Split(':');

              // Create a DateTime instance
              var utcDateTime = new DateTime(
              Convert.ToInt32(dateParts[0]) + 2000,
              Convert.ToInt32(dateParts[1]), Convert.ToInt32(dateParts[2]),
              Convert.ToInt32(timeParts[0]), Convert.ToInt32(timeParts[1]),
              Convert.ToInt32(timeParts[2]));

              //subject milliseconds from it
              if (Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator == "," && tokens[6].Contains("."))
                tokens[6] = tokens[6].Replace(".", ",");
              else if (Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator == "." && tokens[6].Contains(","))
                tokens[6] = tokens[6].Replace(",", ".");

              double millis;
              double.TryParse(tokens[6], out millis);
              utcDateTime = utcDateTime.AddMilliseconds(-millis);

              // Convert received (UTC) DateTime value to the local timezone
              if (_currentAtomicTime == DateTime.MinValue)
              {
                _currentAtomicTime = utcDateTime.ToLocalTime();
                _timeSinceLastValue = new Stopwatch();
                _timeSinceLastValue.Start();
                _countdown.PulseAll(); //we got a valid time, move on and no need to worry about results from other threads
              }
            }
          }
        }
      }
      catch ()
      {
        // Ignore exception and try the next server
      }
    }

    //let CountdownEvent know that we're done here
    _countdown.Signal();
  }

Once we are able to get the time from a server, we are able to I created a DateTime called Now to duplicate the name of DateTime.Now. If we have already retrieved the time from a server, we just have to add the length of time since we last retrieved and return the time. If we were unable to contact any server, we return with DateTime.MinValue and will handle that wherever we call AtomicTime.Now.

public static DateTime Now
{
  get
  {
    //found part of this code here: http://www.datavoila.com/projects/internet/get-nist-atomic-clock-time.html

    //we have attempted to connect to the server and had no luck, no need to try again
    if (_canConnectToServer == false)
      return DateTime.MinValue;

    //keep track so we don't have to keep connecting to the servers
    if (_currentAtomicTime != DateTime.MinValue)
    {
      _currentAtomicTime += _timeSinceLastValue.Elapsed;
      _timeSinceLastValue.Reset();
      _timeSinceLastValue.Start();
    }
    else
    {
      //ensure we aren't doing this multiple times from multiple locations
      lock (Locker)
      {
        if (_currentAtomicTime != DateTime.MinValue) //we got the time already, pass it along
        {
          _currentAtomicTime += _timeSinceLastValue.Elapsed;
          _timeSinceLastValue.Reset();
          _timeSinceLastValue.Start();
        }
        else
        {
          // Initialize the list of NIST time servers
          // http://tf.nist.gov/tf-cgi/servers.cgi
          var servers = new[]
          {
             "nist1-ny.ustiming.org",
             "nist1-nj.ustiming.org",
             "nist1-pa.ustiming.org",
             "nist1.aol-va.symmetricom.com",
             "nist1.columbiacountyga.gov",
             "nist1-atl.ustiming.org",
             "nist.expertsmi.com",
             "nisttime.carsoncity.k12.mi.us",
             "nist1-lnk.binary.net",
             "www.nist.gov",
             "utcnist.colorado.edu",
             "utcnist2.colorado.edu",
             "ntp-nist.ldsbc.edu",
             "nist1-lv.ustiming.org",
             "nist-time-server.eoni.com",
             "nist1.aol-ca.symmetricom.com",
             "nist1.symmetricom.com",
             "nist1-la.ustiming.org",
             "nist1-sj.ustiming.org"
          };

          // Try 5 servers in random order to spread the load
          var rnd = new Random();
          _countdown = new Countdown(5);
          foreach (string server in servers.OrderBy(s => rnd.NextDouble()).Take(5))
          {
            string server1 = server;
            var t = new Thread(o => GetDateTimeFromServer(server1));
            t.SetApartmentState(ApartmentState.STA);
            t.Start();
          }
          _countdown.Wait();
          if (_currentAtomicTime == DateTime.MinValue)
            _canConnectToServer = false;
        }
      }
    }

    return _currentAtomicTime;
  }
}
Show Comments