获取安全时间
“安全时间”的意义大多在于信息安全上,也可以为用户提供更加准确的时间服务。本文主要探讨如何利用网络时间协议 (Network Time Protocol, NTP)来进行网络授时 (Time signal)。NTP 可以适应网络的延迟,从而最大化的保证用户获取到的时间的准确性。其同步算法和原理参见这里 (Clock synchronization algorithm)。
在写本文之前,原本想使用国家授时中心提供的时间数据。但由于本人愚钝,没能在官方网站找到相关的授时接口,所以采用了全球通用的 NTP 来进行网络授时。
获取时间:根据 RFC 2030(已过时,最新为 RFC 5905),NTP 可以利用 UDP 协议工作在 IPv4 和 IPv6 上。通过向 NTP 服务器发送一个请求,服务器给你返回一个时间响应。数据包格式在 RFC 2030 的“4. NTP Message Format”章节进行了定义。
在“5. SNTP Client Operations”章节说明了用户如何向服务器发送请求。因为“A unicast or anycast client initializes the NTP message header, sends the request to the server and strips the time of day from the Transmit Timestamp field of the reply. For this purpose, all of the NTP header fields shown above can be set to 0, except the first octet and (optional) Transmit Timestamp fields. In the first octet, the LI field is set to 0 (no warning) and the Mode field is set to 3 (client). The VN field must agree with the version number of the NTP/SNTP server”,所以我们可以简单的把第一个字节的数据置为 0x1b (0001 1011),即:LI=0, VN=4, Mode=3,其他数据一律置零。
接下来服务器会返回相关报文。RFC 2030 给出的时间计算公式:d = (T4 - T1) - (T2 - T3) t = ((T2 - T1) + (T3 - T4)) / 2,最终我们可以得到相对比较精确的本地时间。简化后的公式近似为(服务器的timespan+本机等待时间/2)。
跳动时间:通过一个线程,每隔一个安全周期主动向服务器获取一次时间;或者在本机发现时间出现过大跳动的时候也向服务器获取一次时间。在获取时间时如果发生了异常,从列表中挑选下一个地址作为服务器地址。
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | public static class SafeDateTime { const int SAFE_CYCLE = 7200; const int CYCLE_INTERVAL = 500; const int ALLOW_DIFF = 650; readonly static string [] _hosts = new [] { // list of NTP servers "ntp.sjtu.edu.cn" , // 上海交通大学 "time-nw.nist.gov" , // Microsoft, Redmond, Washington "s1a.time.edu.cn" , // 北京邮电大学 "time-b.timefreq.bldrdoc.gov" , // NIST, Boulder, Colorado "133.100.11.8" , // 日本 福冈大学 }; readonly static IPEndPoint[] _eps = null ; static int _sIndex = 0; static int _safeCycle = 0; static bool _onGetingTime = false ; static DateTime _localUtcTime; static DateTime _networkUtcTime; static SafeDateTime() { // convert hosts to IPEndPoints //_eps = _hosts.Select(s => (IEnumerable<IPAddress>)Dns.GetHostAddresses(s)).Aggregate((x, y) => x.Concat(y)).Select(s => new IPEndPoint(s, 123)).ToArray(); var list = new List<IPEndPoint>(); foreach ( var host in _hosts) { try { foreach ( var ip in Dns.GetHostAddresses(host)) list.Add( new IPEndPoint(ip, 123)); } catch { } } _eps = list.ToArray(); new Thread(() => { var currentThread = Thread.CurrentThread; currentThread.Priority = ThreadPriority.Highest; currentThread.IsBackground = true ; DateTime lastSafeTime = DateTime.MinValue; DateTime currentSafeTime = DateTime.MinValue; while ( true ) { if (_safeCycle-- <= 0) // expire the safe times { AsyncNetworkUtcTime(); _safeCycle = SAFE_CYCLE; } else { currentSafeTime = GetSafeDateTime(); var diff = (currentSafeTime - lastSafeTime).Ticks; if (Math.Abs(diff) > ALLOW_DIFF) // out of threshold AsyncNetworkUtcTime(); } lastSafeTime = GetSafeDateTime(); Thread.Sleep(CYCLE_INTERVAL); } }).Start(); } private static DateTime GetNetworkUtcTime(IPEndPoint ep) { Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); s.Connect(ep); byte [] ntpData = new byte [48]; // RFC 2030 ntpData[0] = 0x1B; //for (int i = 1; i < 48; i++) // ntpData[i] = 0; // t1, time request sent by client // t2, time request received by server // t3, time reply sent by server // t4, time reply received by client long t1, t2, t3, t4; t1 = DateTime.UtcNow.Ticks; s.Send(ntpData); s.Receive(ntpData); t4 = DateTime.UtcNow.Ticks; s.Close(); t2 = ParseRaw(ntpData, 32); t3 = ParseRaw(ntpData, 40); long d = (t4 - t1) - (t3 - t2); // roundtrip delay long ticks = (t3 + (d >> 1)); var timeSpan = TimeSpan.FromTicks(ticks); var dateTime = new DateTime(1900, 1, 1) + timeSpan; return dateTime; // return Utc time } private static long ParseRaw( byte [] ntpData, int offsetTransmitTime) { ulong intpart = 0; ulong fractpart = 0; for ( int i = 0; i <= 3; i++) intpart = (intpart << 8) | ntpData[offsetTransmitTime + i]; for ( int i = 4; i <= 7; i++) fractpart = (fractpart << 8) | ntpData[offsetTransmitTime + i]; ulong milliseconds = (intpart * 1000 + (fractpart * 1000) / 0x100000000L); return ( long )milliseconds * TimeSpan.TicksPerMillisecond; } private static void AsyncNetworkUtcTime() { if (_onGetingTime) // simple to avoid thread conflict return ; _onGetingTime = true ; bool fail = true ; do { try { _networkUtcTime = GetNetworkUtcTime(_eps[_sIndex]); _localUtcTime = DateTime.UtcNow; fail = false ; } catch { _sIndex = (_sIndex + 1) % _eps.Length; } } while (fail); _onGetingTime = false ; } public static DateTime GetSafeDateTime() { var utcNow = DateTime.UtcNow; var interval = utcNow - _localUtcTime; return (_networkUtcTime + interval).ToLocalTime(); } } |
线程安全:在更新 _networkUtcTime 和 _localUtcTime 的时候可能会导致线程不安全,但是发生冲突概率极低。可以根据情况适当的增加线程锁。
PS:未尽事宜,请自行阅读源代码和 RFC 文档进行理解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异