Click here to Skip to main content
Click here to Skip to main content

Is this computer's clock slow or is it just me?

, 16 Oct 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
How to request and parse data from various time servers using the NTP, DAYTIME, and TIME protocols.

Introduction

Time servers, atomic clocks, time protocols, and other such items have been around for a number of years. Tying them altogether somehow such that you could sit at your computer and get the "correct" time was nothing short of cool. As technology goes, atomic watches, some costing less than dinner at a nice restaurant, have been available for a few years now. My last timepiece was one such watch. I guess it's only a matter of time before your VCR is capable of fetching the correct time thus rendering the flashing 12:00 a thing of the past!

There are three main "time" protocols in use today: the Network Time Protocol (RFC 1305), the Daytime Protocol (RFC 867), and the Time Protocol (RFC 868). The former is the most widely used, and the latter two are being phased out. This article will show code examples for handling each.

There really isn't anything new in this article that hasn't been discussed elsewhere. I was mainly just interested in having a single location for all three approaches, and to show the basic code differences in "parsing" the data that comes back from the time servers. Even though I provided a very basic (read: cheesy) GUI just as a means of presentation, the intent here would be for you to take this information and create something whiz-bang with it!

Each of the three examples is a page on a property sheet. For each page/tab, a 15-second timer is created to handle the polling intervals. A single poll would have been sufficient, however. In the timer function for each, a secondary thread is created to query the time server. This is so the UI is not frozen, waiting on the communication with the time server to finish.

Sockets

When I first started on this exercise, I just used the Windows Sockets API. I did not want MFC to hide the details from me at first. In the simplest sense, using a socket consists of creating the socket object and binding it to a provider, connecting to a socket on some port at some address, and then receiving data from the connected socket. Code for this looks like:

SOCKET rSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET != rSocket)
{
    SOCKADDR_IN rSocketAddr     = { 0 };
    rSocketAddr.sin_family      = AF_INET;
    rSocketAddr.sin_addr.s_addr = inet_addr(<host>);
    rSocketAddr.sin_port        = htons(<port>);

    int nResult = connect(rSocket, (LPSOCKADDR) &rSocketAddr, sizeof(rSocketAddr));
    if (ERROR_SUCCESS == nResult)
        int nBytes = recv(rSocket, <buffer>, <size_of_buffer>, 0);
}

If any of these steps fails, we can use WSAGetLastError() to help track down why. A common problem is forgetting to call WSAStartup(). With that working correctly, I replaced the Windows Sockets API with the MFC equivalent, namely the CSocket class. We now have:

CSocket rSocket;
if (rSocket.Create())
{
    if (rSocket.Connect(<host>, <port>))
        int nBytes = rSocket.Receive(<buffer>, <size_of_buffer>);
}

As you can see, address, family, and protocol have default values. They are AF_INET, SOCK_STREAM, and IPPROTO_TCP, respectively. Not a whole lot of savings in terms of code or complexity.

Another point worth mentioning is that recv() may not receive all of the data it requested in one packet. A more robust implementation would need to use a loop construct to ensure that all bytes requested have been received before continuing.

Daytime Protocol

To use this protocol, which has been around since about 1983, we connect to port 13 of host 129.6.15.28 (time-a.nist.gov). Other hosts exist, too. After connecting, the time server sends back a 49-51 character string in the form:

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

You can read about the different fields at http://tf.nist.gov/service/its.htm. I have no explanation for the leading and trailing 0xA characters in the string. A user-defined message is posted to the primary thread, passing this string of characters as the LPARAM argument. Using PostMessage() is important because we do not want the secondary thread interacting with any of the UI controls owned by the primary thread. That's just a recipe for a deadlock situation. This is done via:

char *pszBuffer = new char[64];
int nBytes = rSocket.Receive(pszBuffer, 64);
if (nBytes > 0)
{
    pszBuffer[nBytes] = '\0';
    pDlg->PostMessage(UWM_INSERT_ITEM, 0, (LPARAM) pszBuffer);
}

In the function handling the user-defined message, the pointer is cast back to a char type. Since the date and time are in a standard format, we can use COleDateTime::ParseDateTime() to take care of the actual parsing for us. Because only the last two digits of the year are sent, "20" is prepended to the year. We also need to adjust for the local time zone. A second COleDateTime object is created that represents the current date and time. To see how our computer's clock compares to the time of the atomic clock, it's just a matter of taking the difference between the two. We'll use a COleDateTimeSpan object for that. At this point, we could change our computer's clock if necessary, or just display the differences on the screen. Also, since we're done with the char pointer that was allocated earlier, it can now be deleted. The code for all this looks like:

char            *pszBuffer = (char *) lpParam;
CString         pstr = pszBuffer;
COleDateTime    timeRemote;
SYSTEMTIME      st1, 
                st2;

