Skip to content

Instantly share code, notes, and snippets.

@tai2
Created April 26, 2013 11:32
Show Gist options
  • Select an option

  • Save tai2/5466578 to your computer and use it in GitHub Desktop.

Select an option

Save tai2/5466578 to your computer and use it in GitHub Desktop.
SNTP client. RFC 2030.
package net.tai2.test;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.PortUnreachableException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/*
* Timestampの1ビット目がゼロになった後(2036年頃)は、このモジュール使えない。
* (プロトコル上は利用可能だが、そのための処理をしていない)
*/
public class SntpClient {
// -----------------------------
// パブリック定数
// -----------------------------
public static int SNTP_VERSION = 4;
public static int SOCKET_TIMEOUT = 5000;
public static int INITIAL_RETRY_SPAN = 2000;
public static int RETRY_LIMIT = 3;
// -----------------------------
// プライベート定数
// -----------------------------
private static String TAG = "SntpClient";
// -----------------------------
// フィールド
// -----------------------------
private List<InetSocketAddress> server_list = new ArrayList<InetSocketAddress>();
private InetAddress local_address;
private int local_port;
private long source_time;
private long sync_time;
// -----------------------------
// パブリックメソッド
// -----------------------------
public SntpClient(List<InetSocketAddress> list) {
server_list = list;
}
public void setLocalAddress(InetAddress addr) {
local_address = addr;
}
public void setLocalPort(int port) {
local_port = port;
}
public boolean sync() {
boolean succeeded = false;
List<TryInfo> info_list = new ArrayList<TryInfo>();
for (InetSocketAddress address : server_list) {
info_list.add(new TryInfo(address));
}
for (;;) {
for (TryInfo info : info_list) {
if (!info.nomore) {
try {
sync(info.address, SOCKET_TIMEOUT);
succeeded = true;
} catch (PortUnreachableException e) {
info.span = info.span == 0 ? INITIAL_RETRY_SPAN : info.span * 2;
} catch (SocketTimeoutException e) {
info.span = info.span == 0 ? INITIAL_RETRY_SPAN : info.span * 2;
} catch (KissODethReceived e) {
info.nomore = true;
} catch (IOException e) {
info.nomore = true;
Log.e(TAG, "sync failed. cause=" + e.getMessage());
}
info.count++;
if (RETRY_LIMIT <= info.count) {
info.nomore = true;
}
}
}
boolean more_chance = false;
for (TryInfo info : info_list) {
if (!info.nomore) {
more_chance = true;
break;
}
}
if (succeeded || !more_chance) {
break;
}
}
return succeeded;
}
public Date getDate() {
long now = System.nanoTime() / (1000 * 1000);
return new Date(source_time + (now - sync_time));
}
// -----------------------------
// ユーティリティーメソッド
// -----------------------------
private void sync(InetSocketAddress address, int timeout) throws IOException, KissODethReceived {
DatagramSocket sock = new DatagramSocket(local_port, local_address);
sock.setSoTimeout(timeout);
Date now = new Date();
Packet request = new Packet();
request.leap_indicator = 0;
request.version_number = 4;
request.mode = 3;
request.stratum = 0;
request.poll = 0;
request.precision = 0;
request.root_delay = 0;
request.root_dispersion = 0;
request.reference_identifier = 0;
request.reference_timestamp = 0;
request.originate_timestamp = 0;
request.receive_timestamp = 0;
request.transmit_timestamp = now.getTime();
byte[] data = request.pack();
DatagramPacket request_datagram = new DatagramPacket(data, data.length, address);
sock.send(request_datagram);
Log.d(TAG, "request src=" + sock.getLocalSocketAddress()
+ " dst=" + address
+ " packet=" + request.toString());
DatagramPacket response_datagram = new DatagramPacket(data, data.length);
sock.receive(response_datagram);
long rt = System.nanoTime();
if (Packet.PACKET_SIZE <= response_datagram.getLength()) {
Packet response = new Packet(data);
Log.d(TAG, "response src=" + response_datagram.getSocketAddress()
+ " dst=" + sock.getLocalSocketAddress()
+ " packet=" + response.toString());
if (response.stratum == 0) {
throw new KissODethReceived();
}
if (request_datagram.getSocketAddress().equals(response_datagram.getSocketAddress())
&& request.transmit_timestamp == response.originate_timestamp
&& response.isValid()) {
Date received = new Date();
long clock_offset = ((response.receive_timestamp - response.originate_timestamp)
+ (response.transmit_timestamp - received.getTime())) / 2;
source_time = response.receive_timestamp + clock_offset;
sync_time = rt / (1000 * 1000);
} else {
throw new IOException("invalid packet");
}
} else {
throw new IOException("packet too short");
}
}
// -----------------------------
// プライベート内部クラス
// -----------------------------
/*
* 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |LI | VN |Mode | Stratum | Poll | Precision |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Root Delay |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Root Dispersion |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Reference Identifier |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* | Reference Timestamp (64) |
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* | Originate Timestamp (64) |
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* | Receive Timestamp (64) |
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* | Transmit Timestamp (64) |
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Key Identifier (optional) (32) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* | |
* | Message Digest (optional) (128) |
* | |
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
*/
private static class Packet {
public static int PACKET_SIZE = 48;
private static long UNIX_TIME_OFFSET = 2208988800L; // from RFC 868
public int leap_indicator;
public int version_number;
public int mode;
public int stratum;
public int poll;
public int precision;
public int root_delay; // milliseconds
public int root_dispersion; // milliseconds
public int reference_identifier;
public long reference_timestamp; // millisecond unix time
public long originate_timestamp; // millisecond unix time
public long receive_timestamp; // millisecond unix time
public long transmit_timestamp; // millisecond unix time
public Packet() {
}
public Packet(byte [] data) {
assert PACKET_SIZE <= data.length;
leap_indicator = (data[0]>>>6) & 0x03; // 8-bit code
version_number = (data[0]>>>3) & 0x07; // 3-bit integer
mode = (data[0]>>>0) & 0x07; // 3-bit integer
stratum = (data[1]) & 0xFF; // 8-bit unsigned integer
poll = (data[2]) & 0xFF; // 8-bit unsigned integer
precision = data[3]; // 8-bit signed integer(assumes two's complement)
root_delay = readSignedFixed32Sec(data, 4);
root_dispersion = readSignedFixed32Sec(data, 8);
reference_identifier = readInt(data, 12);
reference_timestamp = readTimestamp(data, 16);
originate_timestamp = readTimestamp(data, 24);
receive_timestamp = readTimestamp(data, 32);
transmit_timestamp = readTimestamp(data, 40);
}
public String toString() {
StringBuilder result = new StringBuilder();
result.append("{");
result.append("LI=" + leap_indicator + ",");
result.append("VN=" + version_number + ",");
result.append("Mode=" + mode + ",");
result.append("Stratum=" + stratum + ",");
result.append("Poll=" + poll + ",");
result.append("Precision=" + precision + ",");
result.append("Root Delay=" + root_delay + ",");
result.append("Root Dispersion=" + root_dispersion + ",");
result.append("Reference Identifier=" + IdentifierString(stratum, reference_identifier) + ",");
result.append("Reference Timestamp=" + TimestampString(reference_timestamp) + ",");
result.append("Originate Timestamp=" + TimestampString(originate_timestamp) + ",");
result.append("Receive Timestamp=" + TimestampString(receive_timestamp) + ",");
result.append("Transmit Timestamp=" + TimestampString(transmit_timestamp));
result.append("}");
return result.toString();
}
public byte[] pack() {
assert 0 <= leap_indicator && leap_indicator <= 3;
assert 0 <= version_number && version_number <= 7;
assert 0 <= mode && mode <= 7;
assert 0 <= stratum && stratum <= 255;
assert 0 <= poll && poll <= 255;
assert -128 <= precision && precision <= 127;
byte[] result = new byte[48];
result[0] = (byte)((leap_indicator<<6) | (version_number<<3) | mode);
result[1] = (byte)stratum;
result[2] = (byte)poll;
result[3] = (byte)precision;
writeSignedFixed32Sec(result, 4, root_delay);
writeSignedFixed32Sec(result, 8, root_dispersion);
writeInt(result, 12, reference_identifier);
writeTimestamp(result, 16, reference_timestamp);
writeTimestamp(result, 24, originate_timestamp);
writeTimestamp(result, 32, receive_timestamp);
writeTimestamp(result, 40, transmit_timestamp);
return result;
}
public boolean isValid() {
if (leap_indicator < 0 || 3 < leap_indicator) {
return false;
}
if (version_number != SNTP_VERSION) {
return false;
}
if (mode != 4) {
return false;
}
if (stratum < 0 || 15 < stratum) {
return false;
}
if (transmit_timestamp == 0) {
return false;
}
return true;
}
private static int readInt(byte[] bytes, int i) {
return ((bytes[i + 0]<<24) & 0xFF000000)
| ((bytes[i + 1]<<16) & 0x00FF0000)
| ((bytes[i + 2]<< 8) & 0x0000FF00)
| ((bytes[i + 3]<< 0) & 0x000000FF);
}
private static void writeInt(byte[] bytes, int i, int val) {
bytes[i + 0] = (byte)((val>>24) & 0xFF);
bytes[i + 1] = (byte)((val>>16) & 0xFF);
bytes[i + 2] = (byte)((val>> 8) & 0xFF);
bytes[i + 3] = (byte)((val>> 0) & 0xFF);
}
private static int readSignedFixed32Sec(byte[] bytes, int i) {
long sec = readInt(bytes, i);
return (int)(1000 * sec / 65536); // to milliseconds
}
private static void writeSignedFixed32Sec(byte[] bytes, int i, int val) {
long sec = 65536 * val / 1000;
writeInt(bytes, i, (int)sec);
}
private static long readTimestamp(byte[] bytes, int i) {
long sec =
(((long)bytes[i + 0]<<56) & 0xFF00000000000000l)
| (((long)bytes[i + 1]<<48) & 0x00FF000000000000l)
| (((long)bytes[i + 2]<<40) & 0x0000FF0000000000l)
| (((long)bytes[i + 3]<<32) & 0x000000FF00000000l)
| (((long)bytes[i + 4]<<24) & 0x00000000FF000000l)
| (((long)bytes[i + 5]<<16) & 0x0000000000FF0000l)
| (((long)bytes[i + 6]<< 8) & 0x000000000000FF00l)
| (((long)bytes[i + 7]<< 0) & 0x00000000000000FFl);
return (sec>>>22) - (sec>>>28) - (sec>>>29) - UNIX_TIME_OFFSET * 1000; // to milliseconds
}
private static void writeTimestamp(byte[] bytes, int i, long val) {
assert 0 <= val;
long sec = (val / 1000) % (2l<<32) + UNIX_TIME_OFFSET;
long msec = ((val % 1000)<<32) / 1000;
bytes[i + 0] = (byte)(( sec>>24) & 0xFF);
bytes[i + 1] = (byte)(( sec>>16) & 0xFF);
bytes[i + 2] = (byte)(( sec>> 8) & 0xFF);
bytes[i + 3] = (byte)(( sec>> 0) & 0xFF);
bytes[i + 4] = (byte)((msec>>24) & 0xFF);
bytes[i + 5] = (byte)((msec>>16) & 0xFF);
bytes[i + 6] = (byte)((msec>> 8) & 0xFF);
bytes[i + 7] = (byte)((msec>> 0) & 0xFF);
}
private static String IdentifierString(int stratum, int identifier) {
String result = null;
byte[] bytes = {
(byte)((identifier>>24) & 0xFF),
(byte)((identifier>>16) & 0xFF),
(byte)((identifier>> 8) & 0xFF),
(byte)((identifier>> 0) & 0xFF),
};
if (stratum == 0 || stratum == 1) {
try {
result = new String(bytes, "US-ASCII");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "UnsupportedEncodingException. cause=" + e.getMessage());
}
} else {
try {
InetAddress addr = InetAddress.getByAddress(bytes);
result = addr.getHostAddress();
} catch (UnknownHostException e) {
Log.e(TAG, "UnknownHostException. cause=" + e.getMessage());
}
}
return result;
}
private static String TimestampString(long timestamp) {
Date date = new Date(timestamp);
return date.toString();
}
}
private static class KissODethReceived extends IOException {
public static final long serialVersionUID = 1;
}
private static class TryInfo {
InetSocketAddress address;
int count;
int span;
boolean nomore;
public TryInfo(InetSocketAddress addr) {
address = addr;
count = 0;
span = 0;
nomore = false;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment