How does a computer know what time it is?

Aaron Powell - Nov 1 '19 - - Dev Community

This is a post from the archives of my site, but I thought would be interesting still!

Recently I was working with a client who had a need to do some business processes on at certain times. So how do you know what the time is? We crack out System.DateTimeOffset don't we!

Well it turn out that there is a flaw when it comes to using that in the form of clock drift, think when your microwave and phone are no longer showing the same time, even though you are sure you set it to the same time the other day!

On Windows you can dive into your date time settings and there's an option to sync with a time-server. By default this is time.windows.com and I've seen that dialog plenty over my IT career but had never really paid much attention to how my computer knows it's 11.59pm 24th December, have you? So your computer will sync to this place, it'll sync periodically (you have some control over it, but not a lot), and this posed a problem for my client, clock drift could have a problem for them.

Well, what time IS it?

If we're establishing our problem we need to know what time it is, very accurately. Well there's a particular kind of clock which are highly specific, the atomic clock. Since these things are kind of big you can't really put it on your desk so let's think of the next thing, how does Windows know what time it is? Well this happens using an atomic clock (or similar precision device like a GPS) and done via the Network Time Protocol (ntp). You connect to a ntp server which then connects to a server until you hit an atomic clock, or a stratum 0 time instrument.

Well ntp is pretty simple a protocol, you use a UDP socket and get a byte array, 48 characters in length, which has all the important information.

So let's implement a ntp client in F#.

Talking to the server

The first thing we're going to need to do is to connect our ntp server. I'm going to keep using time.windows.com as my sample, but there's plenty of different time servers, of varying stratum levels (time.windows.com is stratum 4, which means there are 4 off the source).

I'm going to resolve the IP of my server first, let's create a getEndPoint function:

let getEndPoint (server : string) =
Enter fullscreen mode Exit fullscreen mode

Next I'm going to use System.Net.Dns to get the IP address:

    let getEndPoint (server : string) =
        let address =
            Dns.GetHostEntryAsync(server)
Enter fullscreen mode Exit fullscreen mode

