[Unity] Unity obtains network time based on NTP server

Foreword:

When we need accurate time information, relying on local system time can lead to inaccurate results, especially in applications that need to ensure time consistency and accuracy. In order to solve this problem, we can obtain the network time through the Network Time Protocol (NTP) server to obtain more accurate time information. This blog will introduce how to use C# to write code in Unity to obtain the network time from the NTP server.

1. What is NTP?

NTP, the full name of Network Time Protocol, is a protocol used to synchronize the time of devices in a computer network. It allows computers to obtain precise time information through the Internet or LAN to ensure that devices maintain time consistency in distributed systems. The NTP protocol works based on a set of public time servers that provide accurate time information.

2. Obtain the NTP server address

To get the time from an NTP server, you first need to know the address of an available NTP server. Here are some common NTP server addresses that are usually available:
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
You can also find the server address you want from the URL http://www.ntp.org.cn/pool.

3. Obtain network time through NTP server

1. Obtain network time synchronously

/// <summary>
/// Get network time utc time
/// </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]; // Create a 48-byte byte array to store NTP data
        ntpData[0] = 0x1B; // Set the first byte of NTP data to 0x1B, which is the request data format of the NTP protocol

        var addresses = Dns.GetHostEntry(ntpServer).AddressList; // Get the IP address list of the NTP server
        var ipEndPoint = new IPEndPoint(addresses[0], udpPort); // Create an IP endpoint for the connection, using the first IP address and port 123 of the NTP server
        var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//Create a socket using the IPv4 address family, datagram socket type and UDP protocol type

        //Set the timeout
        socket.ReceiveTimeout = timeoutMilliseconds;

        socket.Connect(ipEndPoint); // Connect to NTP server
        socket.Send(ntpData); //Send NTP data
        socket.Receive(ntpData); //Receive NTP response data
        socket.Close(); // Close the socket connection

        const byte serverReplyTime = 40; // Offset of server response time in NTP data
        ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime); // Get the unsigned 32-bit integer part from NTP data
        ulong fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4); // Get the unsigned 32-bit fractional part from NTP data
                                                                                                // Swap the byte order of the integer and fractional parts to fit the local byte order
        intPart = SwapEndianness(intPart);
        fractPart = SwapEndianness(fractPart);

        var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L); // Convert the integer and decimal parts to milliseconds
        var networkDateTime = (new DateTime(1900, 1, 1)).AddMilliseconds((long)milliseconds); // Calculate network time based on milliseconds (calculated from January 1, 1900)

        TimeZoneInfo serverTimeZone = TimeZoneInfo.Local; // Server time zone

        networkDateTime = TimeZoneInfo.ConvertTimeFromUtc(networkDateTime, serverTimeZone);

        return networkDateTime;
    }
    catch (Exception ex)
    {<!-- -->
        Debug.Log("Failed to obtain network time: " + ex.Message);
        return DateTime.MinValue;
    }
}

// Swap byte order, convert big endian to little endian or vice versa
private static uint SwapEndianness(ulong x)
{<!-- -->
    return (uint)(((x & amp; 0x000000ff) << 24) +
                   ((x & amp; 0x0000ff00) << 8) +
                   ((x & 0x00ff0000) >> 8) +
                   ((x & amp; 0xff000000) >> 24));
}

2. Obtain network time asynchronously

/// <summary>
/// Asynchronously obtain time utc time
/// </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);

        //Set the timeout
        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; // Server time zone

        networkDateTime = TimeZoneInfo.ConvertTimeFromUtc(networkDateTime, serverTimeZone);

        return networkDateTime;
    }
    catch (Exception ex)
    {<!-- -->
        // An exception occurs, return null or throw an error, as appropriate
        Debug.Log("Failed to obtain network time: " + ex.Message);
        return DateTime.MinValue;
    }
}

// Swap byte order, convert big endian to little endian or vice versa
private static uint SwapEndianness(ulong x)
 {<!-- -->
     return (uint)(((x & amp; 0x000000ff) << 24) +
                    ((x & amp; 0x0000ff00) << 8) +
                    ((x & 0x00ff0000) >> 8) +
                    ((x & amp; 0xff000000) >> 24));
 }

4. Obtain network time through NTP server pool

Using multiple NTP server pools helps ensure that your applications receive accurate, highly available time information and reduces the risk of single points of failure. This is a very useful time management strategy, especially for applications and systems that require time synchronization.

/// <summary>
/// Synchronously obtain network time (UTC time) using multiple NTP servers (pools)
/// </summary>
/// <returns>The network time obtained</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("Get network time: " + networkDateTime);

           return networkDateTime;
       }
   }

   Debug.Log("Get system time: " + DateTime.Now);
   return DateTime.Now;
}

/// <summary>
/// Asynchronously obtain network time (UTC time) using multiple NTP servers (pools)
/// </summary>
/// <returns>The network time obtained</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("Get network time: " + networkDateTime);

           return networkDateTime;
       }
   }

   Debug.Log("Get system time: " + DateTime.Now);
   return DateTime.Now;
}

Finally, you can choose whether to obtain the network time synchronously or asynchronously according to the above.
over, corrections welcome