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