Introduction to NTP (2/2) | WebReference

Introduction to NTP (2/2)

To page 1current page
[previous]

Introduction to NTP

A Basic SNTP-Based Perl Script

The following script implements a basic SNTP interaction with a time server, towards the goal of signaling an alarm (in our case, simply displaying a message) at a specified time. The message will be displayed in a synchronized fashion; which means that if we run the script on more than one server and set it to display a message at a certain time (taking into account time zone differences) then that message will appear on all of the servers at the same moment--even if the system clocks of the various servers are all different in respect to one another. Admittedly this script has little use as is, but should serve to show you how NTP communications can be used in a Perl script to provide an accurate (in testing this script appeared to be typically accurate to well within half a second) synchronized time across two or more machines.

The code for our syncAlarm script is as follows. To run this script, you'll need Perl 5.8 or higher, and you'll also need to be able to communicate on port 123, the standard port used for NTP communications (contact your system administrator if you are unsure).

#!/usr/bin/perl
use strict;
use Time::HiRes;
use Time::Local;
# drop into the Net::NTP package, in
# order to force it to use Time::HiRes
# calls instead of core time()
package Net::NTP;
use Time::HiRes qw(time);
# now back to main, where we can now
# pull in Net::NTP
package main;
use Net::NTP;
# override the core time call
# to our own, which will add
# the adjusted time
use subs qw(time);
# this will hold the clock adjustment offset
my $LOCAL_CLOCK_OFFSET = undef;
# these will be passed to get_ntp_response
my $NTPSERVER = 'ntp1.cs.wisc.edu';
my $NTPPORT   = 123;
# some labels to make working
# with the ntp response simpler
my $RT = "Receive Timestamp";
my $OT = "Originate Timestamp";
my $TT = "Transmit Timestamp";
# check the arguments
die "Usage is: syncAlarm HH:MM:SS\n"
     unless ($ARGV[0]=~/^\d\d:\d\d:\d\d$/);
# first set the LOCAL_CLOCK_OFFSET
# via an NTP call
my %ntp=get_ntp_response($NTPSERVER,$NTPPORT);
$LOCAL_CLOCK_OFFSET =( (($ntp{$RT} - $ntp{$OT}) +
                         ($ntp{$TT} - $Net::NTP::CLIENT_TIME_RECEIVE))
                       / 2 );
# next retrieve the epoch of the
# specified alarm time. Assume today
# for the parameters not specified
my @today=localtime(time);
splice(@today,0,3,(reverse(split(/:/,$ARGV[0]))));
my $alarm_time=timelocal(@today);
# make sure the alarm is in the future
die "Oops! That time has already gone by!\n"
     if (time>$alarm_time);
# sleep until the desired time
Time::HiRes::sleep($alarm_time-&time);
# closing message
print "It is now: $ARGV[0]\n";
exit;
# returns our adjusted time, based on the
# information provided in the NTP response
sub time {
    return -1 unless (defined($LOCAL_CLOCK_OFFSET));
    return (&Time::HiRes::time()+$LOCAL_CLOCK_OFFSET);
}

Much of this code is straightforward, but a few points require further explanation.

Setup

First, note the inclusion of the Time::HiRes module. Normally, when we call sleep or time in perl, we receive reponses relegated to 1 second intervals. For our purposes, this is simply not accurate enough, since our goal is to be accurate within a fraction of a second (and if we're forced to use 1 second intervals, then consistent, fractional accuracy would be impossible to achieve). By using Time::HiRes, we have access to time and sleep calls (among several others) that are millisecond aware. Time::HiRes::time(), for example, returns a fractional epoch (dependent on the resolution of your machine's system clock), and Time::HiRes::sleep() allows you to pass in fractions of seconds, such as 30.32658.

Not only do we use Time::HiRes in our own script, but we additionally force the Net::NTP module (more in a moment on Net::NTP) to use it, as well. This is the point of the following code, which forces the time routine from Time::HiRes to be used in Net::NTP whenever Net::NTP retrieves the time:

# drop into the Net::NTP package, in
# order to force it to use Time::HiRes
# calls instead of core time()
package Net::NTP;
use Time::HiRes qw(time);
# now back to main, where we can now
# pull in Net::NTP
package main;
use Net::NTP;

Now when Net::NTP communicates with the NTP server, it will do so with the more accurate timestamps that Time::HiRes provides, instead of the second-bound core time implementation.

