The SNTP Client

Looking inside the SNTP.GetTime Function.

The SNTP_Helper class function GetTime is the core function for formatting a SNTP timestamp petion and recovering the reply form the NTP server.

SNTP.GetTime Function

The first part of GetTime configures a suitable sntp frame, opens an UDP communication channel on port 123 to the NTP server, send the petition and awaits a reply. The parameters _bSNTPv4 is true if a version 4 frame is to be generated, otherwise a version 3. The parameter _bAuthorisation is et true if authentication is to be used, this import considerably more information into the SNTP frame. However for this to work there must be an agreed exchange of information with the NTP server and is specific to each NTP server. The user must enter into a formal agreement with those resposable for the server owners.

        public Boolean GetTime(ref UInt32 last_update_time, Boolean _bSNTPv4, Boolean _bAuthorisation)
        {
            Boolean bError = false;
            bSntpV4 = _bSNTPv4;
            bAuthorisation = _bAuthorisation;
            if (TimeServerSocket == null) return false;
            iLastErrorNumber = 0;
            //  Set up the SNTP frame
            NTPdata.FLAGS_LEAPINDICATOR = 3;    //  Unknown leap
            if (bSntpV4) NTPdata.FLAGS_VERSIONNUMBER = 4;       //  Latest version 4
            else NTPdata.FLAGS_VERSIONNUMBER = 3;               //  Previous version 3
            NTPdata.FLAGS_MODE = 3;             //  A client
            NTPdata.STRATUM = 16;               //  Unsynchronised
            NTPdata.POLL = 0x0A;                //  min/max log2 poll limit
            NTPdata.PRECISION = 0xC0;           //  log2(precision secs = 1)
            //  Here we use fixed size extension fields of 16 bytes. Identifier = 10, size = 16. This defined by default 
            //  root_delay and root_disppersion should be built up over time polls
            NTPdata.REF_TS_SECS = last_update_time;  //  Should be set to last time own clock was updated

            //  Set up the socket
            TimeServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            IPEndPoint ip_end_point = new IPEndPoint(NTP_Address, 123);
            TimeServerSocketConnectDone.Reset();
            bConnected = false;
            try
            {
                TimeServerSocket.BeginConnect(ip_end_point, new AsyncCallback(TimeServerSocketConnectEvent), TimeServerSocket);
                TimeServerSocketConnectDone.WaitOne(connect_timeout, false);
                if (bConnected)
                {
                    //  At least a connection has been achieved
                    if (last_update_time == 0) tm_initial = DateTime.UtcNow;
                    else tm_initial = TimeStampToDateTime(last_update_time);
                    Byte[] packet = NTPdata.Get_Frame(tm_initial, bSntpV4, bAuthorisation);
                    T_orig_timestamp = (((UInt64)NTPdata.ORIG_TS_SECS) << 32) + (UInt64)NTPdata.ORIG_TS_FRAC;
                    //  Send petition
                    TimeServerSocketSendDone.Reset();
                    bSent = false;
                    try
                    {
                        TimeServerSocket.BeginSend(packet, 0, packet.Length, SocketFlags.None, new AsyncCallback(TimeServerSocketSendEvent), TimeServerSocket);
                        TimeServerSocketSendDone.WaitOne(send_timeout, false);
                        if (bSent)
                        {
                            //  Listen for the reply
                            TimeServerSocketReceiveDone.Reset();
                            bytesReceived = 0;
                            bReceive = false;
                            try
                            {
                                // Create the state object.
                                StateObject state = new StateObject();
                                state.workSocket = TimeServerSocket;
                                // Begin receiving the data from the remote device.
                                TimeServerSocket.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(TimeServerSocketReceiveCallback), state);
                                TimeServerSocketReceiveDone.WaitOne(receive_timeout, false);
                                if (bReceive == false)
                                {
                                    iLastErrorNumber = 5;
                                    bError = true;
                                }
                            }
                            catch (Exception e)
                            {
                                receive_exception = e;
                                iLastErrorNumber = 6;
                                bError = true;
                            }
                        }
                        else
                        {
                            iLastErrorNumber = 3;
                            bError = true;
                        }
                    }
                    catch (Exception e)
                    {
                        send_exception = e;
                        iLastErrorNumber = 4;
                        bError = true;
                    }
                }
                else
                {
                    //  Unable to connect
                    iLastErrorNumber = 1;
                    bError = true;
                }
            }
            catch (Exception e)
            {
                connect_exception = e;
                iLastErrorNumber = 2;
                bError = true;
            }
            //  Finalise
            try
            {
                TimeServerSocket.Shutdown(SocketShutdown.Both);
                TimeServerSocket.Close();
            }
            catch (Exception e)
            {
                if (!bError)
                {
                    finalise_exception = e;
                    iLastErrorNumber = 7;
                    bError = true;
                }
            }
            if (bError) return false;

