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 шт):
Скорее всего проблема происходит из-за того что ответ от 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 лет. :)