C#通過NTP服務器獲取NTP時間
注意事項:
- 如果NTP服務器地址是域名,如阿里云的NTP服務器地址。需要DNS解析。
- NTP使用UDP通訊,默認端口是123
- NTP經過很多年的發展,有4個版本號,目前常用的3和4。NTP區分客戶端和服務端,客戶端角色標志為3。
- NTP發送的時間戳是第41到48個字節。獲取到的字節需要轉為大端序列。
- NTP標準協議返回的時間是自1900.1.1 00:00:00開始的毫秒時間數值,需要字節轉換為你需要的日期時間。
以下是通過NTP服務器獲取NTP時間的代碼:
/// <summary>
/// 獲取NTP時間
/// </summary>
/// <param name="serverList"></param>
/// <param name="timeoutMilliseconds"></param>
/// <param name="isToLocal"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<DateTime> GetNtpTime(List<string> serverList, bool isToLocal = false, int timeoutMilliseconds = 2000)
{List<string> realServerList = new List<string>();bool needUseLastIp = false;if (serverList is null || !serverList.Any()){serverList = NtpServers;needUseLastIp = true;if (!string.IsNullOrEmpty(lastIpAddress)){realServerList.Add(lastIpAddress);if (serverList.Contains(lastIpAddress)){serverList.Remove(lastIpAddress);}}}realServerList.AddRange(serverList);Log.Information($"GetNtpTime realServerList: {JsonConvert.SerializeObject(realServerList)}");foreach (var server in realServerList){try{IPAddress? ipAddress;List<IPAddress> addressList = new List<IPAddress>();if (!IPAddress.TryParse(server, out ipAddress)){// 如果不是IP地址,則嘗試DNS解析try{var hostEntry = await Dns.GetHostEntryAsync(server);addressList = hostEntry.AddressList.ToList();}catch (Exception e){Log.Error($"DNS resolution failed: {e.Message} {e.StackTrace}");}}else{addressList.Add(ipAddress);}if (addressList is null || !addressList.Any()){continue;}foreach (var address in addressList){using (var client = new UdpClient()){client.Client.ReceiveTimeout = timeoutMilliseconds;client.Client.SendTimeout = timeoutMilliseconds;// 發送NTP請求var request = new byte[48];request[0] = 0x1B; // 設置版本號4,模式3(客戶端)var endPoint = new IPEndPoint(address, 123);client.Send(request, request.Length, endPoint);// 發送請求并接收響應var response = await client.ReceiveAsync();// 驗證響應數據長度if (response.Buffer.Length < 48 || !IsValidNtpResponse(response.Buffer))continue;// 解析時間戳(注意:BitConverter默認是小端,需要反轉)ulong intPart = BitConverter.ToUInt32(response.Buffer, 40);ulong fractPart = BitConverter.ToUInt32(response.Buffer, 44);// 轉換為大端序(NTP使用大端)intPart = SwapEndianness((uint)intPart);fractPart = SwapEndianness((uint)fractPart);// 計算總時間戳ulong ntpTimestamp = (intPart << 32) | (fractPart & 0xFFFFFFFF);double totalSeconds = (double)ntpTimestamp / (double)(1UL << 32);if (needUseLastIp){lastIpAddress = endPoint.Address.ToString();Log.Information($"記錄本次獲取NTP時間的IP為:{lastIpAddress}");}// 轉換為DateTimeDateTime epoch = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);DateTime ntpTime = epoch.AddSeconds(totalSeconds);if (isToLocal){ntpTime = ntpTime.ToLocalTime();}return ntpTime;}}}catch (Exception ex){Log.Error($"連接服務器 {server} 失敗: {ex.Message} {ex.StackTrace}");continue;}}throw new Exception("所有NTP服務器均無法連接!");
}
獲取到的時間,如果需要自己進行時區轉換,可以通過C#代碼從電腦本機獲取到標準的時區列表,然后將獲取到的NTP時間加上時區的BaseUtcOffset,即可轉換到所需要的時區時間。
C#獲取時區列表代碼如下:
var allZones = TimeZoneInfo.GetSystemTimeZones().ToList();