使用Unitask定时调用请求,同步时间
using Cysharp.Threading.Tasks;
using GameContent;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;
/// <summary>
/// NTP服务器 网络时间
/// 没隔10s请求一次网络时间,如果失败则弹出断网界面
/// </summary>
public static class NetworkUtcTimeTools
{
/// <summary>
/// 获取计算后的本地UTC时间
/// </summary>
public static DateTime UtcNow
{
get
{
return mNetUtcTime.AddMilliseconds((Time.realtimeSinceStartupAsDouble - mRecordUnityStartupTime) * 1000);
}
}
/// <summary>
/// UI资源是否准备好了
/// </summary>
public static bool UIAssetIsOk = false;
/// <summary>
/// 是否成功获取到网络时间
/// 在首次登录、从后台切回前台时,会重新获取网络时间
/// </summary>
public static bool IsSuccess { get; private set; } = false;
/// <summary>
/// 获取当前网络UTC时间
/// </summary>
private static DateTime mNetUtcTime = DateTime.MinValue;
/// <summary>
/// 当返回网络时间时,记录Unity的游戏开始以来的实时时间
/// </summary>
private static double mRecordUnityStartupTime = 0;
/// <summary>
/// 服务器地址列表
/// </summary>
private static List<string> mCachedNtpServerUrlList = new List<string>()
{
"time.cloudflare.com",
"time.google.com",
"pool.ntp.org",
"time.aws.com",
"time.windows.com",
"time.apple.com",
"time.nist.gov",
"clock.isc.org",
"ntp.ubuntu.com",
};
/// <summary>
/// 是否显示断网提醒
/// </summary>
private static bool mIsShowWarn = false;
/// <summary>
/// 初始化网络时间
/// </summary>
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void InitNetworkUtcTime()
{
Debug.Log("[网络时间] 开始初始化");
IsSuccess = false;
CyclicUpdateTime().Forget();
}
/// <summary>
/// 循环同步时间
/// </summary>
/// <returns></returns>
private static async UniTaskVoid CyclicUpdateTime()
{
while (true)
{
AsyncUpdateTime().Forget();
await UniTask.Delay(10000, ignoreTimeScale: true);
}
}
/// <summary>
/// 异步同步时间
/// </summary>
private static async UniTaskVoid AsyncUpdateTime()
{
var cts = new CancellationTokenSource();
cts.CancelAfter(5000);//5s后自动取消
int urlCount = mCachedNtpServerUrlList.Count;
UniTask<bool>[] taskArray = new UniTask<bool>[urlCount];
bool[] taskCompletedArray = new bool[urlCount];
for (int i = 0; i < urlCount; i++)
{
string url = mCachedNtpServerUrlList[i];
taskArray[i] = UpdateTimeFromNtpServerAsync(i, url, cts);
taskCompletedArray[i] = false;
}
await UniTask.DelayFrame(10, cancellationToken: cts.Token);
int failureCount = 0;
while (true)
{
failureCount = 0;
for (int i = taskArray.Length - 1; i >= 0; i--)
{
if (taskCompletedArray[i])
continue;
var task = taskArray[i];
if (task.Status == UniTaskStatus.Pending)
continue;
taskCompletedArray[i] = true;
if (task.Status == UniTaskStatus.Succeeded)
{
if (task.GetAwaiter().GetResult())
{
CloseWarn();
cts.Cancel();
cts.Dispose();
return;
}
}
else
{
//Faulted or Canceled
if (++failureCount >= mCachedNtpServerUrlList.Count)
{
//判定是否全部失败了
ShowWarn();
cts.Cancel();
cts.Dispose();
return;
}
}
}
if (cts.IsCancellationRequested)
{
//时间到了,但没有结果
ShowWarn();
cts.Cancel();
cts.Dispose();
return;
}
await UniTask.DelayFrame(10);
}
}
private static void ShowWarn()
{
mIsShowWarn = true;
if (SplashCanvasCtrl.Instance != null && SplashCanvasCtrl.Instance.IsActived)
{
//TODO:Loading上的断网提醒
}
if (UIAssetIsOk)
NetworkWarnForm.ShowWarn();
}
private static void CloseWarn()
{
mIsShowWarn = false;
if (SplashCanvasCtrl.Instance != null && SplashCanvasCtrl.Instance.IsActived)
{
}
if (UIAssetIsOk)
NetworkWarnForm.CloseWarn();
}
private static DateTime m_epochTime = new(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// 更新网络时间
/// </summary>
private static async UniTask<bool> UpdateTimeFromNtpServerAsync(int checkIndex, string ntpServerDnsAddress, CancellationTokenSource ctsToken)
{
//ctsToken.Token.ThrowIfCancellationRequested();
await UniTask.Delay(checkIndex * 100, cancellationToken: ctsToken.Token); //间隔下防止同时返回
try
{
const int udpPort = 123;
var ntpData = new byte[48];
ntpData[0] = 0x1B;
var addresses = await Dns.GetHostAddressesAsync(ntpServerDnsAddress);
var ipEndPoint = new IPEndPoint(addresses[0], udpPort);
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.SendTimeout = 1000;
socket.ReceiveTimeout = 1000;
await socket.ConnectAsync(ipEndPoint);
if (ctsToken.IsCancellationRequested)
{
//已成功更新,取消此任务。
socket.Dispose();
return true;
}
await socket.SendAsync(new ReadOnlyMemory<byte>(ntpData), SocketFlags.None, ctsToken.Token);
//await socket.SendAsync(new ArraySegment<byte>(ntpData), SocketFlags.None);
if (ctsToken.IsCancellationRequested)
{
//已成功更新,取消此任务。
socket.Dispose();
return true;
}
var receiveBuffer = new byte[48];
await socket.ReceiveAsync(new Memory<byte>(receiveBuffer), SocketFlags.None, ctsToken.Token);
//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 = m_epochTime.AddMilliseconds((long)milliseconds);
//同步时间
IsSuccess = true;
mNetUtcTime = networkDateTime;
mRecordUnityStartupTime = Time.realtimeSinceStartupAsDouble;
Debug.Log($"[网络时间] checkIndex={checkIndex} 同步时间 {mNetUtcTime} ; {mRecordUnityStartupTime}");
return true;
}
catch (Exception exception)
{
Debug.LogWarning($"[网络时间] {exception}");
return false;
}
}
// 交换字节顺序,将大端序转换为小端序或反之
private static uint SwapEndianness(ulong x)
{
return (uint)(((x & 0x000000ff) << 24) + ((x & 0x0000ff00) << 8) + ((x & 0x00ff0000) >> 8) + ((x & 0xff000000) >> 24));
}
}