本文来记录一个我自己在使用的 NTP 时间校准客户端的实现

核心方法是在国内使用 腾讯 和 阿里 提供的 NTP 时间服务器来获取网络时间,如果连接不上,再依次换成 国家服务器 和 中国授时 服务,如果再连不上,那就换成微软自带的 time.windows.com 服务

从 NTP 服务上获取当前的网络时间,可采用 RFC 2030 提供的协议的方法,此方法只需要发送一条 UDP 消息和接收一条消息即可。服务器端返回的是相对于 1900.1.1 的毫秒时间

我从 https://github.com/michaelschwarz/NETMF-Toolkit/blob/095b01679945c3f518dd52082eca78bbaff9811f/NTP/NtpClient.cs 找到了核心实现方法,然后进行了一些魔改,改动核心是优化了异步

下面是修改之后的代码

// https://github.com/michaelschwarz/NETMF-Toolkit/blob/095b01679945c3f518dd52082eca78bbaff9811f/NTP/NtpClient.cs
public static class NtpClient
{
/// <summary>
/// 国内的授时服务提供的网络时间。默认返回北京时区的时间。如需转换为本机时区时间,请使用 <code> var dateTimeOffset = NtpClient.GetChineseNetworkTime();var 本机时区时间 = dateTimeOffset.LocalDateTime;</code> 转换。本机时区时间和北京时间的差别是,本机系统时区可能被设置为非北京时间,当本机系统时区设置为北京时间,则本机时区时间和北京时间相同
/// </summary>
/// <remarks>实现方法是去询问腾讯和阿里的授时服务器</remarks>
/// <returns>返回空表示没有能够获取到任何的时间,预计是网络错误了。返回北京时区的时间</returns>
/// 本来想着异常对外抛出的,但是似乎抛出异常也没啥用
public static async ValueTask<DateTimeOffset?> GetChineseNetworkTime()
{
// 感谢 [国内外常用公共NTP网络时间同步服务器地址_味辛的博客-CSDN博客_ntp服务器](https://blog.csdn.net/weixin_42588262/article/details/82501488 )
var dateTimeOffset = await GetChineseNetworkTimeCore("ntp.tencent.com"); // 腾讯
dateTimeOffset ??= await GetChineseNetworkTimeCore("ntp.aliyun.com"); // 阿里
dateTimeOffset ??= await GetChineseNetworkTimeCore("cn.pool.ntp.org"); // 国家服务器
dateTimeOffset ??= await GetChineseNetworkTimeCore("cn.ntp.org.cn"); // 中国授时
dateTimeOffset ??= await GetChineseNetworkTimeCore("time.windows.com"); // time.windows.com 微软Windows自带 if (dateTimeOffset is not null)
{
return dateTimeOffset.Value.ToOffset(TimeSpan.FromHours(8));
}
else
{
return null;
} static async ValueTask<DateTimeOffset?> GetChineseNetworkTimeCore(string ntpServer)
{
var cancellationTokenSource = new CancellationTokenSource();
try
{
var hostEntry = await Dns.GetHostEntryAsync(ntpServer);
IPAddress[] addressList = hostEntry.AddressList; if (addressList.Length == 0)
{
// 被投毒了?那就换其他一个吧
return null;
} foreach (var address in addressList)
{
try
{
var ipEndPoint = new IPEndPoint(address, 123);
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(15)); return await GetNetworkUtcTime(ipEndPoint, cancellationTokenSource.Token);
}
catch
{
// 失败就继续换下一个
} if (!cancellationTokenSource.TryReset())
{
cancellationTokenSource.Dispose();
cancellationTokenSource = new CancellationTokenSource();
}
}
}
catch
{
// 失败就失败
// 本来想着异常对外抛出的,但是似乎抛出异常也没啥用
}
finally
{
cancellationTokenSource.Dispose();
} return null;
}
} /// <summary>
/// Gets the current DateTime from time-a.nist.gov.
/// </summary>
/// <returns>A DateTime containing the current time.</returns>
public static ValueTask<DateTimeOffset> GetNetworkUtcTime()
{
return GetNetworkUtcTime("time-a.nist.gov");
} /// <summary>
/// Gets the current DateTime from <paramref name="ntpServer"/>.
/// </summary>
/// <param name="ntpServer">The hostname of the NTP server.</param>
/// <returns>A DateTime containing the current time.</returns>
public static async ValueTask<DateTimeOffset> GetNetworkUtcTime(string ntpServer)
{
var hostEntry = await Dns.GetHostEntryAsync(ntpServer);
IPAddress[] address = hostEntry.AddressList; if (address == null || address.Length == 0)
{
throw new ArgumentException($"Could not resolve ip address from '{ntpServer}'.", "ntpServer");
} var ipEndPoint = new IPEndPoint(address[0], 123); return await GetNetworkUtcTime(ipEndPoint);
} /// <summary>
/// Gets the current DateTime form <paramref name="endPoint"/> IPEndPoint.
/// </summary>
/// <param name="endPoint">The IPEndPoint to connect to.</param>
/// <param name="token"></param>
/// <returns>A DateTime containing the current time.</returns>
public static async ValueTask<DateTimeOffset> GetNetworkUtcTime(IPEndPoint endPoint,
CancellationToken token = default)
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); await socket.ConnectAsync(endPoint, token); const int length = 48; // 实现方法请参阅 RFC 2030 的内容
var ntpData = ArrayPool<byte>.Shared.Rent(length); try
{
// 初始化数据
ntpData[0] = 0x1B;
for (int i = 1; i < length; i++)
{
ntpData[i] = 0;
} await socket.SendAsync(ntpData.AsMemory(0, length), token);
await socket.ReceiveAsync(ntpData.AsMemory(0, length), token); byte offsetTransmitTime = 40;
ulong intPart = 0;
ulong fractPart = 0; for (int i = 0; i <= 3; i++)
{
intPart = 256 * intPart + ntpData[offsetTransmitTime + i];
} for (int i = 4; i <= 7; i++)
{
fractPart = 256 * fractPart + ntpData[offsetTransmitTime + i];
} ulong milliseconds = (intPart * 1000 + (fractPart * 1000) / 0x100000000L); TimeSpan timeSpan = TimeSpan.FromMilliseconds(milliseconds); var dateTime = new DateTime(1900, 1, 1);
dateTime += timeSpan; var dateTimeOffset = new DateTimeOffset(dateTime, TimeSpan.Zero); return dateTimeOffset;
}
finally
{
ArrayPool<byte>.Shared.Return(ntpData);
}
}
}

