文章目錄
  1. 1. 前言
  2. 2. 需求
  3. 3. 时间
    1. 3.1. UTC时间
    2. 3.2. Unix时间戳
  4. 4. 网络对时
    1. 4.1. 实战
  5. 5. 重点知识
  6. 6. 引用
  7. 7. Github

前言

游戏开发过程中经常会设计到时间的运算判定,比如开服时间,触发时间,倒计时时间等,所有的这些时间都要基于和服务器或网络对时后的网络时间为基准来计算。单纯采用本地客户端时间的话会导致通过修改本地客户端时间就能达到加速和调时间的目的。本章节通过深入学习网络对时,实现一套没有服务器也能正确网络对时的对时框架(如果有后端的话以后端时间对时为准即可,本人是出于不懂服务器开发所以才采用网络对时的方案),为未来自己的独立游戏的网络对时打基础。

需求

  1. 通过网络实现对时,确保本地计算用的时间是准确无误不会受本地调时间影响的

时间

UTC时间

在了解同步时间之前,让我们先来了解下什么是时间。

平时我们说的时间年月日时分秒,这个时间是从哪里来的了?为什么不同的国家有不同的时间了?为什么要区分时区了?

带着这些疑问来看以下这篇文章介绍:

网络时间同步是怎么实现的?怎样消除延迟带来的影响?

这里我直接跳到重要的理论知识和结论上来。

世界标准时间:

  1. 世界时:基于天文现象 + 钟表计时,永远与地球自转时间相匹配
  2. 国际原子时:基于原子钟计时,每一秒的周期完全等长且固定

原子时非常稳定,但世界时随着地球自转变慢,会越来越慢. 科学家通过「闰秒」的方式来矫正这个误差,从而实现世界时+国际原子时混合的方式得到准确的计时。

上面说的基于原子时 + 世界时「协调」得出的时间就是我们通常说的UTC时间。有了UTC标准时间,各个国家的时间就是基于UTC+-的方式的出来的,也就是我们通常说的时区时间(e.g. 北京时间,巴黎时间……)。*当我们开发多个国家的游戏的时候,为了确保时间的统一,这个时候我们就可以采用UTC时间作为时间标准,每个国家的时间戳计算标准都是统一的(不需要考虑时区问题)。

在了解了UTC(统一标准时间)的来路后,那么接下来就是本文的重点如何同步网络时间。

Unix时间戳

时间戳适用于表示一个时间从固定时间点到当前时间的总秒数(或微秒数不同编程语言有不同)。

而编程里常用的则是Unix时间戳。

Unix时间戳是从UTC1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒。

Note:

  1. 时间戳没有时区之分
  2. Unix时间戳是从1970年1月1日0点0分0秒作为基准算起

网络对时

网络对时顾名思义需要通过网络进行对时访问,而网络的访问我们都知道会有网络延时,那么我们如何在网络延时的基础上做到正确的网络对时了?

前人已经为此提供了解决方案,那就是NTP(Network Time Protocol)(网络对时服务)

这里简单了解下NTP是如何做到网络时间同步的。

通过在网络报文上打「时间戳」的方式,然后配合计算网络延迟,从而修正本机的时间。

网络对时请求

根据图示可以计算出网络「传输延迟」,以及客户端与服务端的「时间差」:

网络延时 = (t4 - t1) - (t3 - t2)

时间差 = t2 - t1 - 网络延时 / 2

这个计算过程假设网络来回路径是对称的,并且时延相同。

可以看到通过包含发送,接收,返回和返回接收时间戳的方式,我们可以计算出网络延迟从而做到正确的同步的网络时间

为了确保逻辑正确,时间严格意义上是不允许倒退的。那通过NTP同步的时间会出现时光倒流的情况吗?

NTP为此提供了两种方式:

  1. ntpdate:一切以服务端时间为准,「强制修改」本机时间
  2. ntpd:采用「润物细无声」的方式修改本机时间,把时间差均摊到每次小的调整上,避免发生「时光倒流」

在了解了NTP的对时原理后,接下来让我们通过实战Socket网络请求利用NTP实现网络对时功能。

实战

在实现通过Socket访问网络请求时,先让我们了解一下NTP的报文格式:

NTP报文格式

了解了NTP的报文格式我们就能编写Socket代码请求访问获取对时相关信息:

对时NTP网址,我采用的是阿里云提供的地址:

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
/*
* Description: NTPHostConfig.cs
* Author: TonyTang
* Create Date: 2022/07/21
*/


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// NTPHostConfig.cs
/// NTP网址配置
/// </summary>
public static class NTPHostConfig
{
/// <summary>
/// NTP网址列表
/// </summary>
public static List<string> NTPHostList = new List<string>
{
"ntp2.aliyun.com",
"ntp3.aliyun.com",
"ntp4.aliyun.com",
"ntp5.aliyun.com",
"ntp6.aliyun.com",
"ntp7.aliyun.com",
};
}
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/*
* Description: NTPClient.cs
* Author: TonyTang
* Create Date: 2022/07/15
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;

/// <summary>
/// NTPClient.cs
/// NTP客户端
/// </summary>
public class NTPClient : SingletonTemplate<NTPClient>
{
/// <summary>
/// 对时网络地址(直接传IP地址时无值)
/// </summary>
public string Host
{
get;
private set;
}

/// <summary>
/// 对时IP连接地址
/// </summary>
public IPEndPoint IPEnd
{
get;
private set;
}

/// <summary>
/// NTP连接端口号
/// </summary>
private const int PortNumber = 123;

/// <summary>
/// NTP Socket
/// </summary>
private Socket mNTPSocket;

/// <summary>
/// NTP请求数据
/// </summary>
private byte[] mNtpSendData;

/// <summary>
/// NTP接受数据
/// </summary>
private byte[] mNtpReceiveData;

/// <summary>
/// <summary>
/// 服务器接收客户端时间请求时间起始位置
/// </summary>
private const int ServerReceivedTimePos = 32;

/// <summary>
/// 服务器回复时间起始位置
/// </summary>
private const int ServerReplyTimePos = 40;

/// <summary>
/// UTC时间戳基准时间
/// </summary>
private readonly DateTime UTCBaseTime = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);

/// <summary>
/// NTP网址列表
/// </summary>
private List<string> mNTPHostList;

/// <summary>
/// 时间同步是否成功
/// </summary>
private bool IsTimeSyncSuccess;

public NTPClient()
{
mNtpSendData = new byte[48];
// Setting the Leap Indicator, Version Number and Mode values
// LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
mNtpSendData[0] = 0x1B;
mNtpReceiveData = new byte[48];
IsTimeSyncSuccess = false;
}

/// <summary>
/// 设置NTP网址列表
/// </summary>
/// <param name="ntpHostList"></param>
public bool SetHostList(List<string> ntpHostList)
{
mNTPHostList = ntpHostList;
if (!HasHost())
{
Debug.LogError($"请勿设置空NTP网址列表!");
return false;
}
return true;
}

/// <summary>
/// 同步网络时间
/// </summary>
/// <returns></returns>
public bool SyncTime()
{
// 未来有服务器的话,改为和服务器对时
if(!HasHost())
{
Debug.Log($"没有有效NTP网址,对时失败!");
return false;
}
DateTime? syncDateTime = null;
for (int i = 0, length = mNTPHostList.Count; i < length; i++)
{
InitByHost(mNTPHostList[i]);
if (SyncTime(out syncDateTime))
{
IsTimeSyncSuccess = true;
TimeHelper.SetNowUTCTime((DateTime)syncDateTime);
return true;
}
}
IsTimeSyncSuccess = false;
Debug.LogError($"所有NTP地址都同步时间失败!");
return false;
}

/// <summary>
/// 网络对时是否成功
/// </summary>
/// <returns></returns>
public bool IsSyncTimeSuccess()
{
return IsTimeSyncSuccess;
}

/// <summary>
/// 初始化对时地址
/// </summary>
/// <param name="host"></param>
private bool InitByHost(string host)
{
Host = host;
IPEnd = null;
var ipAdresses = Dns.GetHostAddresses(Host);
if(ipAdresses != null && ipAdresses.Length > 0)
{
IPEnd = new IPEndPoint(ipAdresses[0], PortNumber);
return true;
}
else
{
Debug.LogError($"网络地址:{host}的IP解析错误!");
return false;
}
}

/// <summary>
/// 初始化对时IP地址
/// </summary>
/// <param name="ip"></param>
private bool InitByIP(string ip)
{
Host = null;
IPEnd = null;
var ipAdress = IPAddress.Parse(ip);
if (ipAdress != null)
{
IPEnd = new IPEndPoint(ipAdress, PortNumber);
return true;
}
else
{
Debug.LogError($"IP地址:{ip}解析错误!");
return false;
}
}

/// <summary>
/// 网络对时
/// </summary>
/// <param name="syncDateTime"></param>
/// <returns></returns>
private bool SyncTime(out DateTime? syncDateTime)
{
syncDateTime = null;
if (IPEnd != null)
{
try
{
mNTPSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
Debug.Log($"网址:{Host} IP地址:{IPEnd.ToString()}");
mNTPSocket.Connect(IPEnd);
mNTPSocket.ReceiveTimeout = 3000;
// 客户端发送时间
ulong clientSendTime = (ulong)DateTime.UtcNow.Millisecond;
Debug.Log($"客户端发送时间:{clientSendTime}");
mNTPSocket.Send(mNtpSendData);
Array.Clear(mNtpReceiveData, 0, mNtpReceiveData.Length);
var recceiveByteNumbers = mNTPSocket.Receive(mNtpReceiveData);
Debug.Log($"接受返回字节数:{recceiveByteNumbers}");
// 客户端接收时间
ulong clientReceiveTime = (ulong)DateTime.UtcNow.Millisecond;
Debug.Log($"客户端接收时间:{clientReceiveTime}");
mNTPSocket.Shutdown(SocketShutdown.Both);
// 服务器接受消息时间
var serverReceivedTime = GetMilliSeconds(mNtpReceiveData, ServerReceivedTimePos);
Debug.Log($"服务器接受消息时间:{serverReceivedTime}");
// 服务器返回消息时间
var serverReplyTime = GetMilliSeconds(mNtpReceiveData, ServerReplyTimePos);
Debug.Log($"服务器返回消息时间:{serverReplyTime}");
// 网路延时 = (客户端接收时间 - 客户端发送时间) - (服务器返回消息时间 - 服务器接受消息时间)
// 时间差 = 服务器接受消息时间 - 客户端发送时间 - 网络延时 / 2 = ((服务器接受消息时间 - 客户端发送时间) + (服务器返回消息时间 - 客户端接收时间)) / 2
// 当前同步服务器时间 = 客户端接收时间 + 时间差
var offsetTime = ((serverReceivedTime - clientSendTime) + (serverReplyTime - clientReceiveTime)) / 2;
var syncTime = clientReceiveTime + offsetTime;
syncDateTime = UTCBaseTime.AddMilliseconds(syncTime);
Debug.Log($"IP地址:{IPEnd.ToString()},当前同步UTC时间:{syncDateTime.ToString()}");
return true;
}
catch(SocketException e)
{
Debug.LogError($"IP地址:{IPEnd.ToString()}连接异常:{e.Message},ErrorCode:{e.ErrorCode}!");
}
finally
{
//关闭Socket并释放资源
mNTPSocket.Close();
}
return false;
}
else
{
Debug.LogError($"未初始化IP地址,网络对时失败!");
return false;
}
}

/// <summary>
/// 是否有NTP网址
/// </summary>
/// <returns></returns>
private bool HasHost()
{
return mNTPHostList != null && mNTPHostList.Count > 0;
}

/// <summary>
/// 获取指定偏移的时间戳
/// </summary>
/// <param name="byteDatas"></param>
/// <param name="byteOffset"></param>
/// <returns></returns>
private ulong GetMilliSeconds(byte[] byteDatas, int byteOffset)
{
// Note:
// 64bit时间戳,高32bit表示整数部分,低32bit表示小数部分
// 默认网络获取的时间戳是大端
ulong intPart = BitConverter.ToUInt32(mNtpReceiveData, ServerReplyTimePos);
ulong fractPart = BitConverter.ToUInt32(mNtpReceiveData, ServerReplyTimePos + 4);
// 转换成小端
if(ByteUtilities.IsLittleEndian)
{
// 注意只转换64里的低32位
intPart = ByteUtilities.SwapEndianU32(intPart);
fractPart = ByteUtilities.SwapEndianU32(fractPart);
}
return (intPart * 1000) + ((fractPart * 1000) / 0x100000000UL);
}
}
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 二进制辅助静态工具类
/// </summary>
public static class ByteUtilities
{
/// <summary>
/// 是否是小端
/// </summary>
public static bool IsLittleEndian = CheckLittleEndian();

/// <summary>
/// 当前设备是否是小端
/// </summary>
/// <returns></returns>
public static unsafe bool CheckLittleEndian()
{

int i = 1;
byte* b = (byte*)&i;
return b[0] == 1;
}

/// <summary>
/// UInt16大小端转换
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static ushort SwapEndianU16(ushort value)
{

return (ushort)((value & 0xFFU) << 8 | (value & 0xFF00U) >> 8);
}

/// <summary>
/// UInt32大小端转换
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static uint SwapEndianU32(uint value)
{

return (value & 0x000000FFU) << 24 | (value & 0x0000FF00U) << 8 |
(value & 0x00FF0000U) >> 8 | (value & 0xFF000000U) >> 24;
}

/// <summary>
/// UInt64大小端32位转换
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static ulong SwapEndianU32(ulong value)
{

return (value & 0x000000FFU) << 24 | (value & 0x0000FF00U) << 8 |
(value & 0x00FF0000U) >> 8 | (value & 0xFF000000U) >> 24;
}

/// <summary>
/// UInt64大小端64位转换
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static ulong SwapEndianU64(ulong value)
{

return (value & 0x00000000000000FFUL) << 56 | (value & 0x000000000000FF00UL) << 40 |
(value & 0x0000000000FF0000UL) << 24 | (value & 0x00000000FF000000UL) << 8 |
(value & 0x000000FF00000000UL) >> 8 | (value & 0x0000FF0000000000UL) >> 24 |
(value & 0x00FF000000000000UL) >> 40 | (value & 0xFF00000000000000UL) >> 56;
}
}

从上面的代码可以看出,我们为了确保对时的快速,选择了UDP而非TCP。

其次通过解析NTP网址返回的NTP报文信息(e.g. 服务器接受消息时间和服务器返回消息时间),我们可以计算出客户端和服务器的时间差,从而计算出正确的网络UTC时间。

网路延时 = (客户端接收时间 - 客户端发送时间) - (服务器返回消息时间 - 服务器接受消息时间)

时间差 = 服务器接受消息时间 - 客户端发送时间 - 网络延时 / 2 = ((服务器接受消息时间 - 客户端发送时间) + (服务器返回消息时间 - 客户端接收时间)) / 2

当前同步服务器时间 = 客户端接收时间 + 时间差

对时成功后,我们把对时的UTC时间设置到我们本地时间工具类里供访问使用:

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
/*
* Description: TimeHelper.cs
* Author: TonyTang
* Create Date: 2022/07/16
*/


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 时间静态类
/// </summary>
public static class TimeHelper
{
/// <summary>
/// 同步网络UTC时间
/// </summary>
private static DateTime? mSyncUTCTime;

/// <summary>
/// 同步网络本地时间
/// </summary>
private static DateTime? mSyncLocalTime;

/// <summary>
/// 设置当前UTC时间
/// </summary>
/// <param name="nowDateTime"></param>
public static void SetNowUTCTime(DateTime? nowDateTime)
{

mSyncUTCTime = nowDateTime;
mSyncLocalTime = nowDateTime != null ? ((DateTime)nowDateTime).ToLocalTime() : nowDateTime;
if (mSyncUTCTime != null)
{
Debug.Log($"同步当前UTC时间:{mSyncUTCTime.ToString()}");
Debug.Log($"同步当前时区时间:{mSyncLocalTime.ToString()}");
}
else
{
Debug.Log($"清空当前同步时间!");
}
}

/// <summary>
/// 获取当前同步UTC时间(没有成功同步返回本地UTC时间)
/// </summary>
/// <returns></returns>
public static DateTime GetNowUTCTime()
{

return mSyncUTCTime != null ? (DateTime)mSyncUTCTime : GetLocalNowUTCTime();
}

/// <summary>
/// 获取当前同步本地时区时间(没有成功同步返回本地时区时间)
/// </summary>
/// <returns></returns>
public static DateTime GetNowLocalTime()
{

return mSyncUTCTime != null ? ((DateTime)mSyncUTCTime).ToLocalTime() : GetLocalNowTime();
}

/// <summary>
/// 获取本地当前UTC时间
/// </summary>
/// <returns></returns>
public static DateTime GetLocalNowUTCTime()
{

return DateTime.UtcNow;
}

/// <summary>
/// 获取本地当前时间
/// </summary>
/// <returns></returns>
public static DateTime GetLocalNowTime()
{

return DateTime.Now;
}
}

