feat: NTP client
This commit is contained in:
102
app/src/main/java/at/lockstep/player/util/NtpClient.kt
Normal file
102
app/src/main/java/at/lockstep/player/util/NtpClient.kt
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package at.lockstep.player.util
|
||||||
|
|
||||||
|
import java.net.DatagramPacket
|
||||||
|
import java.net.DatagramSocket
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
object NtpClient {
|
||||||
|
|
||||||
|
private const val NTP_HOST = "pool.ntp.org"
|
||||||
|
private const val NTP_PORT = 123
|
||||||
|
private const val NTP_PACKET_SIZE = 48
|
||||||
|
private const val TIMEOUT_MS = 5000
|
||||||
|
private const val NTP_EPOCH_OFFSET_SECONDS = 2208988800L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries [NTP_HOST] and returns how many seconds the system clock is ahead of the server.
|
||||||
|
* Positive = local clock is fast; negative = local clock is slow.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun clockOffsetSeconds(): Double {
|
||||||
|
val socket = DatagramSocket()
|
||||||
|
try {
|
||||||
|
socket.soTimeout = TIMEOUT_MS
|
||||||
|
|
||||||
|
val requestBuffer = ByteArray(NTP_PACKET_SIZE)
|
||||||
|
requestBuffer[0] = 0x1B // LI=0, VN=3, mode=client
|
||||||
|
|
||||||
|
val t1Millis = System.currentTimeMillis()
|
||||||
|
writeNtpTimestamp(requestBuffer, 40, t1Millis)
|
||||||
|
|
||||||
|
val address = InetAddress.getByName(NTP_HOST)
|
||||||
|
val requestPacket = DatagramPacket(requestBuffer, requestBuffer.size, address, NTP_PORT)
|
||||||
|
socket.send(requestPacket)
|
||||||
|
|
||||||
|
val responseBuffer = ByteArray(NTP_PACKET_SIZE)
|
||||||
|
val responsePacket = DatagramPacket(responseBuffer, responseBuffer.size)
|
||||||
|
|
||||||
|
val errorRef = AtomicReference<Exception?>(null)
|
||||||
|
val receiveTimeRef = AtomicReference<Long?>(null)
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
socket.receive(responsePacket)
|
||||||
|
receiveTimeRef.set(System.currentTimeMillis())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorRef.set(e)
|
||||||
|
} finally {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
|
||||||
|
if (!latch.await((TIMEOUT_MS + 1000).toLong(), TimeUnit.MILLISECONDS)) {
|
||||||
|
throw SocketTimeoutException("NTP request timed out")
|
||||||
|
}
|
||||||
|
errorRef.get()?.let { throw it }
|
||||||
|
|
||||||
|
val t4Millis = receiveTimeRef.get()
|
||||||
|
?: throw IllegalStateException("NTP response received without timestamp")
|
||||||
|
|
||||||
|
val t1 = t1Millis / 1000.0
|
||||||
|
val t2 = ntpBytesToSeconds(responseBuffer, 32)
|
||||||
|
val t3 = ntpBytesToSeconds(responseBuffer, 40)
|
||||||
|
val t4 = t4Millis / 1000.0
|
||||||
|
|
||||||
|
return ((t1 - t2) + (t4 - t3)) / 2.0
|
||||||
|
} finally {
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ntpBytesToSeconds(buffer: ByteArray, offset: Int): Double {
|
||||||
|
val seconds = readUint32(buffer, offset)
|
||||||
|
val fraction = readUint32(buffer, offset + 4)
|
||||||
|
return (seconds - NTP_EPOCH_OFFSET_SECONDS) + fraction / 4294967296.0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeNtpTimestamp(buffer: ByteArray, offset: Int, millis: Long) {
|
||||||
|
val unixSeconds = millis / 1000.0
|
||||||
|
val ntpSeconds = (unixSeconds + NTP_EPOCH_OFFSET_SECONDS).toLong()
|
||||||
|
val fraction = ((unixSeconds - unixSeconds.toLong()) * 4294967296.0).toLong()
|
||||||
|
writeUint32(buffer, offset, ntpSeconds)
|
||||||
|
writeUint32(buffer, offset + 4, fraction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readUint32(buffer: ByteArray, offset: Int): Long =
|
||||||
|
((buffer[offset].toLong() and 0xFF) shl 24) or
|
||||||
|
((buffer[offset + 1].toLong() and 0xFF) shl 16) or
|
||||||
|
((buffer[offset + 2].toLong() and 0xFF) shl 8) or
|
||||||
|
(buffer[offset + 3].toLong() and 0xFF)
|
||||||
|
|
||||||
|
private fun writeUint32(buffer: ByteArray, offset: Int, value: Long) {
|
||||||
|
buffer[offset] = ((value shr 24) and 0xFF).toByte()
|
||||||
|
buffer[offset + 1] = ((value shr 16) and 0xFF).toByte()
|
||||||
|
buffer[offset + 2] = ((value shr 8) and 0xFF).toByte()
|
||||||
|
buffer[offset + 3] = (value and 0xFF).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user