SocketException: Operation on non-blocking socket would block при запросе к NTP серверу

Есть код, который используется для запроса времени с NTP сервера.

var ntpData = new byte[48];

//Setting the Leap Indicator, Version Number and Mode values
ntpData[0] = 0x1B; //LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)

var addresses = Dns.GetHostEntry(ntpServer).AddressList;

//The UDP port number assigned to NTP is 123
var ipEndPoint = new IPEndPoint(addresses[0], 123);

using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
{
    socket.Connect(ipEndPoint);

    //Stops code hang if NTP is blocked
    socket.ReceiveTimeout = 3000;

    socket.Send(ntpData);
    socket.Receive(ntpData);
    socket.Close();
}

И иногда без видимых причин он выдает следующую ошибку

Message: SocketException: Operation on non-blocking socket would block
Exception: java.lang.Throwable:
System.Net.Sockets.Socket.Receive (System.Byte[] buffer, System.Int32 offset, System.Int32 size, System.Net.Sockets.SocketFlags socketFlags) (at <00000000000000000000000000000000>:0)

Добавлено позже - как видно из стректрейса, ошибка затрагивает System.Net.Sockets и приходит из java.lang.Throwable. Это связанно с тем, что код выполняется из UnityPlayer на Android, т.е. это шарповый код, находящийся в JVM. Вопрос именно в том, что вызывает такую ошибку и как этого избежать? Видел такие ошибки на SO при запросе к MSSQL (ссылка), но не к серверу времени.


Ответы (1 шт):

Автор решения: aepot

Скорее всего проблема происходит из-за того что ответ от NTP сервера прилетает раньше, чем вы начинаете его ждать в коде и теряется. А потом возникает таймаут от того, что вы ничего не дождались, оттуда и SocketException.

Другими словами, не стоит использовать таймауты на UDP сокетах, так как таймаут - это время между отправленным запросом и ответом на него в рамках одного TCP подключения, что для UDP не имеет примерно никакого смысла, потому что он работает по правилу "отправил и забыл". Нет никакой гарантии, что пакет прилетит, нет никакой гарантии, что прилетит именно тот пакет, который вы ждете. Так устроен UDP - высокая скорость, и никаких гарантий.

Вот, переписал с использованием UdpClient и асинхронно.

public class NtpClient
{
    public static async Task<DateTime> GetNetworkTimeAsync(string ntpServer, int timeout = 500)
    {
        byte[] ntpData = new byte[48];
        ntpData[0] = 0x1B;
        double jitter = 0;
        using (UdpClient client = new UdpClient(ntpServer, 123))
        {
            Task<UdpReceiveResult> receiveTask = client.ReceiveAsync(); // начинаем ждать ответа
            DateTime now = DateTime.Now;
            await client.SendAsync(ntpData, ntpData.Length); // и только потом отправляем запрос
            if (receiveTask == await Task.WhenAny(receiveTask, Task.Delay(timeout)))
                ntpData = receiveTask.Result.Buffer;
            else
                throw new TimeoutException("Timeout occured while waiting for NTP server response");
            jitter = (DateTime.Now - now).TotalMilliseconds / 2; // разброс = пинг / 2, для меня это примерно 25 мс
        }
        byte[] data = ntpData[40..];
        Array.Reverse(data);
        uint fraction = BitConverter.ToUInt32(data);
        uint seconds = BitConverter.ToUInt32(data, 4);
        double milliseconds = (double)seconds * 1000 + (double)fraction * 1000 / 0x100000000L + jitter;
        DateTime networkDateTime = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(milliseconds);
        return networkDateTime.ToLocalTime();
    }
}

Использование

static async Task Main(string[] args)
{
    Console.WriteLine(await NtpClient.GetNetworkTimeAsync("pool.ntp.org"));
    Console.ReadKey();
}

Вывод в консоль

09.03.2021 19:46:13

P.S. в одном случае из 10 000 примерно - вам не кажется, что вы слишком часто долбаете NTP сервер запросами? Время для коррекции запрашивать следует ну точно не чаще, чем 2 раза в сутки. То есть серверы NTP не предназначены для частых запросов, и сбой 1 раз из 10000 запросов при нормальном использовании NTP происходил бы примерно 1 раз в 13 лет. :)

→ Ссылка