来看看实战效果:

我把本地时间同步关闭,并修改时间从2022/07/22 00:06到2022/07/24/03:06时间

同步时间前:

同步时间前

同步时间后:

同步时间后

从上面可以看到通过NTP的对时后,我们成功获取到了网络的UTC时间。

但上面的代码还有一个问题,对时后的UTC时间是固定的,我们无法向DateTime.Now或DateTime.UtcNow的方式实时访问同步的UTC时间。

这个时候我们需要通过网络同步时间+本地运行时间=本地最新网络同步时间的方式计算出最新的本地网络同步时间,毕竟我们整个游戏开发过程中并不想一直去通过短连接同步网络时间。

关于本地运行时间Unity给我们提供了一个无关Time.scale变化的一个运行时间,那就是Time.realtimeSinceStartup(标识游戏启动开始算起经历的时间s,不受Time.scale影响)

当前同步网络时间公式:

当前同步网络时间 = 网络对时 + (本地运行时长 - 对时时本地运行时长)

当前同步网络UTC时间

最终代码如下:

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
/*
* Description: TimeHelper.cs
* Author: TonyTang
* Create Date: 2022/07/16
*/


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 时间静态类
/// </summary>
public static class TimeHelper
{
/// <summary>
/// 同步网络UTC时间
/// </summary>
private static DateTime? mSyncUTCTime;

/// <summary>
/// 同步网络本地时间
/// </summary>
private static DateTime? mSyncLocalTime;

/// <summary>
/// 同步时间时的本地运行时间
/// </summary>
private static float mSyncRealtime;

/// <summary>
/// 设置当前UTC时间
/// </summary>
/// <param name="nowDateTime"></param>
public static void SetNowUTCTime(DateTime? nowDateTime)
{

mSyncUTCTime = nowDateTime;
mSyncLocalTime = nowDateTime != null ? ((DateTime)nowDateTime).ToLocalTime() : nowDateTime;
if (mSyncUTCTime != null)
{
mSyncRealtime = Time.realtimeSinceStartup;
Debug.Log($"同步时间时本地运行时间:{mSyncRealtime}");
Debug.Log($"同步当前UTC时间:{mSyncUTCTime.ToString()}");
Debug.Log($"同步当前时区时间:{mSyncLocalTime.ToString()}");
}
else
{
Debug.Log($"清空当前同步时间!");
}
}

/// <summary>
/// 获取当前同步UTC时间(没有成功同步返回本地UTC时间)
/// </summary>
/// <returns></returns>
public static DateTime GetNowUTCTime()
{

if(mSyncUTCTime != null)
{
var syncUTCTime = (DateTime)mSyncUTCTime;
return syncUTCTime.AddSeconds(Time.realtimeSinceStartup - mSyncRealtime);
}
else
{
return GetLocalNowUTCTime();
}
}

/// <summary>
/// 获取当前同步本地时区时间(没有成功同步返回本地时区时间)
/// </summary>
/// <returns></returns>
public static DateTime GetNowLocalTime()
{

if (mSyncLocalTime != null)
{
var syncLocalTime = (DateTime)mSyncLocalTime;
return syncLocalTime.AddSeconds(Time.realtimeSinceStartup - mSyncRealtime);
}
else
{
return GetLocalNowTime();
}
}

/// <summary>
/// 获取本地当前UTC时间
/// </summary>
/// <returns></returns>
public static DateTime GetLocalNowUTCTime()
{

return DateTime.UtcNow;
}

/// <summary>
/// 获取本地当前时间
/// </summary>
/// <returns></returns>
public static DateTime GetLocalNowTime()
{

return DateTime.Now;
}
}

