前言:
当我们需要准确的时间信息时,依赖于本地系统时间可能会带来不准确的结果,特别是在需要确保时间的一致性和精确性的应用中。为了解决这个问题,我们可以通过网络时间协议(NTP)服务器来获取网络时间,从而获得更准确的时间信息。本篇博客将介绍如何在Unity中使用C#编写代码来从NTP服务器获取网络时间。
一、什么是NTP?
NTP,全称网络时间协议(Network Time Protocol),是一种用于同步计算机网络中设备的时间的协议。它允许计算机通过互联网或局域网获取精确的时间信息,以确保设备在分布式系统中保持时间的一致性。NTP协议的工作原理基于一组公共的时间服务器,这些服务器提供了准确的时间信息。
二、获取NTP服务器地址
要从NTP服务器获取时间,首先需要知道可用的NTP服务器的地址。以下是一些常用的NTP服务器地址,这些地址通常是可用的:
pool.ntp.org
cn.pool.ntp.org
ntp1.aliyun.com
ntp2.aliyun.com
ntp3.aliyun.com
asia.pool.ntp.org
time.windows.com
time1.cloud.tencent.com
也可以从http://www.ntp.org.cn/pool这个网址中查找自己想要的服务器地址。
三、通过NTP服务器获取网络时间
1、同步方式获取网络时间
/// <summary>
/// 获取网络时间 utc时间
/// </summary>
/// <param name="ntpServer"></param>
/// <param name="timeoutMilliseconds "></param>
/// <returns></returns>
private static DateTime GetNetworkTime(string ntpServer, int timeoutMilliseconds = 5000)
{
try
{
const int udpPort = 123;
var ntpData = new byte[48]; // 创建一个 48 字节大小的字节数组来存储 NTP 数据
ntpData[0] = 0x1B; // 将 NTP 数据的第一个字节设置为 0x1B,这是 NTP 协议的请求数据格式
var addresses = Dns.GetHostEntry(ntpServer).AddressList; // 获取 NTP 服务器的 IP 地址列表
var ipEndPoint = new IPEndPoint(addresses[0], udpPort); // 创建用于连接的 IP 端点,使用第一个 IP 地址和 NTP 服务器的端口 123
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);// 创建套接字,使用 IPv4 地址族、数据报套接字类型和 UDP 协议类型
// 设置超时时间
socket.ReceiveTimeout = timeoutMilliseconds;
socket.Connect(ipEndPoint); // 连接到 NTP 服务器
socket.Send(ntpData); // 发送 NTP 数据
socket.Receive(ntpData); // 接收 NTP 响应数据
socket.Close(); // 关闭套接字连接
const byte serverReplyTime = 40; // 服务器响应时间在 NTP 数据中的偏移量
ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime); // 从 NTP 数据中获取无符号 32 位整数部分
ulong fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4); // 从 NTP 数据中获取无符号 32 位小数部分
// 交换整数部分和小数部分的字节顺序,以适应本地字节顺序
intPart = SwapEndianness(intPart);
fractPart = SwapEndianness(fractPart);
var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L); // 将整数部分和小数部分转换为毫秒数
var networkDateTime = (new DateTime(1900, 1, 1)).AddMilliseconds((long)milliseconds); // 根据毫秒数计算网络时间(从 1900 年 1 月 1 日开始计算)
TimeZoneInfo serverTimeZone = TimeZoneInfo.Local; // 服务器的时区
networkDateTime = TimeZoneInfo.ConvertTimeFromUtc(networkDateTime, serverTimeZone);
return networkDateTime;
}
catch (Exception ex)
{
Debug.Log("获取网络时间失败: " + ex.Message);
return DateTime.MinValue;
}
}
// 交换字节顺序,将大端序转换为小端序或反之
private static uint SwapEndianness(ulong x)
{
return (uint)(((x & 0x000000ff) << 24) +
((x & 0x0000ff00) << 8) +
((x & 0x00ff0000) >> 8) +
((x & 0xff000000) >> 24));
}
2、异步方式获取网络时间
/// <summary>
/// 异步获取时间 utc时间
/// </summary>
/// <param name="ntpServer"></param>
/// <param name="timeoutMilliseconds"></param>
/// <returns></returns>
private async static Task<DateTime> GetNetworkTimeAsync(string ntpServer, int timeoutMilliseconds = 5000)
{
try
{
const int udpPort = 123;
var ntpData = new byte[48];
ntpData[0] = 0x1B;
var addresses = await Dns.GetHostAddressesAsync(ntpServer);
var ipEndPoint = new IPEndPoint(addresses[0], udpPort);
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 设置超时时间
socket.ReceiveTimeout = timeoutMilliseconds;
await socket.ConnectAsync(ipEndPoint);
await socket.SendAsync(new ArraySegment<byte>(ntpData), SocketFlags.None);
var receiveBuffer = new byte[48];
await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), SocketFlags.None);
socket.Dispose();
const byte serverReplyTime = 40;
ulong intPart = BitConverter.ToUInt32(receiveBuffer, serverReplyTime);
ulong fractPart = BitConverter.ToUInt32(receiveBuffer, serverReplyTime + 4);
intPart = SwapEndianness(intPart);
fractPart = SwapEndianness(fractPart);
var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L);
var networkDateTime = new DateTime(1900, 1, 1).AddMilliseconds((long)milliseconds);
TimeZoneInfo serverTimeZone = TimeZoneInfo.Local; // 服务器的时区
networkDateTime = TimeZoneInfo.ConvertTimeFromUtc(networkDateTime, serverTimeZone);
return networkDateTime;
}
catch (Exception ex)
{
// 出现异常,返回 null 或抛出错误,视情况而定
Debug.Log("获取网络时间失败: " + ex.Message);
return DateTime.MinValue;
}
}
// 交换字节顺序,将大端序转换为小端序或反之
private static uint SwapEndianness(ulong x)
{
return (uint)(((x & 0x000000ff) << 24) +
((x & 0x0000ff00) << 8) +
((x & 0x00ff0000) >> 8) +
((x & 0xff000000) >> 24));
}
四、通过NTP服务器池获取网络时间
使用多个NTP服务器池有助于确保你的应用程序获得准确、高可用性的时间信息,并降低了单点故障的风险。这是一个非常有用的时间管理策略,特别是对于需要时间同步的应用程序和系统。
/// <summary>
/// 同步获取网络时间(UTC时间)使用多个NTP服务器(池)
/// </summary>
/// <returns>获取到的网络时间</returns>
public static DateTime GetNetworkTimePool()
{
var ntpServerAddresses = new List<string>
{
"cn.pool.ntp.org",
"ntp1.aliyun.com" ,
"ntp2.aliyun.com" ,
"ntp3.aliyun.com" ,
"ntp4.aliyun.com" ,
"ntp5.aliyun.com" ,
"ntp6.aliyun.com" ,
"cn.pool.ntp.org" ,
"asia.pool.ntp.org" ,
"time.windows.com" ,
"time1.cloud.tencent.com"
};
foreach (var serverAddress in ntpServerAddresses)
{
DateTime networkDateTime = GetNetworkTime(serverAddress, 2000);
if (networkDateTime != DateTime.MinValue)
{
Debug.Log("获取网络时间:" + networkDateTime);
return networkDateTime;
}
}
Debug.Log("获取系统时间:" + DateTime.Now);
return DateTime.Now;
}
/// <summary>
/// 异步获取网络时间(UTC时间)使用多个NTP服务器(池)
/// </summary>
/// <returns>获取到的网络时间</returns>
public static async Task<DateTime> GetNetworkTimeAsyncPool()
{
var ntpServerAddresses = new List<string>
{
"cn.pool.ntp.org",
"ntp1.aliyun.com" ,
"ntp2.aliyun.com" ,
"ntp3.aliyun.com" ,
"ntp4.aliyun.com" ,
"ntp5.aliyun.com" ,
"ntp6.aliyun.com" ,
"cn.pool.ntp.org" ,
"asia.pool.ntp.org" ,
"time.windows.com" ,
"time1.cloud.tencent.com"
};
var tasks = ntpServerAddresses.Select(serverAddress => Task.Run(async () => await GetNetworkTimeAsync(serverAddress, 2000))).ToArray();
while (tasks.Length > 0)
{
var completedTask = await Task.WhenAny(tasks);
tasks = tasks.Where(task => task != completedTask).ToArray();
DateTime networkDateTime = completedTask.Result;
if (networkDateTime != DateTime.MinValue)
{
Debug.Log("获取网络时间:" + networkDateTime);
return networkDateTime;
}
}
Debug.Log("获取系统时间:" + DateTime.Now);
return DateTime.Now;
}
最后可以根据上面自行选择进行同步还是异步的方式获取网络时间。
over,欢迎指正