在之前写了两篇关于NTP的文章,一篇是使用NTP协议实现本地时间跟NTP授时服务器进行同步介绍了NTP协议的原理,另一篇是实现一个时间同步客户端NTPClock介绍了使用WPF编写一个模仿NTPClock的时间同步的App,因为Windows的自动较时功能不是很完善,所以这个应用我自己使用了很长时间。
在这篇文章里,我准备将前面两篇文章总结一下,对一些方法进行重构,另外实现一个利用NTP来实现授时的服务端,以完善整个功能。一般的我们都是使用一些公开的NTP授时服务地址,比如"time.windows.com"、"pool.ntp.org"等来获取时间,这有一些限制:
- 必须要求这个服务器能够访问和解析这些授时服务器地址,这在有些机房的内网计算机上其实是无法满足条件的。
- 这些公开的NTP授时服务器大多有请求频率限制,如果请求次数多过或过快,可能会请求失败。
有时候,我们有自己的GPS接收装置或者原子钟能够获取准确的时间,那么就可以在本地搭建一个NTP授时服务器,其它的计算机如果需要时间同步,只需要利用NTP协议给这台NTP授时服务器发送信息即可。本文重新简单实现一个各个部分,包括一个NTPCore类库、一个NTPClientApp时间同步客户端和一个NTPServerApp授时服务端,下面就简单介绍一下各模块的功能。
NTP协议及相关实现
这部分核心就是对NTP协议的实现,包括一个NtpPacket,NtpClient和一个SimpleNtpServer,这些放在名为NTPCore的类库中。
NtpPacket
它的核心是一个48字节的NtpPacket包。详细代码如下:
/// <summary>
/// 表示一个NTP数据包。
/// </summary>
public class NtpPacket
{
// NTP包的各个字段偏移量
private const byte OffLeapIndicator = 0;
private const byte OffVersionNumber = 0;
private const byte OffMode = 0;
private const byte OffStratum = 1;
private const byte OffPollInterval = 2;
private const byte OffPrecision = 3;
private const byte OffRootDelay = 4;
private const byte OffRootDispersion = 8;
private const byte OffReferenceId = 12;
private const byte OffReferenceTimestamp = 16;
private const byte OffOriginateTimestamp = 24;
private const byte OffReceiveTimestamp = 32;
private const byte OffTransmitTimestamp = 40;
public const int PacketLength = 48; // NTP包的标准长度
private byte[] _data = new byte[PacketLength];
public byte LeapIndicator
{
get => (byte)((_data[OffLeapIndicator] >> 6) & 0x03);
set => _data[OffLeapIndicator] = (byte)((_data[OffLeapIndicator] & 0x3F) | (value << 6));
}
public byte VersionNumber
{
get => (byte)((_data[OffVersionNumber] >> 3) & 0x07);
set => _data[OffVersionNumber] = (byte)((_data[OffVersionNumber] & 0xC7) | (value << 3));
}
public byte Mode
{
get => (byte)(_data[OffMode] & 0x07);
set => _data[OffMode] = (byte)((_data[OffMode] & 0xF8) | value);
}
public byte Stratum
{
get => _data[OffStratum];
set => _data[OffStratum] = value;
}
public sbyte PollInterval
{
get => (sbyte)_data[OffPollInterval];
set => _data[OffPollInterval] = (byte)value;
}
public sbyte Precision
{
get => (sbyte)_data[OffPrecision];
set => _data[OffPrecision] = (byte)value;
}
public uint RootDelay
{
get => GetUInt32BE(OffRootDelay);
set => SetUInt32BE(OffRootDelay, value);
}
public uint RootDispersion
{
get => GetUInt32BE(OffRootDispersion);
set => SetUInt32BE(OffRootDispersion, value);
}
public uint ReferenceId
{
get => GetUInt32BE(OffReferenceId);
set => SetUInt32BE(OffReferenceId, value);
}
public NtpTimestamp ReferenceTimestamp
{
get => GetNtpTimestamp(OffReferenceTimestamp);
set => SetNtpTimestamp(OffReferenceTimestamp, value);
}
public NtpTimestamp OriginateTimestamp
{
get => GetNtpTimestamp(OffOriginateTimestamp);
set => SetNtpTimestamp(OffOriginateTimestamp, value);
}
public NtpTimestamp ReceiveTimestamp
{
get => GetNtpTimestamp(OffReceiveTimestamp);
set => SetNtpTimestamp(OffReceiveTimestamp, value);
}
public NtpTimestamp TransmitTimestamp
{
get => GetNtpTimestamp(OffTransmitTimestamp);
set => SetNtpTimestamp(OffTransmitTimestamp, value);
}
public NtpTimestamp DestinationTimestamp { get; set; }
public NtpPacket()
{
VersionNumber = 4;
Mode = 3; // Client
TransmitTimestamp = new NtpTimestamp(DateTime.UtcNow);
}
public NtpPacket(byte[] packetData)
{
if (packetData == null || packetData.Length < PacketLength)
throw new ArgumentException($"NTP packet data must be at least {PacketLength} bytes long.");
Buffer.BlockCopy(packetData, 0, _data, 0, PacketLength);
}
public byte[] ToByteArray()
{
byte[] buffer = new byte[PacketLength];
Buffer.BlockCopy(_data, 0, buffer, 0, PacketLength);
return buffer;
}
private uint GetUInt32BE(int offset)
{
return ((uint)_data[offset] << 24) |
((uint)_data[offset + 1] << 16) |
((uint)_data[offset + 2] << 8) |
_data[offset + 3];
}
private void SetUInt32BE(int offset, uint value)
{
_data[offset] = (byte)(value >> 24);
_data[offset + 1] = (byte)(value >> 16);
_data[offset + 2] = (byte)(value >> 8);
_data[offset + 3] = (byte)value;
}
private NtpTimestamp GetNtpTimestamp(int offset)
{
uint seconds = GetUInt32BE(offset);
uint fraction = GetUInt32BE(offset + 4);
return new NtpTimestamp(seconds, fraction);
}
private void SetNtpTimestamp(int offset, NtpTimestamp timestamp)
{
SetUInt32BE(offset, timestamp.Seconds);
SetUInt32BE(offset + 4, timestamp.Fraction);
}
public TimeSpan CalculateOffset()
{
DateTime t1 = OriginateTimestamp.ToDateTime();
DateTime t2 = ReceiveTimestamp.ToDateTime();
DateTime t3 = TransmitTimestamp.ToDateTime();
DateTime t4 = DestinationTimestamp.ToDateTime();
if (t1 == NtpTimestamp.NtpEpoch || t2 == NtpTimestamp.NtpEpoch ||
t3 == NtpTimestamp.NtpEpoch || t4 == NtpTimestamp.NtpEpoch ||
t1 > t4 || t2 > t3)
{
throw new InvalidOperationException($"Cannot calculate offset due to invalid or incomplete timestamps. T1:{t1}, T2:{t2}, T3:{t3}, T4:{t4}");
}
TimeSpan combinedSpan = (t2 - t1) + (t3 - t4);
TimeSpan offset = TimeSpan.FromTicks(combinedSpan.Ticks / 2L);
return offset;
}
public TimeSpan CalculateRoundtripDelay()
{
DateTime t1 = OriginateTimestamp.ToDateTime();
DateTime t2 = ReceiveTimestamp.ToDateTime();
DateTime t3 = TransmitTimestamp.ToDateTime();
DateTime t4 = DestinationTimestamp.ToDateTime();
if (t1 == NtpTimestamp.NtpEpoch || t2 == NtpTimestamp.NtpEpoch ||
t3 == NtpTimestamp.NtpEpoch || t4 == NtpTimestamp.NtpEpoch ||
t1 > t4 || t2 > t3)
{
throw new InvalidOperationException($"Cannot calculate roundtrip delay due to invalid or incomplete timestamps. T1:{t1}, T2:{t2}, T3:{t3}, T4:{t4}");
}
TimeSpan delay = (t4 - t1) - (t3 - t2);
if (delay < TimeSpan.Zero)
{
return TimeSpan.MaxValue;
}
return delay;
}
public override string ToString()
{
string offsetStr = "N/A";
string delayStr = "N/A";
try
{
offsetStr = $"{CalculateOffset().TotalMilliseconds:F3} ms";
}
catch (InvalidOperationException) { /* Keep N/A */ }
try
{
TimeSpan delay = CalculateRoundtripDelay();
delayStr = delay == TimeSpan.MaxValue ? "N/A (Invalid Timestamps)" : $"{delay.TotalMilliseconds:F3} ms";
}
catch (InvalidOperationException) { /* Keep N/A */ }
return $"LI: {LeapIndicator}, VN: {VersionNumber}, Mode: {Mode}, Stratum: {Stratum}\n" +
$"Poll: {PollInterval}, Precision: {Precision}\n" +
$"Root Delay: {RootDelay / 65536.0:F3}s, Root Dispersion: {RootDispersion / 65536.0:F3}s\n" +
$"Reference ID: 0x{ReferenceId:X8}\n" +
$"Reference Timestamp: {ReferenceTimestamp}\n" +
$"Originate Timestamp (T1): {OriginateTimestamp}\n" +
$"Receive Timestamp (T2): {ReceiveTimestamp}\n" +
$"Transmit Timestamp (T3): {TransmitTimestamp}\n" +
(DestinationTimestamp.ToDateTime() == NtpTimestamp.NtpEpoch ? "" : $"Destination Timestamp (T4): {DestinationTimestamp}\n") +
$"Calculated Offset: {offsetStr}\n" +
$"Calculated Roundtrip Delay: {delayStr}";
}
}
/// <summary>
/// 表示NTP时间戳。
/// NTP时间戳是64位无符号定点数,整数部分在前32位,小数部分在后32位。
/// NTP的纪元是1900年1月1日。
/// </summary>
public struct NtpTimestamp
{
public static readonly DateTime NtpEpoch = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const ulong NtpTimestampToSecondsRatio = 0x100000000UL; // 2^32
public uint Seconds { get; }
public uint Fraction { get; }
public NtpTimestamp(DateTime dateTime)
{
if (dateTime.Kind == DateTimeKind.Local)
{
dateTime = dateTime.ToUniversalTime();
}
else if (dateTime.Kind == DateTimeKind.Unspecified)
{
dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
}
if (dateTime < NtpEpoch)
{
Seconds = 0;
Fraction = 0;
}
else
{
TimeSpan span = dateTime - NtpEpoch;
Seconds = (uint)span.TotalSeconds;
Fraction = (uint)((span.TotalMilliseconds - (Seconds * 1000.0)) * (NtpTimestampToSecondsRatio / 1000.0));
}
}
public NtpTimestamp(uint seconds, uint fraction)
{
Seconds = seconds;
Fraction = fraction;
}
public DateTime ToDateTime()
{
if (Seconds == 0 && Fraction == 0) // NTP的空时间戳或1900年以前的时间
{
return NtpEpoch;
}
double milliseconds = Seconds * 1000.0 + (Fraction / (double)NtpTimestampToSecondsRatio) * 1000.0;
return NtpEpoch.AddMilliseconds(milliseconds);
}
public override string ToString() => ToDateTime().ToString("yyyy-MM-dd HH:mm:ss.fff UTC");
}
当客户端使用时,只需要实例化一个NtpPacket,它的构造函数默认设置为了客户端请求,并把TransmitTimestamp时间设置为了客户端发送时的时间。实例化完整NtpPacket之后,等待接收服务端返回的NtpPacket对象,此时这个NtpPacket对象里面已经包含了服务器的相关事件信息,客户端只需要调用NtpPacket的CalculateOffset即可获得客户端时间与服务端时间的差值,只需要将这个差值加上本地时间,就可以得到正确的服务端时间。
NtpClient
NtpClient代码如下:
class NtpClient
{
private readonly string _serverAddress;
private readonly int _port;
private readonly int _timeoutMilliseconds;
private readonly ILogger _logger;
private const int MaxRetries = 3; // Maximum number of retries
private const int RetryDelayMilliseconds = 1000; // Delay between retries
public NtpClient(string serverAddress, int port = 123, int timeoutMilliseconds = 5000, ILogger logger = null)
{
_serverAddress = serverAddress ?? throw new ArgumentNullException(nameof(serverAddress));
_port = port;
_timeoutMilliseconds = timeoutMilliseconds;
_logger = logger ?? new NullLogger(); // Use a NullLogger if no logger is provided
}
public async Task<NtpPacket> GetTimeAsync()
{
int attempts = 0;
while (attempts < MaxRetries)
{
attempts++;
_logger.Debug($"Attempt {attempts}/{MaxRetries} to get time from {_serverAddress}.");
try
{
using (var udpClient = new UdpClient())
{
IPAddress[] addresses = await Dns.GetHostAddressesAsync(_serverAddress);
if (addresses == null || addresses.Length == 0)
{
_logger.Warn($"Attempt {attempts}: Hostname '{_serverAddress}' could not be resolved.");
if (attempts >= MaxRetries) throw new ArgumentException("Hostname could not be resolved after multiple attempts.", nameof(_serverAddress));
await Task.Delay(RetryDelayMilliseconds); // Wait before retrying DNS resolution
continue;
}
IPAddress serverIp = addresses[0];
_logger.Debug($"Attempt {attempts}: Resolved {_serverAddress} to {serverIp}. Connecting...");
if (serverIp.AddressFamily == AddressFamily.InterNetworkV6)
{
_logger.Debug($"Attempt {attempts}: Attempting to connect to IPv6 address: {serverIp}");
}
udpClient.Connect(serverIp, _port);
var requestPacket = new NtpPacket { TransmitTimestamp = new NtpTimestamp(DateTime.UtcNow) };
byte[] requestBytes = requestPacket.ToByteArray();
await udpClient.SendAsync(requestBytes, requestBytes.Length);
_logger.Debug($"Attempt {attempts}: NTP request sent to {serverIp}:{_port}.");
var receivingTask = udpClient.ReceiveAsync();
if (await Task.WhenAny(receivingTask, Task.Delay(_timeoutMilliseconds)) == receivingTask)
{
if (receivingTask.IsFaulted)
{
throw receivingTask.Exception.GetBaseException(); // Let catch block handle it
}
UdpReceiveResult result = receivingTask.Result;
_logger.Debug($"Attempt {attempts}: Received {result.Buffer.Length} bytes from {serverIp}.");
var responsePacket = new NtpPacket(result.Buffer) { DestinationTimestamp = new NtpTimestamp(DateTime.UtcNow) };
_logger.Info($"Attempt {attempts}: Successfully received and parsed NTP packet from {_serverAddress}.");
return responsePacket;
}
else
{
_logger.Warn($"Attempt {attempts}: Timeout receiving data from {_serverAddress}.");
if (attempts >= MaxRetries) throw new TimeoutException($"Request to '{_serverAddress}' timed out after {MaxRetries} attempts and {_timeoutMilliseconds}ms per attempt.");
}
}
}
catch (Exception ex)
{
_logger.Error($"Attempt {attempts} failed for {_serverAddress}.", ex);
if (attempts >= MaxRetries)
{
throw; // Rethrow the last exception to be handled by the caller
}
}
if (attempts < MaxRetries)
{
_logger.Info($"Retrying {_serverAddress} in {RetryDelayMilliseconds}ms...");
await Task.Delay(RetryDelayMilliseconds);
}
}
_logger.Error($"All {MaxRetries} attempts to get time from {_serverAddress} failed.");
return null; // Indicates failure after all retries
}
}
当需要使用NTP客户端同步时间时,只需要实例化一个NtpClient,然后调用GetTimeAsync异步方法,方法返回一个NtpPacket,调用CalculateOffset方法即可返回本机与授时服务器的时间差。上述方法还设置了超时重试次数。
SimpleNtpServer
对于授时服务端,可以使用SimpleNtpServer类,代码如下:
public class SimpleNtpServer
{
private UdpClient _listener; // Changed
private readonly int _port;
private volatile bool _isRunning;
private Task _listenTask; // Changed
public event Action<string> LogMessage; // Changed
private readonly ILogger _logger;
public SimpleNtpServer(int port = 123, ILogger logger = null)
{
_port = port;
_logger = logger ?? new NullLogger(); // Use a NullLogger if no logger is provided
}
public void Start()
{
if (_isRunning)
{
LogMessage?.Invoke("Server is already running.");
_logger.Warn("Server start requested, but it is already running.");
return;
}
try
{
_listener = new UdpClient(new IPEndPoint(IPAddress.Any, _port));
_isRunning = true;
_listenTask = Task.Run((Action)ListenLoop);
LogMessage?.Invoke($"NTP Server started on port {_port}, listening on all interfaces.");
_logger.Info($"NTP Server started on port {_port}, listening on all interfaces.");
}
catch (SocketException ex)
{
LogMessage?.Invoke($"SocketException starting NTP server on port {_port}: {ex.Message}. (Port might be in use, e.g., by Windows Time service w32time).");
_logger.Error($"SocketException starting NTP server on port {_port}: {ex.Message}. (Port might be in use, e.g., by Windows Time service w32time).");
_isRunning = false;
_listener?.Close();
_listener = null;
}
catch (Exception ex)
{
LogMessage?.Invoke($"Error starting NTP server: {ex.Message}");
_logger.Error($"Failed to start NTP server on port {_port}.", ex);
_isRunning = false;
_listener?.Close();
_listener = null;
}
}
public async Task StopAsync()
{
if (!_isRunning || _listener == null)
{
_logger.Info("Server stop requested, but it is not running or already stopped.");
LogMessage?.Invoke("Server is not running or already stopped.");
return;
}
_logger.Info("NTP Server stopping...");
_isRunning = false;
_listener.Close();
if (_listenTask != null)
{
try
{
bool completed = await Task.WhenAny(_listenTask, Task.Delay(2000)) == _listenTask;
if (!completed && _listenTask.Status != TaskStatus.RanToCompletion && _listenTask.Status != TaskStatus.Faulted && _listenTask.Status != TaskStatus.Canceled)
{
_logger.Warn("NTP server listen loop did not terminate gracefully within the timeout period.");
LogMessage?.Invoke("Warning: Listen loop did not terminate gracefully within timeout.");
}
}
catch (ObjectDisposedException) { /* Expected */ }
catch (SocketException) { /* Expected */ }
catch (Exception ex)
{
_logger.Error("Exception during server stop task waiting.", ex);
LogMessage?.Invoke($"Exception during server stop task: {ex.Message}");
}
}
_listener = null;
_listenTask = null;
LogMessage?.Invoke("NTP Server stopped.");
_logger.Info("NTP Server stopped.");
}
private async void ListenLoop() // async void is generally for event handlers, but for Task.Run's Action, this needs careful handling on exceptions
{
if (_listener == null) return;
_logger.Info("NTP Server listening loop started.");
LogMessage?.Invoke("NTP Server listening loop started...");
while (_isRunning)
{
try
{
UdpReceiveResult result = await _listener.ReceiveAsync();
DateTime receiptTime = DateTime.UtcNow;
IPEndPoint clientEndPoint = result.RemoteEndPoint;
byte[] clientRequestBytes = result.Buffer;
LogMessage?.Invoke($"Received {clientRequestBytes.Length} bytes from {clientEndPoint}");
_logger.Debug($"Received {result.Buffer.Length} bytes from {clientEndPoint}.");
if (clientRequestBytes.Length < NtpPacket.PacketLength)
{
LogMessage?.Invoke($"Received undersized packet ({clientRequestBytes.Length} bytes) from {clientEndPoint}. Ignoring.");
_logger.Error($"Received undersized packet ({clientRequestBytes.Length} bytes) from {clientEndPoint}. Ignoring.");
continue;
}
var clientPacket = new NtpPacket(clientRequestBytes);
LogMessage?.Invoke($"Client Packet (VN:{clientPacket.VersionNumber}, Mode:{clientPacket.Mode}, T1_client:{clientPacket.TransmitTimestamp})");
_logger.Debug($"Client Packet (VN:{clientPacket.VersionNumber}, Mode:{clientPacket.Mode}, T1_client:{clientPacket.TransmitTimestamp})");
if (clientPacket.Mode != 3)
{
_logger.Warn($"Received packet with non-client mode ({clientPacket.Mode}) from {clientEndPoint}. Ignoring.");
LogMessage?.Invoke($"Received packet with non-client mode ({clientPacket.Mode}) from {clientEndPoint}. Ignoring.");
continue;
}
var responsePacket = new NtpPacket
{
LeapIndicator = 0,
VersionNumber = clientPacket.VersionNumber,
Mode = 4, // Server mode
Stratum = 2, // Secondary reference
PollInterval = clientPacket.PollInterval,
Precision = -20, // Approx. 1 microsecond precision
RootDelay = 0, // Placeholder
RootDispersion = (uint)(10 * 65536 / 1000), // 10ms dispersion placeholder
ReferenceId = BitConverter.ToUInt32(IPAddress.Parse("127.0.0.1").GetAddressBytes(), 0), // 'LOCL'
ReferenceTimestamp = new NtpTimestamp(DateTime.UtcNow.AddMinutes(-5)), // Pretend last sync
OriginateTimestamp = clientPacket.TransmitTimestamp, // T1 from client
ReceiveTimestamp = new NtpTimestamp(receiptTime), // T2
TransmitTimestamp = new NtpTimestamp(DateTime.UtcNow) // T3
};
byte[] responseBytes = responsePacket.ToByteArray();
await _listener.SendAsync(responseBytes, responseBytes.Length, clientEndPoint);
LogMessage?.Invoke($"Sent response to {clientEndPoint}. T1_client:{responsePacket.OriginateTimestamp}, T2_server:{responsePacket.ReceiveTimestamp}, T3_server:{responsePacket.TransmitTimestamp}");
_logger.Info($"Sent NTP response to {clientEndPoint}.");
}
catch (ObjectDisposedException)
{
if (_isRunning)
{
LogMessage?.Invoke("NTP Listener socket was unexpectedly disposed while still supposed to be running.");
_logger.Info("NTP Listener socket was unexpectedly disposed while still supposed to be running.");
}
else
{
LogMessage?.Invoke("NTP Listener socket closed (server stopping).");
_logger.Info("NTP Listener socket closed (server stopping). Loop terminating.");
}
break;
}
catch (SocketException ex)
{
if (_isRunning)
{
LogMessage?.Invoke($"SocketException in listen loop: {ex.Message} (ErrorCode: {ex.SocketErrorCode}). Server continues to run.");
_logger.Info($"SocketException in listen loop: {ex.Message} (ErrorCode: {ex.SocketErrorCode}). Server continues to run.");
await Task.Delay(100);
}
else
{
LogMessage?.Invoke($"SocketException in listen loop (server stopping): {ex.Message}");
_logger.Info($"NTP Listener socket exception (server stopping): {ex.Message}. Loop terminating.");
break;
}
}
catch (Exception ex) // Catch all exceptions in the async void method to prevent app crash
{
LogMessage?.Invoke($"Unhandled error in NTP listen loop: {ex.Message}");
_logger.Error($"Unhandled error in NTP listen loop: {ex.Message}");
if (!_isRunning) break;
await Task.Delay(1000);
}
}
LogMessage?.Invoke("NTP Server listening loop ended.");
_logger.Info("NTP Server listening loop ended.");
}
}
基本就是在本地开启一个Udp监听,接收客户端传过来的NtpPacket包,然后返回一个回复的NtpPacket,在回复的包中,填写一些服务端相关的字段和设置。
NTPClientApp
NTPClientApp是一个时间同步客户端,它引用上面的NTPCore库。
界面设计如下:
<Window x:Class="NTPClientApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:NTPClientApp"
mc:Ignorable="d"
Title="NTP 时间同步客户端 (v1.1)" Height="720" Width="850" MinHeight="600" MinWidth="700"
Background="#FFF0F0F0" Foreground="#FF333333" FontFamily="Segoe UI">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="#FF0078D4"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="MinWidth" Value="80"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="3" SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FF005A9E"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#FF004578"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#FFCCCCCC"/>
<Setter Property="Foreground" Value="#FF666666"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TextBox">
<Setter Property="Padding" Value="5,3"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="BorderBrush" Value="#FFABADB3"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="MinHeight" Value="28"/>
</Style>
<Style TargetType="ListBox">
<Setter Property="Margin" Value="5"/>
<Setter Property="BorderBrush" Value="#FFABADB3"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style TargetType="ComboBox">
<Setter Property="Padding" Value="5,3"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="MinHeight" Value="28"/>
</Style>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="5,0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="GroupBox">
<Setter Property="Padding" Value="10"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="BorderBrush" Value="#FFABADB3"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" FontWeight="SemiBold" FontSize="14" Margin="0,0,0,5"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="0,0,10,0"/>
</Style>
</Window.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<!-- Server Management -->
<RowDefinition Height="Auto"/>
<!-- Sync Settings -->
<RowDefinition Height="Auto"/>
<!-- Status Info -->
<RowDefinition Height="*"/>
<!-- Log Output -->
<RowDefinition Height="Auto"/>
<!-- Status Bar -->
</Grid.RowDefinitions>
<GroupBox Header="NTP 服务器管理" Grid.Row="0">
<StackPanel>
<DockPanel Margin="0,0,0,10">
<Button x:Name="RemoveServerButton" DockPanel.Dock="Right" Content="移除选中" Click="RemoveServer_Click" Padding="10,3"/>
<Button x:Name="AddServerButton" DockPanel.Dock="Right" Content="添加" Click="AddServer_Click" Margin="5,5,0,5" Padding="10,3"/>
<TextBox x:Name="ServerAddressTextBox" Text="pool.ntp.org" VerticalAlignment="Center"/>
</DockPanel>
<ListBox x:Name="ServerListBox" Height="100" SelectionMode="Single" ItemsSource="{Binding NtpServers}" DisplayMemberPath="."/>
</StackPanel>
</GroupBox>
<GroupBox Header="同步设置" Grid.Row="1">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<RadioButton x:Name="SimpleIntervalRadio" GroupName="SyncMode" Content="简单间隔" IsChecked="True" Checked="SyncMode_Changed"/>
<RadioButton x:Name="AdvancedScheduleRadio" GroupName="SyncMode" Content="高级每日计划" Margin="20,0,0,0" Checked="SyncMode_Changed"/>
</StackPanel>
<StackPanel x:Name="SimpleIntervalPanel" Orientation="Horizontal">
<TextBlock Text="同步频率:"/>
<ComboBox x:Name="SyncIntervalComboBox" Width="120" SelectedIndex="2" SelectionChanged="Settings_Changed">
<!-- Default to 1 hour -->
<ComboBoxItem Content="5 分钟"/>
<ComboBoxItem Content="15 分钟"/>
<ComboBoxItem Content="30 分钟"/>
<ComboBoxItem Content="1 小时"/>
<ComboBoxItem Content="3 小时"/>
<ComboBoxItem Content="6 小时"/>
<ComboBoxItem Content="12 小时"/>
<ComboBoxItem Content="24 小时"/>
</ComboBox>
</StackPanel>
<StackPanel x:Name="AdvancedSchedulePanel" Visibility="Collapsed">
<TextBlock Text="在每天的以下时间段内,按指定频率同步:" Margin="0,0,0,5" TextWrapping="Wrap"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Text="开始时间:"/>
<TextBox Grid.Column="1" x:Name="StartTimeTextBox" Width="60" Text="02:00" ToolTip="HH:mm (24小时制)" LostFocus="Settings_Changed"/>
<TextBlock Grid.Column="2" Text="结束时间:" Margin="10,0,0,0"/>
<TextBox Grid.Column="3" x:Name="EndTimeTextBox" Width="60" Text="05:00" ToolTip="HH:mm (24小时制)" LostFocus="Settings_Changed"/>
<TextBlock Grid.Column="4" Text="同步间隔 (分钟):" Margin="10,0,0,0"/>
<TextBox Grid.Column="5" x:Name="AdvancedIntervalTextBox" Width="50" Text="30" ToolTip="例如: 15, 30, 60" LostFocus="Settings_Changed"/>
</Grid>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,15,0,0">
<Button x:Name="SyncNowButton" Content="立即同步" Click="SyncNow_Click" />
<Button x:Name="ToggleAutoSyncButton" Content="启动自动同步" Click="ToggleAutoSync_Click"/>
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox Header="状态信息" Grid.Row="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="当前本地时间:" FontWeight="SemiBold"/>
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="CurrentTimeTextBlock" Text="---"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="上次成功同步:" FontWeight="SemiBold"/>
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="LastSyncTextBlock" Text="从未"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="下次计划同步:" FontWeight="SemiBold"/>
<TextBlock Grid.Row="2" Grid.Column="1" x:Name="NextSyncTextBlock" Text="---"/>
</Grid>
</GroupBox>
<GroupBox Header="同步日志 (UI)" Grid.Row="3">
<ListBox x:Name="LogListBox" ItemsSource="{Binding UiLogMessages}" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto"/>
</GroupBox>
<StatusBar Grid.Row="4" Background="#FFE0E0E0">
<StatusBarItem>
<TextBlock x:Name="StatusTextBlock" Text="就绪。请以管理员权限运行以设置系统时间。"/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
核心代码SynchronizeTimeAsync如下:
private async void SyncNow_Click(object sender, RoutedEventArgs e)
{
LogToUiAndFile("手动同步已触发。", "INFO");
await SynchronizeTimeAsync(true); // true for manual sync
}
private async Task SynchronizeTimeAsync(bool isManualSync)
{
if (ServerListBox.SelectedItem == null || string.IsNullOrWhiteSpace(ServerListBox.SelectedItem.ToString()))
{
LogToUiAndFile("错误: 请先从列表中选择一个NTP服务器进行同步。", "WARN");
MessageBox.Show(this, "请选择一个NTP服务器。", "未选择服务器", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
string selectedServer = ServerListBox.SelectedItem.ToString();
LogToUiAndFile($"开始同步时间 ({(isManualSync ? "手动" : "计划")}) 从 {selectedServer}...", "INFO");
StatusTextBlock.Text = $"正在连接到 {selectedServer}...";
SetUiEnabled(false);
try
{
NtpClient client = new NtpClient(selectedServer, logger: _logger); // Pass logger
NtpPacket packet = await client.GetTimeAsync(); // This method now has retries and throws on final failure
if (packet == null) // Should be caught by GetTimeAsync's throw, but as a safeguard
{
throw new Exception($"无法从 {selectedServer} 获取NTP数据包,所有重试均失败。");
}
TimeSpan offset = packet.CalculateOffset(); // Can throw InvalidOperationException
DateTime correctedUtcTime = DateTime.UtcNow + offset;
LogToUiAndFile($"从 {selectedServer} 获取时间成功。偏移: {offset.TotalMilliseconds:F3} ms. 包详情: {packet}", "INFO");
LogToUiAndFile($"当前UTC时间: {DateTime.UtcNow:O}, 校正后UTC时间: {correctedUtcTime:O}", "DEBUG");
LogToUiAndFile($"尝试设置系统时间...", "INFO");
if (SystemTimeSetter.SetSystemUtcTime(correctedUtcTime))
{
LastSuccessfulSyncTime = DateTime.Now; // Record local time of this successful sync
LogToUiAndFile("系统时间已成功校准!", "INFO");
StatusTextBlock.Text = $"时间已与 {selectedServer} 同步。偏移: {offset.TotalMilliseconds:F3} ms";
if (_isAutoSyncEnabled && !isManualSync && SimpleIntervalRadio.IsChecked == true) // If scheduled simple sync
{
CalculatedNextSyncTime = DateTime.Now + GetSimpleInterval();
LogToUiAndFile($"下次简单间隔同步计划在: {CalculatedNextSyncTime:G}", "DEBUG");
}
else if (_isAutoSyncEnabled && !isManualSync && AdvancedScheduleRadio.IsChecked == true)
{
// For advanced, next sync is determined by interval from LastSuccessfulSyncTime within the window.
// MasterTimer_Tick will re-evaluate on its next pass.
// Setting a specific _calculatedNextSyncTime here might be complex if it falls outside window.
// MasterTimer will handle it.
LogToUiAndFile("高级计划同步完成。下次同步将按计划确定。", "DEBUG");
}
}
else
{
throw new Exception($"设置系统时间失败。Win32错误码: {Marshal.GetLastWin32Error()}. 请确保以管理员权限运行程序。");
}
}
catch (InvalidOperationException opEx) // Specific to NtpPacket calculations
{
LogToUiAndFile($"同步计算错误: {opEx.Message}. 服务器返回的时间戳可能无效。", "ERROR", opEx);
StatusTextBlock.Text = "同步错误: 时间戳无效。";
MessageBox.Show(this, $"无法完成时间同步,服务器返回的时间戳无效或不完整。\n\n错误: {opEx.Message}", "同步计算错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
catch (Exception ex)
{
LogToUiAndFile($"同步失败: {ex.Message}", "ERROR", ex);
StatusTextBlock.Text = "同步失败。请查看文件日志获取详细信息。";
MessageBox.Show(this, $"无法完成时间同步。\n\n错误: {ex.Message}\n\n请检查您的网络连接、NTP服务器地址、程序权限以及文件日志 (logs\\client.log)。", "同步失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
SetUiEnabled(true);
UpdateStatusTextBlocks(); // Refresh UI status
}
}
项目中使用了log4net记录日志,用户的配置保存在了配置文件中。
▲时间同步客户端界面,NTP授时服务器地址设置为了另一台IP地址为192.168.6.200的服务器,上面部署了授时服务程序
NTPServerApp
授时服务器的界面设计如下:
<Window x:Class="NTPServerApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:NTPServerApp"
mc:Ignorable="d"
Title="NTP 授时服务器" Height="450" Width="600"
Background="#FFF0F0F0" Foreground="#FF333333" FontFamily="Microsoft YaHei UI">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="#FF007ACC"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="3">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FF005A9E"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#FFCCCCCC"/>
<Setter Property="Foreground" Value="#FF666666"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ListBox">
<Setter Property="Margin" Value="5"/>
<Setter Property="BorderBrush" Value="#FFABADB3"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="5,0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="GroupBox">
<Setter Property="Padding" Value="10"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="BorderBrush" Value="#FFABADB3"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
</Window.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<Button x:Name="StartServerButton" Content="启动服务器" Click="StartServer_Click"/>
<Button x:Name="StopServerButton" Content="停止服务器" Click="StopServer_Click" IsEnabled="False"/>
<TextBlock Text="服务器状态:" Margin="15,5,5,5" FontWeight="SemiBold"/>
<TextBlock x:Name="ServerStatusTextBlock" Text="已停止" Foreground="DarkRed"/>
</StackPanel>
<GroupBox Header="服务器日志" Grid.Row="1">
<ListBox x:Name="LogListBox" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
</GroupBox>
<StatusBar Grid.Row="2" Background="#FFE0E0E0">
<StatusBarItem>
<TextBlock Text="NTP 服务器 (UDP 端口 123)"/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
代码如下:
public partial class MainWindow : Window
{
private SimpleNtpServer _ntpServer;
public ObservableCollection<string> LogMessages { get; set; }
public MainWindow()
{
InitializeComponent();
LogMessages = new ObservableCollection<string>();
LogListBox.ItemsSource = LogMessages;
UpdateServerStatus(false); // Initially stopped
}
private void StartServer_Click(object sender, RoutedEventArgs e)
{
if (_ntpServer == null)
{
_ntpServer = new SimpleNtpServer(); // Uses default port 123
_ntpServer.LogMessage += Server_LogMessage;
}
try
{
_ntpServer.Start(); // This method is synchronous in the provided NTPCore but handles async internally.
// If Start itself were async, you'd await it.
UpdateServerStatus(true);
}
catch (Exception ex)
{
Log($"启动服务器失败: {ex.Message}");
UpdateServerStatus(false);
}
}
private async void StopServer_Click(object sender, RoutedEventArgs e)
{
if (_ntpServer != null)
{
Log("正在停止服务器...");
await _ntpServer.StopAsync(); // StopAsync is asynchronous
if (_ntpServer != null) // Check again, as StopAsync might nullify it or LogMessage could be called after disposal
{
_ntpServer.LogMessage -= Server_LogMessage; // Unsubscribe
}
_ntpServer = null;
UpdateServerStatus(false);
// Log("服务器已停止。"); // StopAsync already logs this.
}
}
private void Server_LogMessage(string message)
{
Log(message);
}
private void Log(string message)
{
Dispatcher.Invoke(() =>
{
string logEntry = $"[{DateTime.Now:HH:mm:ss}] {message}";
LogMessages.Add(logEntry);
if (LogMessages.Count > 200) // Limit log history
{
LogMessages.RemoveAt(0);
}
// Auto-scroll to the bottom of the ListBox
if (VisualTreeHelper.GetChildrenCount(LogListBox) > 0)
{
var border = (Border)VisualTreeHelper.GetChild(LogListBox, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
});
}
private void UpdateServerStatus(bool isRunning)
{
if (isRunning)
{
ServerStatusTextBlock.Text = "运行中";
ServerStatusTextBlock.Foreground = Brushes.DarkGreen;
StartServerButton.IsEnabled = false;
StopServerButton.IsEnabled = true;
}
else
{
ServerStatusTextBlock.Text = "已停止";
ServerStatusTextBlock.Foreground = Brushes.DarkRed;
StartServerButton.IsEnabled = true;
StopServerButton.IsEnabled = false;
}
}
protected override async void OnClosing(CancelEventArgs e)
{
if (_ntpServer != null)
{
// Prevent window from closing immediately if server is stopping
e.Cancel = true; // Cancel the close for now
StopServerButton.IsEnabled = false; // Disable stop button during forced stop
StartServerButton.IsEnabled = false;
Log("应用程序正在关闭,正在停止NTP服务器...");
await _ntpServer.StopAsync(); // Ensure server is stopped
if (_ntpServer != null)
{
_ntpServer.LogMessage -= Server_LogMessage;
}
_ntpServer = null;
Log("NTP服务器已停止。应用程序现在将关闭。");
// Allow closing now
e.Cancel = false;
Application.Current.Shutdown(); // Force shutdown if needed or just let it close
}
base.OnClosing(e);
}
}
运行界面如下:
▲NTP授时服务端程序界面