可以看到我们通过记录同步网络时间时的本地真实运行时间,在下一次获取当前网络时间时通过当前真实运行时间 - 同步网络时本地真实运行时间得出同步时间后真实运行的时间,从而计算出当前网络最新时间。

成功同步时间以后,如果我们制作的游戏要支持多个国家的话,我们需要统一游戏里的时间计算,这个时候我们会用到UTC时间和时间戳的概念。结合前面时间戳的介绍,我知道Unix时间戳是基于1970年1月1日0点0分0秒,那么通过网络UTC对时后,我们可以成功计算出网络对时的时间戳:

当前时间戳 = 当前同步网络时间 - Unix时间戳基准时间(1970/1/1)

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
/// <summary>
/// Unix时间戳基准时间
/// </summary>
private static DateTime UnixBaseTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

var offsetTimeSpan = (DateTime)mSyncUTCTime - UnixBaseTime;
mSyncUTCTimeStamp = (long)offsetTimeSpan.TotalSeconds;
Debug.Log($"同步时间UTC时间戳:{mSyncUTCTimeStamp}");

/// <summary>
/// 获取当前同步UTC时间戳(没有成功同步返回本地UTC时间戳)
/// </summary>
/// <returns></returns>
public static long GetNowUTCTimeStamp()
{

var nowUTCTime = GetNowUTCTime();
var offsetSeconds = nowUTCTime - UnixBaseTime;
return (long)offsetSeconds.TotalSeconds;
}