It is the Net::NTP module, which you can pick up from CPAN, which provides the bulk of our heavy lifting. Net::NTP exports the get_ntp_response function by default, which communicates with a specified NTP server, sending it our timestamp information, and returns a hash containing the various NTP packet fields as defined in RFC1350 and RFC2030. get_ntp_response provides numerical translations that allow our efforts to be more "perl-like." For example, the packet field names are spelled out in full exactly like the RFC (such as "Transmit Timestamp" and "Originate Timestamp," for example), and the timestamps themselves are presented as normal (albeit fractional) perl epochs.

Which Server?

To call get_ntp_response, it is necessary for us to designate a server and a communications port for the UDP communications. The port is easy; as port 123 has been designated as the standard port for use with NTP.

NTP servers are generally designated as Stratum 1 or Stratum 2 servers; with the number relating to the hierarchical distance the server is away from the actual time source. The servers that are directly connected to the time sources are called Stratum 1 servers; while the servers that synchronize their time from Stratum 1 servers are called Stratum 2 servers, and so on. In the vast majority of cases (and certainly in this case) a Stratum 2 server is more than adequate for your needs; as these are typically still extremely accurate (as they observe the full rigors of the full NTP specification) and they are readily available. A listing of available NTP servers can be found at http://ntp.isc.org/bin/view/Servers/WebHome. When selecting servers, be sure to honor the implementation requirements of the server you will be using; some servers, for example, require that you contact a person before using them. Also try to select a server close to you (close to the machine you will be synchronizing), as shorter, symmetrical (in terms of speed) communications generally tend to produce the most accurate results. For our purposes, we selected the time server operated by the University of Wisconsin-Madison at ntp1.cs.wisc.edu.

Having selected a server, it's time to retrieve our NTP packet and generate from it the offset that we will apply to our own time to bring it in sync with UTC:

# first set the LOCAL_CLOCK_OFFSET
# via an NTP call
my %ntp=get_ntp_response($NTPSERVER,$NTPPORT);
$LOCAL_CLOCK_OFFSET =( (($ntp{$RT} - $ntp{$OT}) +
                         ($ntp{$TT} - $Net::NTP::CLIENT_TIME_RECEIVE))
                       / 2 );

This calculation is exactly as noted in RFC2030, and has the advantage of taking into account the length of time the packets spent being delivered to and from the target server. Note, however, that the logic must assume a symmetrical response time (the same time to and from the target server); which is why smaller, symmetrical communications tend to work best.

Using the Offset

Now that we have a valid clock offset, we use it in our own time function so that the time returned from this point on in our application is automatically adjusted based on our NTP response. We override the core perl time function with our own by using the use subs pragma:

use subs qw(time);

To "set" the alarm, we simply call Time::HiRes::sleep(), passing to it the difference between our target alarm time (passed to the script on the command line as HH:MM:SS). Once the sleep expires, we display a message on the console and quit.

To test the script, try loading it one or more servers, preferably with a known, incorrect time, and set it to "go off" at the same alarm time. Then compare the results to the Java clock provided at http://nist.time.gov. In a perfect world, you should see your script(s) indicate the time just as the Java clock does. In reality, the transmission latencies between your server and the selected time server, and the Java clock itself (on my system it is within .2 seconds of accuracy) may cause the script to differ slightly from the Java clock. But in my testing, the two were never more than a second apart; and most of the difference could be readily accounted for by the fact that I have an ADSL connection.

Other Thoughts

As I mentioned before, this script isn't intended for production use (as is it's not all that useful anyway), but is instead intended to demonstrate a simple SNTP methodology implementation. If you were to expand this script to a more useful construct, note that you would (at least) need to account for time zone differences (which are ignored in the script); and you may also need to adjust other time-related system routines (or provide your own) such as gmtime. Additionally, you may find it appropriate to query several time servers, and average your results (a step closer to, but still a long ways away from a full NTP implementation); at the very least, you will need to incorporate some means of error checking to accommodate for the possibility of your initial NTP call failing.

Conclusion

Consider your specific needs when determining what type of synchronization is necessary for your implementation. If a highly accurate (within a few milliseconds) and graceful synchronization is necessary, then a full NTP implementation is called for. If, on the other hand, a fraction of a second (or up to a second) is reasonable, then the simpler to use SNTP methodology as described above is well up to the task. Either way, NTP provides the means for computers across the street or across the world to agree on the current time of day with a level of accuracy suitable for the majority of jobs.


To page 1current page
[previous]

Created: June 10, 2005
Revised: June 10, 2005

URL: http://webreference.com/programming/perl/ntp/2.html