以上代码使用返回值是 DateTimeOffset 类型,此 DateTimeOffset 和 DateTime 的最大差别在于 DateTimeOffset 是带时区的。回顾一下小学知识,北京时间是 +8 小时的时间。时间服务器返回的是 UTC 时区时间,也就是 +0 小时。这就是为什么上层函数使用了 dateTimeOffset.Value.ToOffset(TimeSpan.FromHours(8)); 代码的原因,将 UTC 时区修改为北京时区

以上代码的使用方法如下

        var dateTimeOffset = await NtpClient.GetChineseNetworkTime();

        if (dateTimeOffset is null)
{
Console.WriteLine("获取不到时间");
}
else
{
Console.WriteLine(dateTimeOffset);
Console.WriteLine(dateTimeOffset.Value.LocalDateTime); // 本机时区时间和北京时间的差别是,本机系统时区可能被设置为非北京时间,当本机系统时区设置为北京时间,则本机时区时间和北京时间相同
DateTime beijingTime = dateTimeOffset.Value.UtcDateTime.AddHours(8);
Console.WriteLine(beijingTime);
}

本文的代码放在githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 0a9b16e50faad9240b07f62064bc1f498b1d6619

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 0a9b16e50faad9240b07f62064bc1f498b1d6619

获取代码之后,进入 JakairhefeHajelaycaqa 文件夹

更多博客,请参阅我的 博客导航

dotnet6 C# 一个国内还能用的 NTP 时间校准客户端的实现的更多相关文章

  1. 国内常用的几个NTP时间服务器

    问题描述: 经常Windows或者Linux系统上面的时间跟我们本地的时间不一致 有时候就是Windows的Internet时间设置里面的Windows自带的时间同步服务器不好使 Linux配置NTP ...

  2. 为你的pip更换一个国内的镜像源

    为你的pip更换一个国内的镜像源 是否常常为pypi官网被无故和谐掉导致pip不能下载python的各个包而痛心疾首? 是否常常在深夜里看着pip install 下载包的速度慢如乌龟而长吁短叹? 是 ...

  3. mysql实战45讲 (三) 事务隔离:为什么你改了我还看不见 极客时间读书笔记

    提到事务,你肯定不陌生,和数据库打交道的时候,我们总是会用到事务.最经典的例子就是转账,你要给朋友小王转100块钱,而此时你的银行卡只有100块钱. 转账过程具体到程序里会有一系列的操作,比如查询余额 ...

  4. 还在用SimpleDateFormat格式化时间?小心经理锤你

    还在用SimpleDateFormat格式化时间?小心经理锤你 场景 本来开开心心的周末时光,线上突然就疯狂报错,以为程序炸了,截停日志,发现是就是类似下述一段错误 java.lang.NumberF ...

  5. 从一个国内普通开发者的视角谈谈Sitecore

    一.Sitecore是个神马玩意 简而言之,Sitecore就是一个基于ASP.NET技术的CMS系统,它不仅具有传统Web CMS的所有功能,还集成了Marketing营销(当然,这个功能价格不菲) ...

  6. 充满未来和科幻的界面设计FUI在国内还没有起步在国外早起相当成熟

    所谓FUI可以是幻想界面(Fantasy User Interfaces).科幻界面(Fictional User Interfaces).假界面(Fake User Interfaces).未来主义 ...

  7. 发现一个国内牛逼的maven仓库,速度真的太快了

    前天网上下了一个项目,在公司还好,网络比较流畅,很快就把依赖下好了:回家的时候,想耍耍,结果下了一天也没把依赖下好,速度是几k每秒,甚至一k每秒,哎~心都碎了,网上一搜,结果发现了一个惊天的用nexu ...

  8. pip操作以及window和虚拟机中为pip更换一个国内的镜像源的方法

    前言 在学习PyQt5的过程中,参考王硕和孙洋洋的PyQt5快速开发与实战中,看到的关于Python开发技巧与实战,觉得挺好的 所以将其摘抄了下来方便阅读.之后还有一个关于更换pip镜像源的方法,方便 ...

  9. 分享一个国内首个企业级开源的GO语言网关--GoKu API Gateway

    一. 简介 GoKu API Gateway,中文名:悟空API网关,是国内首个开源go语言API网关,帮助企业进行API服务治理与API性能安全维护,为企业数字化赋能. GoKu API Gatew ...

  10. 一个 static 还能难得住我?

    static 是我们日常生活中经常用到的关键字,也是 Java 中非常重要的一个关键字,static 可以修饰变量.方法.做静态代码块.静态导包等,下面我们就来具体聊一聊这个关键字,我们先从基础开始, ...

随机推荐

  1. uni组件传值注意

    目录介绍 01.组件传值遇到坑 02.父组件传值给子组件 03.子组件传值给父组件 01.组件传值遇到坑 子组件给父组件传值注意点 注意子组件触发事件定义的方法,首先在父组件中需要绑定子组件内部对应事 ...

  2. KingbaseES V8R6 集群中复制槽非活跃状态的可能原因

    背景 此问题环境是一主五备物理集群,其中node1是主节点,node2,3是集群同步节点,node4,5是集群异地异步节点,由于异地和主节点不同网段,网速非常慢. kdts-plus工具纯迁数据,每分 ...

  3. KingbaseES例程_普通表在线转分区表(基于规则)

    KingbaseES例程_普通表在线转分区表 概述 普通表转分区表,使用视图的替换式规则,以路由方式,实现在线转移数据. 数据准备 /*普通大表*/ create table tab_single a ...

  4. UE4蓝图对Actor的引用

    通过关卡蓝图调用 在关卡中放置一个Actor,在关卡蓝图中右键 create a reference to actor,即可 注意使用该方法创建时,需要现在关卡中选择上该类Actor 当Actor生成 ...

  5. Java实现哈希表

    2.哈希表 2.1.哈希冲突 冲突位置,把数据构建为链表结构. 装载因子=哈希表中的元素个数 / (散列表)哈希表的长度 装载因子越大,说明链表越长,性能就越低,那么哈希表就需要扩容,把数据迁移到新的 ...

  6. Java实现软件设计模式---抽象工厂模式(性别产品等级结构肤色产品族)

    一.题目要求 二.画出对应的类图 三.文件目录结构 四.具体实现代码 Black.java 1 package com.a004; 2 3 public class Black implements ...

  7. #主席树,并查集#CodeChef Sereja and Ballons

    SEABAL 分析 考虑用并查集维护当前连续被打破的气球段,那么每次新增的区间就是 \([l_{x-1},x]\) 到 \([x,r_{x+1}]\) 的连接. 只要 \(l,r\) 分别满足在这之间 ...

  8. #树形dp#洛谷 4395 [BOI2003]Gem 气垫车

    题目 给出一棵树,要求你为树上的结点标上权值,权值可以是任意的正整数 唯一的限制条件是相邻的两个结点不能标上相同的权值,要求一种方案,使得整棵树的总价值最小. 分析 每个结点的权值最大可能为 \(\l ...

  9. #01背包#洛谷 2340 [USACO03FALL]Cow Exhibition G

    题目 有\(n\)个物品,对于第\(i\)个物品, 有两种属性,第一种属性为\(x_i\),第二种属性为\(y_i\) 问选择若干个物品使得\(\sum{x_j}\geq 0\)且\(\sum{y_j ...

  10. #线段树,二分#洛谷 2824 [HEOI2016/TJOI2016]排序

    题目 分析 这排序就很难实现,考虑定一个基准,小于该基准的视为0,否则视为1, 那排序可以看作将0和1分开,这就很好用线段树实现了 如果该位置是0,说明这个基准太高,显然可以用二分答案(基准),那么时 ...