/// <summary>
/// 获取本地当前UTC时间戳
/// </summary>
/// <returns></returns>
public static long GetLocalNowUTCTimeStamp()
{

return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}

同步UTC时间前时间戳:

同步UTC时间前时间戳

同步UTC时间后时间戳:

同步UTC时间后时间戳

可以看到我们通过和Unix时间戳基准时间(1970年1月1月0时0分0秒)成功计算出了当前对时的UTC时间戳

重点知识

  1. 同步网络对网络要求不高,可以采用UDP
  2. NTP报文里有服务器相关获得请求时间和服务求返回请求时间,通过这些数据的解析我们能计算出真实的网络时间
  3. NTP里的时间戳是基于1900年1月1日,而非像Unix时间戳基于1970年1月1日
  4. 本地最新网络时间需要通过记录对时时的运行时间来计算最新的当前网络时间
  5. 网络时间同步没必要一直同步,可以采用短连接在必要的时候同步一次

引用

网络时间同步是怎么实现的?怎样消除延迟带来的影响?

统一标准时间(UTC)

网络时间协议(Network Time Protocol, 缩写NTP)

通过NTP协议进行时间同步

ntp原理及客户端实现

客户端秒级时间同步方案

Unix时间戳

Github

SyncTime

文章目錄
  1. 1. 前言
  2. 2. 需求
  3. 3. 时间
    1. 3.1. UTC时间
    2. 3.2. Unix时间戳
  4. 4. 网络对时
    1. 4.1. 实战
  5. 5. 重点知识
  6. 6. 引用
  7. 7. Github