// the date/time is in YY-MM-DD HH:MM:SS format
timeRemote.ParseDateTime(_T("20") + pstr.Mid(7, 17));

timeRemote.GetAsSystemTime(st1);
SystemTimeToTzSpecificLocalTime(&m_tzi, &st1, &st2);

timeRemote = st2;
COleDateTime timeLocal = COleDateTime::GetCurrentTime();

int nItem = m_lcTimes.InsertItem(0, timeRemote.Format(_T("%c")));
m_lcTimes.SetItemText(nItem, 1, timeLocal.Format(_T("%c")));
    
COleDateTimeSpan timeSpan = timeRemote - timeLocal;
m_lcTimes.SetItemText(nItem, 2, timeSpan.Format(_T("%H:%M:%S")));

delete pszBuffer;

Time Protocol

To use this protocol, we connect to port 37 of host 129.6.15.29 (time-b.nist.gov). After connecting, the time server sends back a 32-bit timestamp containing the time in UTC seconds since January 1, 1900. Since the value is based on 1900 (the epoch), we must subtract 70 years worth of seconds from it so the COleDateTime constructor that takes a time_t argument will behave properly. Next, a time_t pointer is created, and a user-defined message is posted to the primary thread, passing the pointer as the LPARAM argument. This is done via:

UINT uBuffer;
if (rSocket.Receive(&uBuffer, sizeof(UINT)) > 0)
{
    time_t *t = new time_t(ntohl(uBuffer) - 0x83aa7e80);
    pDlg->PostMessage(UWM_INSERT_ITEM, 0, (LPARAM) t);
}

Instead of sending the UINT variable in the message, we create a time_t variable instead. Otherwise, the casting from 32-bit to 64-bit will cause a bit of grief. In the function handling the user-defined message, we cast the LPARAM value back to a time_t pointer and pass its value to the COleDateTime constructor. The code for all this looks like:

time_t          *t = (time_t *) lpParam;
COleDateTime    timeLocal = COleDateTime::GetCurrentTime(),
                timeRemote(*t);
 
int nItem = m_lcTimes.InsertItem(0, timeRemote.Format(_T("%c")));
m_lcTimes.SetItemText(nItem, 1, timeLocal.Format(_T("%c")));

COleDateTimeSpan timeSpan = timeRemote.m_dt - timeLocal.m_dt;
m_lcTimes.SetItemText(nItem, 2, timeSpan.Format(_T("%H:%M:%S")));

delete t;

Network Time Protocol

This is the most commonly used Internet time protocol, and the one that provides the best performance. To use it, we connect to port 123 of some time server. There are roughly 1,846 of these servers available. You can go to the NTP Pool Project for more. Rather than pick one specific server to connect to, I opted for us.pool.ntp.org instead so that the next available time server (more on this below) responds to my query. Whichever you opt for, remember that many of the time servers are provided by volunteers, and almost all of them are really file or mail or webservers which just happen to also run NTP.

After connecting, we need to send a small bit of information (NTP control message) to the time server so that it can respond. The first three bits of the message is the NTP version number. Although 4 is the most recent, we'll use 3 to avoid the extra data. The next three bits of the message is the mode of the NTP control message, currently 6. The balance of the message bits is 0. The code for this looks like:

NTP_Packet NTP_Send = { 0 }; // NTP_Packet is described below
NTP_Send.nControlWord = 0x1B;
rSocket.Send(&NTP_Send, sizeof(NTP_Send));

Once received, the time server will send back a 60-byte timestamp containing the time in UTC seconds since January 1, 1900. The structure to receive the first 48 bytes of the response looks like:

struct NTP_Packet
{
    union
    {
        struct _ControlWord
        {
            unsigned int uLI:2;       // 00 = no leap, clock ok   
            unsigned int uVersion:3;  // version 3 or version 4
            unsigned int uMode:3;     // 3 for client, 4 for server, etc.
            unsigned int uStratum:8;  // 0 is unspecified, 1 for primary reference system, 
                                      // 2 for next level, etc.
            int nPoll:8;              // seconds as the nearest power of 2
            int nPrecision:8;         // seconds to the nearest power of 2
        };

        int nControlWord;             // 4
    };

    int nRootDelay;                   // 4
    int nRootDispersion;              // 4
    int nReferenceIdentifier;         // 4

    __int64 n64ReferenceTimestamp;    // 8
    __int64 n64OriginateTimestamp;    // 8
    __int64 n64ReceiveTimestamp;      // 8

    int nTransmitTimestampSeconds;    // 4
    int nTransmitTimestampFractions;  // 4

    // 12 more bytes here
};

The field that we are interested in for this exercise is nTransmitTimestampSeconds, and since the value is based on 1900, we again must subtract 70 years worth of seconds from it so the COleDateTime constructor will behave properly. Next, a time_t pointer is created, and a user-defined message is posted to the primary thread, passing the pointer as the LPARAM argument. The code for this looks like:

