使用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 Canceledif (++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));}}
