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;
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_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; unsigned int uVersion:3; unsigned int uMode:3; unsigned int uStratum:8; int nPoll:8; int nPrecision:8; };
int nControlWord; };
int nRootDelay; int nRootDispersion; int nReferenceIdentifier;
__int64 n64ReferenceTimestamp; __int64 n64OriginateTimestamp; __int64 n64ReceiveTimestamp;
int nTransmitTimestampSeconds; int nTransmitTimestampFractions;
};
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!