An UDP connection is made with TimeServerSocket.BeginConnect(..). The function TimeServerSocketConnectEvent is calledback and the event TimeServerSocketConnectDone raised if successfull, otherwise a timeout.

Then the frame is formulated using NTPdata.Get_Frame where the initial time is the time of the last petition. the frame is sent using TimeServerSocket.BeginSend with TimeServerSocketSendEvent as the callback event and TimeServerSocketSendDone event is raised if successfull otherwise timeout.

Finally the socket has to be placed in the Listen mode using TimeServerSocket.BeginReceive to await any reply from the NTP server. data is accumulated in the TimeServerSocketReceiveCallback callback function and the event TimeServerSocketReceiveDone set when a good packet has been received.

To avoid thread blocking callbacks are used to break the socket actions. The events are used to signal correct termination of the action or trigger a timeout error if the action ndoes not complete in the time allocated.

        private static void TimeServerSocketConnectEvent(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket client = (Socket)ar.AsyncState;

                // Complete the connection.
                client.EndConnect(ar);

                // Signal that the connection has been made.
                TimeServerSocketConnectDone.Set();
                bConnected = true;
            }
            catch (Exception e)
            {
                bConnected = false;
                connect_exception = e;
            }
        }

        private static void TimeServerSocketSendEvent(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket client = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.
                bytesSent = client.EndSend(ar);
                // Signal that all bytes have been sent.
                TimeServerSocketSendDone.Set();
                bSent = true;
            }
            catch (Exception e)
            {
                bSent = false;
                send_exception = e;
            }
        }

        private static void TimeServerSocketReceiveCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the state object and the client socket 
                // from the asynchronous state object.
                StateObject state = (StateObject)ar.AsyncState;
                Socket client = state.workSocket;
                // Read data from the remote device.
                Int32 bytesRead = client.EndReceive(ar);
                if (bytesRead > 0)
                {
                    // There might be more data, so store the data received so far.
                    state.Append(state.buffer, 0, bytesRead);
                    if (state.BytesReceived >= 48)
                    {
                        bReceive = true;
                        bytesReceived = state.BytesReceived;
                        response = new byte[state.BytesReceived];
                        response = state.GetBytes(state.BytesReceived); ;
                        // Signal that all bytes have been received.
                        TimeServerSocketReceiveDone.Set();
                    }
                    else
                    {
                        //  Get the rest of the data.
                        client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(TimeServerSocketReceiveCallback), state);
                    }
                }
                else
                {
                    // All the data has arrived; put it in response.
                    if (state.BytesReceived > 1)
                    {
                        bReceive = true;
                        bytesReceived = state.BytesReceived;
                        response = new byte[state.BytesReceived];
                        response = state.GetBytes(state.BytesReceived); ;
                    }
                    // Signal that all bytes have been received.
                    TimeServerSocketReceiveDone.Set();
                }
            }
            catch { bReceive = false; }
        }

        public class StateObject
        {
            // Client socket.
            public Socket workSocket = null;
            // Size of receive buffer.
            public const int BufferSize = 513;
            // Receive buffer.
            public Byte[] buffer = new Byte[BufferSize];
            public Byte[] received_buffer = new Byte[BufferSize];

            private Int32 append_position = 0;

            public StateObject()
            {
                append_position = 0;
            }

            public void Reset() { append_position = 0; }

            public void Append(Byte[] in_buffer, Int32 start_position, Int32 number_of_bytes_to_transfer)
            {
                //  Accumulates the bytes read from buffer into receive_buffer, limited by BufferSize
                if ((append_position + number_of_bytes_to_transfer) >= BufferSize) return;
                for (Int32 i = 0; i < number_of_bytes_to_transfer; i++) received_buffer[(append_position + i)] = in_buffer[i];
                append_position += number_of_bytes_to_transfer;
                return;
            }

            public Int32 BytesReceived {  get { return append_position; } }

            public Byte[] GetBytes(Int32 number_to_get)
            {
                Byte[] reply = new Byte[number_to_get];
                if (number_to_get >= BufferSize) return reply;
                for (Int32 i = 0; i < number_to_get; i++) reply[i] = received_buffer[i];
                return reply;
            }
        }

The received frame is returned in the response array as raw bytes along with a count of the bytes received.

Once a valid reply hase been obtained, then the time data and other information can be extracted.

            //  Decode the incoming data
            if (bytesReceived >= 48)
            {
                //  What appears to be a valid reply frame
                Ntp_Packet ntp_response = new Ntp_Packet(response);
                t_ref_timestamp = (((UInt64)ntp_response.REF_TS_SECS) << 32) + (UInt64)ntp_response.REF_TS_FRAC;
                t_orig_timestamp = (((UInt64)ntp_response.ORIG_TS_SECS) << 32) + (UInt64)ntp_response.ORIG_TS_FRAC;
                t_recv_timestamp = (((UInt64)ntp_response.RECV_TS_SECS) << 32) + (UInt64)ntp_response.RECV_TS_FRAC;
                t_tx_timestamp = (((UInt64)ntp_response.TX_TS_SECS) << 32) + (UInt64)ntp_response.TX_TS_FRAC;
                //  Set final arrival time at this destination
                UInt32 tx_secs = 0;
                UInt32 tx_frac = 0;
                Ntp_Packet.SNTP_TimeMark(ref tx_secs, ref tx_frac, DateTime.UtcNow);
                ntp_response.RECV_TX_SECS = tx_secs;
                ntp_response.RECV_TX_FRAC = tx_frac;
                t_arrival_timestamp = (((UInt64)tx_secs) << 32) + (UInt64)tx_frac;
                //  Sanity checks
                //  Duplicate if T_tx_timestamp = t-tx_timestamp
                if (T_tx_timestamp == t_tx_timestamp)
                {
                    iLastErrorNumber = 8;
                    return false;
                }
                T_tx_timestamp = t_tx_timestamp;
                //  Bogus if T_orig_timestamp != t_orig_timestamp NtpV4, NtpV3 origin  returned is 0
                if (bAuthorisation)
                {
                    //  We require a minimum frame length as a reply. Though if authentication not recognised there will be
                    //  a KeyID 
                    if (bytesReceived != 100)
                    {
                        if (bytesReceived > 48)
                        {
                            //  extract the keyID
                            Byte[] extra_bytes = new Byte[bytesReceived - 48];
                            for (Int32 j = 0; j < extra_bytes.Length; j++)
                            {
                                extra_bytes[j] = response[(j + 48)];
                                if (extra_bytes[j] < 0x20) extra_bytes[j] = (Byte)(0xA6 + j);
                            }
                            iLastErrorNumber = 12;
                            sLastErrorString[12] += System.Text.Encoding.UTF8.GetString(extra_bytes);
                            return false;
                        }
                        else
                        {
                            iLastErrorNumber = 9;
                            return false;
                        }
                    }
                }
                else
                {
                    if (T_orig_timestamp == t_orig_timestamp)
                    {
                        iLastErrorNumber = 10;
                        return false;
                    }

                }
                //  Completed sanity check
                T_ref_timestamp = t_ref_timestamp;
                T_recv_timestamp = t_recv_timestamp;
                theta = (((t_recv_timestamp - T_orig_timestamp) + (t_arrival_timestamp - t_tx_timestamp)) >> 1);
                delta = (t_arrival_timestamp - T_orig_timestamp) - (t_tx_timestamp - t_recv_timestamp);
                //  Correct the time and place in a DateTime object
                corrected_timestamp = t_tx_timestamp + theta;
                try
                {
                    tm_latest = TimeStampToDateTime(corrected_timestamp);
                }
                catch
                {
                    iLastErrorNumber = 11;
                    return false;
                }
                root_delay = ntp_response.ROOT_DELAY;
                root_dispersion = ntp_response.ROOT_DISPERSION;
                stratum = ntp_response.STRATUM;
                version = ntp_response.FLAGS_VERSIONNUMBER;
                precision = ntp_response.PRECISION;
                last_update_time = (UInt32)((corrected_timestamp& 0xFFFFFFFF00000000) >> 32);
                return true;
            }
            return false;

There is a need for a time correction to take into account the delay between sending the petition and receiving the reply. This correction is calculated from the 4 timestamps. There are two values of importance theta ( Ǝ ) and delta ( Ɗ ), where:

 Ǝ  =  1/2 * [(arrival at server timestamp - leave from client timestamp) + (leave from server timesatmp - arrival at client timestamp] 
 Ɗ = (arrival at client timestamp - leave from client timestamp) - (leave from server timestamp - arrival at server timestamp)
 corrected time = leave from server timestamp + Ǝ