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