NTP_Packet NTP_Recv;
if (rSocket.Receive(&NTP_Recv, sizeof(NTP_Recv)) > 0)
{
    time_t *t = new time_t(ntohl(NTP_Recv.nTransmitTimestampSeconds) - 0x83aa7e80);
    pDlg->PostMessage(UWM_INSERT_ITEM, 0, (LPARAM) t);
}

In the function handling the user-defined message, we cast the LPARAM value back to a time_t pointer and pass its value to the COleDateTime constructor. The last bit of code looks like:

time_t          *t = (time_t *) lpParam;
COleDateTime    timeLocal = COleDateTime::GetCurrentTime(),
                timeRemote(*t);

int nItem = m_lcTimes.InsertItem(0, timeRemote.Format(_T("%c")));
m_lcTimes.SetItemText(nItem, 1, timeLocal.Format(_T("%c")));

COleDateTimeSpan timeSpan = timeRemote - timeLocal;
m_lcTimes.SetItemText(nItem, 2, timeSpan.Format(_T("%H:%M:%S")));

delete t;

Which Time Server Responded?

Earlier, I mentioned that us.pool.ntp.org is simply a pool of NTP time servers. While not necessary, you can find out which host actually responded to the request, by using getnameinfo(). The code I used for this looks like:

char szHost[NI_MAXHOST];
getnameinfo((sockaddr *) &server_addr, 
            sizeof(server_addr), szHost, sizeof(szHost), NULL, 0, 0);

TRACE(_T("Host: %S\n"), szHost);
TRACE(_T("IP address: %S\n"), inet_ntoa(server_addr.sin_addr));

The MFC equivalent to this is only slightly more involved:

CString strPeer;
UINT uPeer;
rSocket.GetPeerName(strPeer, uPeer);

ULONG ulAddr = inet_addr(strPeer);
HOSTENT *hostent = gethostbyaddr((const char *) &ulAddr, sizeof(ULONG), AF_INET);

TRACE(_T("Host: %s\n"), hostent->h_name);
TRACE(_T("IP address: %s\n"), strPeer);

Changes from VS6

This exercise was first created with VS6 (the compiler I use on a daily basis). When I got it all working, I moved it over to my machine that had VS2005 on it. I suspected it would have a few Unicode-related issues to correct. Much to my enjoyment, it compiled fine. A successful run was not meant to be, however. After the timer event fired (15 seconds), I was presented with an assertion in some MFC file dealing with CAsyncSocket methods. What? After about 45 minutes, I narrowed it down to the CTimeDlg::OnInsertItem() method. It seems that time_t was now a 64-bit type (unless _USE_32BIT_TIME_T is defined), but I was only passing it a 32-bit value. After the cast, the most significant 32 bits (high DWORD) had some value like 0xfdfdfdfd. This can easily be reproduced with:

unsigned int *u32 = new unsigned int;
*u32 = 0x12345678;
unsigned __int64 *u64 = (unsigned __int64 *) u32;

When that value was then sent to the COleDateTime constructor, it created an invalid object, which resulted in the aforementioned assertion when I went to use that object. At a minimum, I should have checked the m_status member before using the object.

Extras

When using a property sheet, the framework automatically adds OK, Cancel, Apply, and Help buttons to the sheet. For this particular exercise, only the Cancel button is relevant. To address this, the OK, Apply, and Help buttons need to be removed. This is easily done in the sheet's OnInitDialog() method like:

CWnd *pWnd = GetDlgItem(IDHELP);
ASSERT(NULL != pWnd);
pWnd->ShowWindow(SW_HIDE);

pWnd = GetDlgItem(ID_APPLY_NOW);
ASSERT(NULL != pWnd);
pWnd->ShowWindow(SW_HIDE);

pWnd = GetDlgItem(IDOK);
ASSERT(NULL != pWnd);
pWnd->ShowWindow(SW_HIDE);

Now, the Cancel button needs to be moved to the right to fill the void left by those other buttons. We'll put it where the Help button was, using:

CRect rect;
GetDlgItem(IDHELP)->GetWindowRect(rect);
ScreenToClient(rect);
GetDlgItem(IDCANCEL)->MoveWindow(rect);

Epilogue

This was a fun exercise to work on, and one that sparked my interest in a similar one involving weather. Given my lack of experience with sockets and time servers, there's bound to be something I left out, included but failed to explain, or just flat out got wrong. Feel free to correct me.

Enjoy!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

DavidCrow
Software Developer (Senior) Pinnacle Business Systems
United States United States

The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.
 
HTTP 404 - File not found
Internet Information Services

Comments and Discussions

 
GeneralA little mistake PinmemberStephan Poirier16-Oct-09 16:43 
GeneralRe: A little mistake PinmvpDavidCrow20-Oct-09 3:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 16 Oct 2009
Article Copyright 2009 by DavidCrow
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid