/* * [MiniSNTP.java] * * Summary: handles SNTP Simple Network Time Protocol to ask the time of an atomic clock. * * Copyright: (c) 1999-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 7.3 2005-05-09 * 7.4 2005-07-31 ANT scripts, Javadoc, release source, change message to make button say "done" when complete, * add an about button. * 7.5 2005-07-31 new icon, bigger type. * 7.6 2006-02-05 new icon, bigger type. * 7.7 2006-03-05 reformat with IntelliJ and add Javadoc * 7.8 2007-05-24 IntelliJ inspector lint. * 7.9 2007-06-14 catch error if set time fails. * 8.0 2008-04-06 add build to title, tidy code, correct spelling errors. * 8.1 2008-08-18 use xx.pool.ntp.org to find a nearby time server. * 8.2 2008-09-08 use 3 different pool servers and two probes on each to get a better average. * 8.3 2008-09-23 fix problem with Microsoft C++ runtime library. */ package com.mindprod.setclock; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; import static java.lang.System.*; /** * handles SNTP Simple Network Time Protocol to ask the time of an atomic clock. *

* see RFC 1769 for a full description of * the datagram. The packet is written in big-endian binary format. Timestamps in the packets are in scaled unsigned * seconds since Jan 1, 1900 0:00, with 32 bits of integer and 32 bit of binary fraction. *

* set PC Clock from an atomic clock. * * @author Roedy Green, Canadian Mind Products * @version 8.3 2008-09-23 fix problem with Microsoft C++ runtime library. * @since 1999 */ public final class MiniSNTP { /** * Field DEBUGGING */ private static final boolean DEBUGGING = false; /** * bytes in data portion of datagram */ private static final int DATAGRAMLENGTH = 48; /** * port of remote time server */ private static final int REMOTEPORT = 123; /** * This magic number was provided by BigDate's TestDate. */ private static final long SECSBETWEN1900AND1970 = 2208988800L; /** * keep socket open for multiple datagrams read/write */ private DatagramSocket socket; /** * IP of remote time server */ private InetAddress remoteIP; /** * 2 bits 0 = no warning 1 = last minute has 61 seconds 2 = last minute has 59 seconds 3 = alarm condition. */ private byte leapIndicator; /** * 3 bits 3 = client created datagram 4 = server created datagram. */ private byte mode; // fields in datagram, both send and receive. /** * 8 bits, signed maximum interval between polls. power of two, interval in seconds 4 = 16 seconds 6 = 64 seconds */ private byte pollInterval; /** * 8 bits, signed precision of local clock, as power of 2 seconds -6 = 16 ms = 60Hz -7 = 8 ms -8 = 4 ms -9 = 2 ms * -10 = 1 millisecond -20 = 1 microsecond */ private byte precision; /** * 8 bits how many levels of indirection are we away from the primary time source. 0 = unavailable. */ private byte stratum; /** * 3 bits version number currently on version 3 */ private byte versionNumber; /** * 32 bits, e.g. 0-padded ASCII string reference identifier */ private int referenceID; /** * 32 bits signed, (16 bits fraction) total round trip delay in seconds */ private int rootDelay; /** * 32 bits signed, (16 bits fraction) error local-actual time in seconds, as computed by server */ private int rootDispersion; /** * 64 bits, timestamp time at which response was sent arrived back at the client. This is not part of the packet. */ private long destinationTimestamp; /** * 64 bits, timestamp Time that client sent original request for time. Does not properly get echoed back to us. We * must save a copy. */ private long originateTimestamp; /** * 64 bits, timestamp time at which request arrived at server. */ private long receiveTimestamp; /** * 64 bits, timestamp Time at which server's clock was last corrected. */ private long referenceTimestamp; /** * 64 bits, timestamp time at which response was sent from server. */ private long transmitTimestamp; /** * default constructor * * @param remoteIP = "216.66.157.8" or "www.setclock.com"."ntp.belnet.be" or e.g. "ca.pool.ntp.org" remote SNTP * time server. * * @throws java.net.SocketException if cannot connect. * @throws java.net.UnknownHostException if no such host. */ @SuppressWarnings( { "SameParameterValue" } ) private MiniSNTP( String remoteIP ) throws SocketException, UnknownHostException { // the DNS lookup is done ahead of time so it won't distort timings. this.remoteIP = InetAddress.getByName( remoteIP ); this.socket = new DatagramSocket(); this.socket.setSoTimeout( 10000 ); } // end constructor /** * Convert Java timestamp to SNTP format. * * @param javaTimestamp milliseconds since 1970-01-01 0:00. * * @return unsigned fractional seconds since 1900-01 1 0:00. */ private static long javaToSNTPTimestamp( long javaTimestamp ) { long secs = javaTimestamp / 1000 + SECSBETWEN1900AND1970; int millis = ( int ) ( javaTimestamp % 1000 ); int frac = ( int ) ( 0x100000000L * millis / 1000 ); // unsigned seconds in high order 32 bit, unsigned binary fractional // seconds in low order return ( secs << 32 ) | ( frac & 0xffffffffL ); } /** * Convert SNTP timestamp to Java format. * * @param sntpTimestamp unsigned fractional seconds since 1900-01 1 0:00. * * @return javaTimestamp milliseconds since 1970-01-01 0:00. */ private static long sntpToJavaTimestamp( long sntpTimestamp ) { long secs = ( sntpTimestamp >>> 32 ) - SECSBETWEN1900AND1970; // truncate high order seconds part off int frac = ( int ) sntpTimestamp; // round int millis = ( int ) ( ( ( frac * 1000L ) + 0x80000000L ) >>> 32 ); return secs * 1000L + millis; } /** * shutdown */ private void close() { socket.close(); } /** * debugging dump of state variables */ protected void dump() { if ( DEBUGGING ) { out.println( "leap: " + leapIndicator ); out.println( "version: " + versionNumber ); out.println( "mode: " + mode ); out.println( "stratum: " + stratum ); out.println( "poll: " + pollInterval ); out.println( "precision: " + precision ); out.println( "delay: " + rootDelay ); out.println( "dispersion: " + rootDispersion ); out.println( "refID:" + referenceID ); out.println( "ref ts: " + referenceTimestamp + " " + sntpToJavaTimestamp( referenceTimestamp ) ); out.println( "orig ts: " + originateTimestamp + " " + sntpToJavaTimestamp( originateTimestamp ) ); out.println( "receive ts : " + receiveTimestamp + " " + sntpToJavaTimestamp( receiveTimestamp ) ); out.println( "transmit ts: " + transmitTimestamp + " " + sntpToJavaTimestamp( transmitTimestamp ) ); out.println( "dest ts: " + destinationTimestamp + " " + sntpToJavaTimestamp( destinationTimestamp ) ); } } // end dump /** * receive datagram from server, giving time of day. * * @throws IOException */ @SuppressWarnings( { "JavaDoc", "JavaDoc", "JavaDoc", "JavaDoc", "ResultOfMethodCallIgnored" } ) private void receiveTimeFromServer() throws IOException { // R E C E I V E byte[] bai = new byte[ DATAGRAMLENGTH + 12 ]; DatagramPacket p = new DatagramPacket( bai, bai.length ); socket.receive( p ); // get freshest possible current UTC time from PC clock destinationTimestamp = javaToSNTPTimestamp( System.currentTimeMillis() ); if ( DEBUGGING ) { out.println( bai.length ); for ( byte aBai : bai ) { out.print( aBai + " " ); } out.println(); } if ( !p.getAddress().equals( remoteIP ) || p.getPort() != REMOTEPORT ) { throw new IOException( "datagram received from unauthorised ip" ); } // O P E N ByteArrayInputStream bais = new ByteArrayInputStream( bai ); DataInputStream dis = new DataInputStream( bais ); // R E A D byte b = dis.readByte(); leapIndicator = ( byte ) ( ( b >>> 6 ) & 0x3 ); versionNumber = ( byte ) ( ( b >>> 3 ) & 0x7 ); mode = ( byte ) ( b & 0x7 ); stratum = ( byte ) dis.readUnsignedByte(); pollInterval = dis.readByte(); precision = dis.readByte(); rootDelay = dis.readInt(); rootDispersion = dis.readInt(); referenceID = dis.readInt(); referenceTimestamp = dis.readLong(); /* * leave orginateTimeStamp as it was. It is not properly echoed back to * us */ /* bypass it */ dis.skip( 8 ); receiveTimestamp = dis.readLong(); transmitTimestamp = dis.readLong(); // C L O S E dis.close(); // leave socket open } // end receiveTimeFromServer // we don't bother with the authenticator stuff /** * send datagram to server, requesting time of day. * * @throws IOException */ @SuppressWarnings( { "JavaDoc" } ) private void requestTimeFromServer() throws IOException { // I N I T leapIndicator = 0; versionNumber = 3; mode = 3; stratum = 0; pollInterval = 6; precision = -10; // O P E N ByteArrayOutputStream baos = new ByteArrayOutputStream( DATAGRAMLENGTH ); DataOutputStream dos = new DataOutputStream( baos ); // W R I T E byte b = ( byte ) ( leapIndicator << 6 | versionNumber << 3 | mode ); dos.writeByte( b ); dos.writeByte( stratum ); dos.writeByte( pollInterval ); dos.writeByte( precision ); dos.writeInt( rootDelay ); dos.writeInt( rootDispersion ); dos.writeInt( referenceID ); dos.writeLong( referenceTimestamp ); // get freshest possible current UTC time from PC clock originateTimestamp = javaToSNTPTimestamp( System.currentTimeMillis() ); dos.writeLong( originateTimestamp ); dos.writeLong( receiveTimestamp ); dos.writeLong( transmitTimestamp ); byte[] bao = baos.toByteArray(); if ( DEBUGGING ) { out.println( bao.length ); for ( byte aBao : bao ) { out.print( aBao + " " ); } out.println(); } // S E N D DatagramPacket p = new DatagramPacket( bao, bao.length ); p.setAddress( remoteIP ); p.setPort( REMOTEPORT ); socket.send( p ); // C L O S E // postpone close as it may be time consuming dos.close(); // leave socket open } // end requestTimeFromServer /** * Calculate how far out local time is from what server says: Positive result means local clock is running, slow, it * requires a number added to it to give the correct time. A negative result means local clock is running fast. * * @param ntpServer domain or IP of ntp server no http:// on ntp:// on front e.g. "ca.pool.ntp.org" or "209.167 * .68.100" * * @return how many milliseconds fast the local PC clock is running. returns Long.MAX_VALUE if it cannot provide an * accurate correction. */ public static long correction( String ntpServer ) { try { // ntp.belnet.be 193.190.198.19 // see list of timeservers at // http://tycho.usno.navy.mil/clocks1.html stratum 1 // http://www.eecis.udel.edu/~mills/ntp/clock2.htm stratum 2 // For PC's we should not pester the highly accurate stratum 1 // clocks. MiniSNTP m = new MiniSNTP( ntpServer );// time server IP m.requestTimeFromServer(); m.receiveTimeFromServer(); m.close(); // calc difference in seconds/fractions of a second. long error = ( ( m.receiveTimestamp - m.originateTimestamp ) + ( m.transmitTimestamp - m.destinationTimestamp ) ) / 2; boolean negative; if ( error < 0 ) { error = -error; negative = true; } else { negative = false; } long secs = error >>> 32; // truncate high order seconds part off int frac = ( int ) error; // round int millis = ( int ) ( ( ( frac * 1000L ) + 0x80000000L ) >>> 32 ); long result = secs * 1000L + millis; if ( negative ) { result = -result; } return result; } catch ( IOException e ) { return Long.MAX_VALUE; } } // end correction /** * test harness * * @param args not used */ public static void main( String[] args ) { if ( DEBUGGING ) { out.println(); out.println( correction( "ca.pool.ntp.org" ) ); out.println(); } } // end main }