feat: NTP client

This commit is contained in:
2026-05-31 20:00:41 +02:00
parent 922cc5d82a
commit 965bec72d2

View 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()
}
}