Measuring accurately
editA single query is one measurement: a reply delayed in a queue on the way back makes its offset wrong, and a server whose clock is wrong gives a wrong offset. The client can combine several measurements, as NTP implementations do (RFC 5905).
Several samples from one server
MeasureAsync queries the server several times and keeps the sample with the shortest round-trip delay: the one that spent the least time in queues, whose offset is the most accurate.
var client = new NtpClient("127.0.0.1", new NtpClientOptions { Port = port });
// 4 queries, 2 seconds apart, by default
var measurement = await client.MeasureAsync(new NtpMeasurementOptions
{
SampleCount = 4,
SampleInterval = TimeSpan.FromMilliseconds(100),
});
// The offset of the sample with the shortest round-trip delay
var offset = measurement.ClockOffset;
// The maximum error of the offset, and how much the samples disagree
var maximumError = measurement.RootDistance;
var jitter = measurement.Jitter;
Assert.Equal(4, measurement.Samples.Count + measurement.Errors.Count);The NtpServerMeasurement it returns contains:
| Property | Contains |
|---|---|
| BestSample, ClockOffset, RoundTripDelay | The sample with the shortest round-trip delay, and its offset and delay |
| Samples, Errors | Every valid reply, and the error of every query that got none |
| Jitter | How much the offsets of the samples differ from the best one |
| RootDistance | The maximum error of the offset: half the delay to the server and to its reference clock, plus the dispersion and the precision the server reports, plus the jitter |
The call fails only when no query gets a valid reply: it throws TimeoutException when they all time out, and AggregateException when they fail for different reasons.
| Option | Default | Use |
|---|---|---|
| SampleCount | 4 | The number of queries. |
| SampleInterval | 2 seconds | The delay between two queries. Public servers rate-limit their clients: do not set it to zero with them. |
Several servers
Querying several servers protects against a server whose clock is wrong. The static NtpClient.MeasureAsync takes one client per server, measures them concurrently, and applies the selection algorithm of NTP: the offset of each server is an interval, its offset plus or minus its root distance, and the servers whose intervals do not overlap the ones of the majority are discarded. The offsets of the others are averaged, weighted by their accuracy.
// Three servers, one of which is one hour ahead
using var server1 = new NtpServer(new NtpServerOptions { Port = 0, BindAddress = IPAddress.Loopback });
using var server2 = new NtpServer(new NtpServerOptions { Port = 0, BindAddress = IPAddress.Loopback });
using var wrongServer = new NtpServer(new NtpServerOptions
{
Port = 0,
BindAddress = IPAddress.Loopback,
TimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow.AddHours(1)),
});
await server1.StartAsync();
await server2.StartAsync();
await wrongServer.StartAsync();
NtpClient[] clients =
[
new NtpClient("127.0.0.1", new NtpClientOptions { Port = server1.Port }),
new NtpClient("127.0.0.1", new NtpClientOptions { Port = server2.Port }),
new NtpClient("127.0.0.1", new NtpClientOptions { Port = wrongServer.Port }),
];
var measurement = await NtpClient.MeasureAsync(clients, new NtpMeasurementOptions { SampleCount = 2, SampleInterval = TimeSpan.Zero });
// The server that is one hour ahead is outvoted
Assert.Equal(2, measurement.SelectedServers.Count);
Assert.Same(clients[2], Assert.Single(measurement.RejectedServers).Client);
// The offset to add to the local clock, and its bounds
Assert.InRange(measurement.ClockOffset, TimeSpan.FromSeconds(-1), TimeSpan.FromSeconds(1));
Assert.InRange(measurement.ClockOffset, measurement.MinimumClockOffset, measurement.MaximumClockOffset);- Query at least 3 servers: with 2 servers that disagree, there is no majority. Each client is one vote, so do not query the same server twice, for instance through two names.
- Each client has its own options: its port, its timeout, its key.
- SelectedServers agree with the majority, RejectedServers do not, and FailedServers gave no valid reply, with the exception of each.
- The true offset lies between MinimumClockOffset and MaximumClockOffset, as long as the majority of the servers is right.
The call throws NtpClockSelectionException when servers answered but no majority of them agrees; its Measurements property lists what each one answered. When no server answers, it throws TimeoutException or AggregateException.
Retrying a lost request
RetryCount sends a new request when no valid reply arrives within Timeout. It applies to QueryAsync and to each query of MeasureAsync. See Retries.