But this is an async method (and C# async) so we'll do some pipelining:

    let getEndPoint (server : string) =
        let address =
            Dns.GetHostEntryAsync(server)
            |> Async.AwaitTask
            |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode

This returns an IPHostEntry which then I can use to get the IPs for the host and I'll take the first IP from that:

    let getEndPoint (server : string) =
        let address =
            Dns.GetHostEntryAsync(server)
            |> Async.AwaitTask
            |> Async.RunSynchronously
            |> fun entry -> entry.AddressList
            |> fun list -> list.[0]
Enter fullscreen mode Exit fullscreen mode

Finally I'll create the endpoint connection using port 123 which is the standard ntp port:

    let getEndPoint (server : string) =
        let address =
            Dns.GetHostEntryAsync(server)
            |> Async.AwaitTask
            |> Async.RunSynchronously
            |> fun entry -> entry.AddressList
            |> fun list -> list.[0]
        new IPEndPoint(address, 123)
Enter fullscreen mode Exit fullscreen mode

Now we're ready to connect our ntp server via a UDP socket:

    let getTime (ipEndpoint : IPEndPoint) =
        let ntpData =
            [| for i in 0..47 ->
                match i with
                | 0 -> byte 0x1B
                | _ -> byte 0 |]

        let socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
        socket.Connect(ipEndpoint)
        socket.ReceiveTimeout <- 3000
        socket.Send(ntpData) |> ignore
        socket.Receive(ntpData) |> ignore
        socket.Close()
        ntpData
Enter fullscreen mode Exit fullscreen mode

F# makes generating the data packet array really easy and using a match means that we can set the various bytes as they need to be set (for example I'm initialising a version in the first byte). I should be setting the Originate Timestamp at bytes 32 - 39 which you could also do in the match but I'm a bit lazy (note: not doing this means it's harder to work out the time based on latency).

Extracting our data

Now I have a byte array that is full of useful information that has been returned from our ntp server, but it's in raw bytes and they aren't overly readable so we probably want to split that apart. I'm going to create a record type to represent it:

    type NtpResponse =
        { Stratum : int
          ReferenceTime : DateTime
          OriginateTime : DateTime
          ReceiveTime : DateTime
          TransmitTime : DateTime }
Enter fullscreen mode Exit fullscreen mode

Then we'll make a function to break apart our byte array

    let extractor (ntpData : byte[]) =
        { Stratum = int ntpData.[1]
          ReferenceTime = DateTime.Now
          OriginateTime = DateTime.Now
          ReceiveTime = DateTime.Now
          TransmitTime = DateTime.Now }
Enter fullscreen mode Exit fullscreen mode

The first bit that I'm extracting is the stratum, which tells us what level the server is. This information is useful if you're wanting to know how close a server is to the time source, as the further away from the time source the more network hops have occurred. We're also casting the value from a byte to an int, which in F# we need to be more explicit about than if we were doing it in C#

But the more important part of the data that we have is the date/time components, which are 8 bytes starting at 16, 24, 32 and 40 respectively, so let's look at how we can get them out of the array.

The 8 bytes that make up the timestamp are comprised of a 64 bit fixed point timestamp which we need break into the 32 bit integer value representing the seconds since Unix Epoch and 32 bit floating point value that is the precision of the timestamp.

To do this I'm going to create a function that takes the 8 bytes and gets me a timestamp:

    let extractTime ntpData position =
        let intPart = BitConverter.ToUInt32(ntpData, int position)
        let fracPart = BitConverter.ToUInt32(ntpData, int position + 4)
Enter fullscreen mode Exit fullscreen mode

We're using uint32 to represent the data that we're pulling out (since it could be a large number). But the next this we need to deal with is that the value is Big Endianness notation so it needs to be swapped for us to use. Well that function isn't too hard to write...

    let swapEndianness (x : uint64) =
        (((x &&& uint64 0x000000ff) <<< 24) + ((x &&& uint64 0x0000ff00) <<< 8) + ((x &&& uint64 0x00ff0000) >>> 8)
        + ((x &&& uint64 0xff000000) >>> 24))
Enter fullscreen mode Exit fullscreen mode

Now we can update our extractTime method with this to generate the miliseconds and create our DateTime object:

    let extractTime ntpData position =
        let intPart = BitConverter.ToUInt32(ntpData, int position)
        let fracPart = BitConverter.ToUInt32(ntpData, int position + 4)
        let ms =
            ((swapEndianness (uint64 intPart)) * uint64 1000)
            + ((swapEndianness (uint64 fracPart)) * uint64 1000) / uint64 0x100000000L
        (new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc)).AddMilliseconds(float ms)
Enter fullscreen mode Exit fullscreen mode

It's times like this I really miss the operator overloading of C#...

With our extractTime method working let's go back to completing the extractor:

    let extractor extractTime (ntpData : byte[]) =
        let extractor' = extractTime ntpData
        let referenceTime = extractor' 16
        let originateTime = extractor' 24
        let receiveTime = extractor' 32
        let transmitTime = extractor' 40

        { Stratum = int ntpData.[1]
          ReferenceTime = referenceTime
          OriginateTime = originateTime
          ReceiveTime = receiveTime
          TransmitTime = transmitTime }
Enter fullscreen mode Exit fullscreen mode

And with a bit of partial application we can bind the extractTime method to the ntpData so that it is simpler to invoke.

Finishing it all up

With all our functions written we can now work out what the time is:

    let time = getEndPoint "time.windows.com"
                |> getTime
                |> extractor extractTime

    printfn "The time is %s" (time.TransmitTime.ToString("dd/MM/yyyy hh:mm:ss.fffffff"))
Enter fullscreen mode Exit fullscreen mode

And we're done. Well mostly, I've left a few exercises to you dear reader if you'd like to finish this off:

  • Set the Originate Time in the data you send to the ntp server
  • Use the time the request was sent from the client with the time on the server to determine the real time without latency (using the clock synchronisation algorithm)
  • Extract the other parts from the response packet, like version, poll interval (so you know how often you can hit the server), etc

Conclusion

Time is all relative but with ntp we can work out just how relative it is. With a little bit of F# we can query a ntp server and get the time information back. Hopefully this has been a bit of fun looking at how one of the protocols we take for granted works.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .