dotnet6 C# 一个国内还能用的 NTP 时间校准客户端的实现
本文来记录一个我自己在使用的 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);
}
可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 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 时间校准客户端的实现的更多相关文章
- 国内常用的几个NTP时间服务器
问题描述: 经常Windows或者Linux系统上面的时间跟我们本地的时间不一致 有时候就是Windows的Internet时间设置里面的Windows自带的时间同步服务器不好使 Linux配置NTP ...
- 为你的pip更换一个国内的镜像源
为你的pip更换一个国内的镜像源 是否常常为pypi官网被无故和谐掉导致pip不能下载python的各个包而痛心疾首? 是否常常在深夜里看着pip install 下载包的速度慢如乌龟而长吁短叹? 是 ...
- mysql实战45讲 (三) 事务隔离:为什么你改了我还看不见 极客时间读书笔记
提到事务,你肯定不陌生,和数据库打交道的时候,我们总是会用到事务.最经典的例子就是转账,你要给朋友小王转100块钱,而此时你的银行卡只有100块钱. 转账过程具体到程序里会有一系列的操作,比如查询余额 ...
- 还在用SimpleDateFormat格式化时间?小心经理锤你
还在用SimpleDateFormat格式化时间?小心经理锤你 场景 本来开开心心的周末时光,线上突然就疯狂报错,以为程序炸了,截停日志,发现是就是类似下述一段错误 java.lang.NumberF ...
- 从一个国内普通开发者的视角谈谈Sitecore
一.Sitecore是个神马玩意 简而言之,Sitecore就是一个基于ASP.NET技术的CMS系统,它不仅具有传统Web CMS的所有功能,还集成了Marketing营销(当然,这个功能价格不菲) ...
- 充满未来和科幻的界面设计FUI在国内还没有起步在国外早起相当成熟
所谓FUI可以是幻想界面(Fantasy User Interfaces).科幻界面(Fictional User Interfaces).假界面(Fake User Interfaces).未来主义 ...
- 发现一个国内牛逼的maven仓库,速度真的太快了
前天网上下了一个项目,在公司还好,网络比较流畅,很快就把依赖下好了:回家的时候,想耍耍,结果下了一天也没把依赖下好,速度是几k每秒,甚至一k每秒,哎~心都碎了,网上一搜,结果发现了一个惊天的用nexu ...
- pip操作以及window和虚拟机中为pip更换一个国内的镜像源的方法
前言 在学习PyQt5的过程中,参考王硕和孙洋洋的PyQt5快速开发与实战中,看到的关于Python开发技巧与实战,觉得挺好的 所以将其摘抄了下来方便阅读.之后还有一个关于更换pip镜像源的方法,方便 ...
- 分享一个国内首个企业级开源的GO语言网关--GoKu API Gateway
一. 简介 GoKu API Gateway,中文名:悟空API网关,是国内首个开源go语言API网关,帮助企业进行API服务治理与API性能安全维护,为企业数字化赋能. GoKu API Gatew ...
- 一个 static 还能难得住我?
static 是我们日常生活中经常用到的关键字,也是 Java 中非常重要的一个关键字,static 可以修饰变量.方法.做静态代码块.静态导包等,下面我们就来具体聊一聊这个关键字,我们先从基础开始, ...
随机推荐
- uni组件传值注意
目录介绍 01.组件传值遇到坑 02.父组件传值给子组件 03.子组件传值给父组件 01.组件传值遇到坑 子组件给父组件传值注意点 注意子组件触发事件定义的方法,首先在父组件中需要绑定子组件内部对应事 ...
- KingbaseES V8R6 集群中复制槽非活跃状态的可能原因
背景 此问题环境是一主五备物理集群,其中node1是主节点,node2,3是集群同步节点,node4,5是集群异地异步节点,由于异地和主节点不同网段,网速非常慢. kdts-plus工具纯迁数据,每分 ...
- KingbaseES例程_普通表在线转分区表(基于规则)
KingbaseES例程_普通表在线转分区表 概述 普通表转分区表,使用视图的替换式规则,以路由方式,实现在线转移数据. 数据准备 /*普通大表*/ create table tab_single a ...
- UE4蓝图对Actor的引用
通过关卡蓝图调用 在关卡中放置一个Actor,在关卡蓝图中右键 create a reference to actor,即可 注意使用该方法创建时,需要现在关卡中选择上该类Actor 当Actor生成 ...
- Java实现哈希表
2.哈希表 2.1.哈希冲突 冲突位置,把数据构建为链表结构. 装载因子=哈希表中的元素个数 / (散列表)哈希表的长度 装载因子越大,说明链表越长,性能就越低,那么哈希表就需要扩容,把数据迁移到新的 ...
- Java实现软件设计模式---抽象工厂模式(性别产品等级结构肤色产品族)
一.题目要求 二.画出对应的类图 三.文件目录结构 四.具体实现代码 Black.java 1 package com.a004; 2 3 public class Black implements ...
- #主席树,并查集#CodeChef Sereja and Ballons
SEABAL 分析 考虑用并查集维护当前连续被打破的气球段,那么每次新增的区间就是 \([l_{x-1},x]\) 到 \([x,r_{x+1}]\) 的连接. 只要 \(l,r\) 分别满足在这之间 ...
- #树形dp#洛谷 4395 [BOI2003]Gem 气垫车
题目 给出一棵树,要求你为树上的结点标上权值,权值可以是任意的正整数 唯一的限制条件是相邻的两个结点不能标上相同的权值,要求一种方案,使得整棵树的总价值最小. 分析 每个结点的权值最大可能为 \(\l ...
- #01背包#洛谷 2340 [USACO03FALL]Cow Exhibition G
题目 有\(n\)个物品,对于第\(i\)个物品, 有两种属性,第一种属性为\(x_i\),第二种属性为\(y_i\) 问选择若干个物品使得\(\sum{x_j}\geq 0\)且\(\sum{y_j ...
- #线段树,二分#洛谷 2824 [HEOI2016/TJOI2016]排序
题目 分析 这排序就很难实现,考虑定一个基准,小于该基准的视为0,否则视为1, 那排序可以看作将0和1分开,这就很好用线段树实现了 如果该位置是0,说明这个基准太高,显然可以用二分答案(基准),那么时 ...