/*
* -----------------------------------------------------------------------
* Copyright © 2013-2015 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (SntpConnector.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.clock;
import net.time4j.Moment;
import net.time4j.SystemClock;
import net.time4j.scale.TimeScale;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ServiceLoader;
/**
* <p>Connects to a modern time server using the NTP-protocol. </p>
*
* <p>This class needs a socket connection via the port 123. The exact
* configuration can be set in the constructors but can also be changed
* at runtime. It is recommended not to connect to the NTP-server during
* or near a leap second because the NTP-protocol only repeats such a
* timestamp causing ambivalences. </p>
*
* @author Meno Hochschild
* @since 2.1
* @doctags.concurrency {threadsafe}
*/
/*[deutsch]
* <p>Nimmt die Verbindung zu einem modernen Uhrzeit-Server gemäß
* dem NTP-Protokoll auf. </p>
*
* <p>Diese Klasse benötigt einen Internet-Zugang über
* Port 123 (NTP). Die genaue Konfiguration wird zunächst im
* Konstruktor festgelegt, kann aber zur Laufzeit geändert werden.
* Es wird empfohlen, nicht während oder nahe einer Schaltsekunde
* zum NTP-Server zu verbinden, weil das NTP-Protokoll solch einen
* Zeitstempel nur wiederholt und sich somit hier ambivalent zeigt. </p>
*
* <p>Die Physikalisch-Technische Bundesanstalt in Braunschweig (PTB),
* die dort eine Atomuhr betreibt, benötigt als Adresse den Wert
* "ptbtime1.ptb.de" und das Protokoll NTP4. Eine Alternative
* ist die Adresse "ptbtime2.ptb.de". </p>
*
* @author Meno Hochschild
* @since 2.1
* @doctags.concurrency {threadsafe}
*/
public class SntpConnector
extends NetTimeConnector<SntpConfiguration> {
//~ Statische Felder/Initialisierungen --------------------------------
private static final int MIO = 1000000;
//~ Instanzvariablen --------------------------------------------------
private volatile SntpMessage lastReply = null;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Creates a new instance which is configured by a
* {@code ServiceLoader}. </p>
*
* @throws IllegalStateException if no configuration could be found
* @see ServiceLoader
*/
/*[deutsch]
* <p>Konstruiert eine neue Instanz, die über einen
* {@code ServiceLoader} konfiguriert ist. </p>
*
* @throws IllegalStateException if no configuration could be found
* @see ServiceLoader
*/
public SntpConnector() {
super(initConfiguration());
}
/**
* <p>Creates a new instance which is configured by given argument. </p>
*
* @param ntc SNTP-configuration
*/
/*[deutsch]
* <p>Konstruiert eine neue Instanz, die wie angegeben
* konfiguriert ist. </p>
*
* @param ntc vorgesehene Konfiguration
*/
public SntpConnector(SntpConfiguration ntc) {
super(ntc);
}
/**
* <p>Creates a new instance which uses a default configuration
* using the specified NTP4-server. </p>
*
* <p>Example: </p>
*
* <pre>
* SntpConnector clock = new SntpConnector("ptbtime1.ptb.de");
* clock.connect();
* System.out.println(clock.currentTime());
* </pre>
*
* @param server NTP4-server
*/
/*[deutsch]
* <p>Konstruiert eine neue Instanz, die zum angegebenen NTP-Server
* verbindet. </p>
*
* <p>Beispiel: </p>
*
* <pre>
* SntpConnector clock = new SntpConnector("ptbtime1.ptb.de");
* clock.connect();
* System.out.println(clock.currentTime());
* </pre>
*
* @param server NTP4-server
*/
public SntpConnector(String server) {
super(new SimpleNtpConfiguration(server));
}
//~ Methoden ----------------------------------------------------------
/**
* <p>Returns the current time in milliseconds since the Unix epoch
* [1970-01-01T00:00:00,000Z]. </p>
*
* <p>UTC leap seconds are never counted. </p>
*
* @return count of milliseconds since UNIX-epoch without leap seconds
* @since 2.1
*/
/*[deutsch]
* <p>Liefert die aktuelle Zeit in Millisekunden seit dem Beginn der
* UNIX-Epoche, nämlich [1970-01-01T00:00:00,000Z]. </p>
*
* <p>Es handelt sich immer um eine Zeitangabe ohne UTC-Schaltsekunden. </p>
*
* @return count of milliseconds since UNIX-epoch without leap seconds
* @since 2.1
*/
public long currentTimeInMillis() {
if (!this.isRunning()) {
Moment m = this.currentTime();
return (
m.getPosixTime() * 1000
+ m.getNanosecond() / MIO
);
}
long millis = SystemClock.MONOTONIC.currentTimeInMillis();
return (millis + (this.getLastOffset(millis * 1000) / 1000));
}
/**
* <p>Returns the current time in microseconds since the Unix epoch
* [1970-01-01T00:00:00,000000Z]. </p>
*
* <p>UTC leap seconds are never counted. </p>
*
* @return count of microseconds since UNIX-epoch without leap seconds
* @since 2.1
*/
/*[deutsch]
* <p>Liefert die aktuelle Zeit in Mikrosekunden seit dem Beginn der
* UNIX-Epoche, nämlich [1970-01-01T00:00:00,000000Z]. </p>
*
* <p>Es handelt sich immer um eine Zeitangabe ohne UTC-Schaltsekunden. </p>
*
* @return count of microseconds since UNIX-epoch without leap seconds
* @since 2.1
*/
public long currentTimeInMicros() {
if (!this.isRunning()) {
Moment m = this.currentTime();
return (
m.getPosixTime() * MIO
+ m.getNanosecond() / 1000
);
}
long micros = SystemClock.MONOTONIC.currentTimeInMicros();
return (micros + this.getLastOffset(micros));
}
/**
* <p>Returns the last received message of the NTP-server. </p>
*
* @return server message or {@code null} if not yet received
* @since 2.1
* @see #connect()
*/
/*[deutsch]
* <p>Liefert die zuletzt erhaltene Nachricht des Servers. </p>
*
* @return Server-Nachricht oder {@code null}, wenn noch nicht empfangen
* @since 2.1
* @see #connect()
*/
public SntpMessage getLastReply() {
return this.lastReply;
}
@Override
protected Moment doConnect() throws IOException {
// Konfigurationswerte holen
final SntpConfiguration config = this.getNetTimeConfiguration();
short requestCount = config.getRequestCount();
if (requestCount <= 0) {
return SystemClock.MONOTONIC.currentTime();
}
String address = config.getTimeServerAddress();
int port = config.getTimeServerPort();
int timeout = config.getConnectionTimeout();
boolean version4 = config.isNTP4();
long pollInterval = config.getRequestInterval() * 1000;
// lokaler Offset-Mittelwert in Mikrosekunden
long sum = 0;
long averageOffset = 0;
// UDP-Socket öffnen
DatagramSocket socket = null;
try {
socket = new DatagramSocket();
socket.setSoTimeout(timeout * 1000);
for (int i = 1; i <= requestCount; i++) {
// Zeitanfrage formulieren
this.log(null, "Connecting NTP-Server, waiting for reply...");
InetAddress iaddr = InetAddress.getByName(address);
SntpMessage requestMessage = new SntpMessage(version4);
double transmitTS = requestMessage.getTransmitTimestamp();
byte version = requestMessage.getVersion();
byte[] data = requestMessage.getBytes();
// Zeitanfrage abschicken
DatagramPacket request =
new DatagramPacket(data, data.length, iaddr, port);
socket.send(request);
// Antwort abwarten
DatagramPacket reply = new DatagramPacket(data, data.length);
socket.receive(reply);
// Sofort eigenen Timestamp notieren
double destinationTimestamp = SntpMessage.getLocalTimestamp();
// Antwort auspacken
SntpMessage replyMessage =
new SntpMessage(reply.getData(), transmitTS, version);
this.lastReply = replyMessage;
if (this.isLogEnabled()) {
this.log("NTP-Server connected: ", replyMessage.toString());
}
// Annahme gleicher Netzlaufzeiten für Anfrage und Antwort
// round-trip-delay: (D - O) - (T - R) = 2 * Netzlaufzeit
// REAL-LOCAL-TIME = T + Netzlaufzeit = D + localClockOffset
double localClockOffset = (
replyMessage.getReceiveTimestamp()
- replyMessage.getOriginateTimestamp()
+ replyMessage.getTransmitTimestamp()
- destinationTimestamp
) / 2.0;
sum += (localClockOffset * MIO);
averageOffset = (sum / i);
if (replyMessage.getStratum() == 0) {
this.log("NTP-Server replied: ", "<kiss-o'-death>");
break;
} else if (
(i > 1)
&& (i < requestCount)
) {
try {
Thread.sleep(pollInterval);
} catch (InterruptedException ie) {
this.log(null, "NTP-Connection interrupted.");
break;
}
}
}
} finally {
if (socket != null) {
socket.close();
}
}
long micros = SystemClock.MONOTONIC.currentTimeInMicros() + averageOffset;
long seconds = micros / MIO;
int nanosecond = (int) ((micros % MIO) * 1000);
byte leapIndicator = this.lastReply.getLeapIndicator();
if (leapIndicator == 3) {
throw new IOException(
"Alarm condition: "
+ "NTP-Server is not synchronized with any clock source.");
} else if (leapIndicator == 1) {
this.log(null, "Positive leap second announced.");
} else if (leapIndicator == 2) {
this.log(null, "Negative leap second announced.");
}
return Moment.of(seconds, nanosecond, TimeScale.POSIX);
}
@Override
protected SntpConfiguration loadNetTimeConfiguration() {
final SntpConfiguration sc = super.loadNetTimeConfiguration();
short rcount = sc.getRequestCount();
int rinterval = sc.getRequestInterval();
if ((rcount < 0) || (rcount >= 1000)) {
throw new IllegalStateException("Wrong request count: " + rcount);
} else if (rinterval <= 0) {
throw new IllegalStateException("Wrong request interval.");
}
return sc;
}
@Override
protected Class<SntpConfiguration> getConfigurationType() {
return SntpConfiguration.class;
}
private static SntpConfiguration initConfiguration() {
ServiceLoader<SntpConfiguration> sl =
ServiceLoader.load(SntpConfiguration.class);
for (SntpConfiguration cfg : sl) {
if (cfg != null) {
return cfg;
}
}
throw new IllegalStateException("SNTP-configuration not found.");
}
//~ Innere Klassen ----------------------------------------------------
private static class SimpleNtpConfiguration
implements SntpConfiguration {
//~ Instanzvariablen ----------------------------------------------
private final String server;
//~ Konstruktoren -------------------------------------------------
SimpleNtpConfiguration(String server) {
super();
if (server == null) {
throw new NullPointerException("Missing time server address.");
}
this.server = server;
}
//~ Methoden ------------------------------------------------------
@Override
public String getTimeServerAddress() {
return this.server;
}
@Override
public int getTimeServerPort() {
return 123;
}
@Override
public int getConnectionTimeout() {
return DEFAULT_CONNECTION_TIMEOUT;
}
@Override
public boolean isNTP4() {
return true;
}
@Override
public int getRequestInterval() {
return 60 * 4;
}
@Override
public short getRequestCount() {
return 1;
}
@Override
public int getClockShiftWindow() {
return 0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("SimpleNtpConfiguration:[server=");
sb.append(this.server);
sb.append(",port=");
sb.append(this.getTimeServerPort());
sb.append(']');
return sb.toString();
}
}
}