MaxLevs
@MaxLevs

C#: Автоматическое переподключение UdpClient после рестарта сервера?

Пишу приложение-сервис, который запрашивает с сервера по UDP данные о состоянии и выводит оповещения при необходимости.
Задача в том, что сервер живет своей жизнью. Он может быть работающим в момент старта приложения, а может и нет. А может и вообще упасть/перезапуститься/etc, и сервису требуется ждать и переодически проверять, не поднялся ли сервер и можно ли снова подключаться.

Реализовал я это таким образом.
Много кода

public Server(string name, string host, int? queryPort = null, int? rConPort = null)
        {
            Name = name;
            Host = Dns.GetHostAddresses(host)[0];
            QueryPort = queryPort;
            RConPort = rConPort;

            if (QueryPort == null && RConPort == null)
            {
                throw new IncorrectServerEntryPoint(Host);
            }
        }
        
        public async void Watch()
        {
            if (QueryPort == null) return;
            _statusWatcherClient = new UdpClient(Host.ToString(), QueryPort.Value);
            
            UpdateChallengeTokenTimer = new Timer(async obj =>
            {
                Console.WriteLine($"[INFO] [{Name}] Send handshake request");
                    
                Request handshakeRequest = Request.GetHandshakeRequest();
                byte[] response = null;
                try
                {
                    response = await SendResponseService.SendReceive(_statusWatcherClient, handshakeRequest.Data, ReceiveAwaitIntervalSeconds);
                    IsOnline = true;
                }
                catch (SocketException)
                {
                    WaitForServerAlive(QueryPort.Value);
                }
                    
                if (response == null) return;
                
                var challengeTokenRaw = Response.ParseHandshake(response);
                lock (_challengeTokenLock)
                {
                    SetChallengeToken(challengeTokenRaw);
                }
                    
                Console.WriteLine($"[INFO] [{Name}] ChallengeToken is set up: " + BitConverter.ToString(challengeTokenRaw));
            }, null, 0, GettingChallengeTokenInterval);
                
            UpdateServerStatusTimer = new Timer(async obj =>
            {
                Console.WriteLine($"[INFO] [{Name}] Send full status request");
                    
                var challengeToken = new byte[4];
                lock (_challengeTokenLock)
                {
                    Buffer.BlockCopy(_challengeToken, 0, challengeToken, 0, 4);
                }
                    
                var fullStatusRequest = Request.GetFullStatusRequest(challengeToken);

                byte[] response = null;
                try
                {
                    response = await SendResponseService.SendReceive(_statusWatcherClient, fullStatusRequest.Data, ReceiveAwaitIntervalSeconds);
                    IsOnline = true;
                }
                
                catch (SocketException)
                {
                    WaitForServerAlive(QueryPort.Value);
                }
                
                if (response == null) return;

                ServerFullState fullState = Response.ParseFullState(response);
                    
                Console.WriteLine($"[INFO] [{Name}] Full status is received");
                    
                OnFullStatusUpdated?.Invoke(this, new ServerStateEventArgs(Name, fullState));
                    
            }, null, 500, GettingStatusInterval);
        }

        public async Task Unwatch()
        {
            await UpdateChallengeTokenTimer.DisposeAsync();
            await UpdateServerStatusTimer.DisposeAsync();
            _statusWatcherClient.Dispose();
            _statusWatcherClient = null;
        }

        public async void WaitForServerAlive(int port)
        {
            Console.WriteLine($"[WARNING] [{Name}] Server is unavailable. Waiting for reconnection...");
            IsOnline = false;
            await Unwatch();
            Timer waitTimer = null;
            waitTimer = new Timer(async obj => {
                try
                {
                    using TcpClient tcpClient = new TcpClient();
                    await tcpClient.ConnectAsync(Host, port);
                    if (waitTimer == null) return;
                    await waitTimer.DisposeAsync();
                    Console.WriteLine($"[INFO] [{Name}] Server is available again");
                    Watch();
                }  catch (SocketException) { }
            }, null, 500, 5000);
        }

        public static int ReceiveAwaitIntervalSeconds = 10;
        public static int GettingChallengeTokenInterval = 30000;
        public static int GettingStatusInterval = 5000;



Здесь мы создаём экземпляр сервера и запускаем его отслеживание через Watch(), который занимается тем, что обновляет токены запросов в актуальном состоянии (каждые 30 секунд) и запрашивает статус сервера каждые 5 секунд.
Если же соединение прерывается или не доступно с самого начала в дело вступает WaitForServerAlive(), который останавливает таймеры запросов к серверу, уничтожает старый сокет и начинает каждые 5 секунд стучаться к серверу по порту в попытке получить подключение. Если подключение проходит, то возобновляет мониторинг сервера вызовом Watch().

Код отправки и полуения пакетов по UDP выглядит так
public static async Task<byte[]> SendReceive(UdpClient client, byte[] data, int receiveAwaitIntervalSeconds)
        {
            if (client == null)
            {
                throw new NullReferenceException("UdpClient client is null");
            }
            
            IPEndPoint ipEndPoint = null;
            byte[] response = null;
            
            await client.SendAsync(data, data.Length);
            var responseToken = client.BeginReceive(null, null);
            responseToken.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(receiveAwaitIntervalSeconds));
            if (responseToken.IsCompleted)
            {
                try
                {
                    response = client.EndReceive(responseToken, ref ipEndPoint);
                }

                catch (Exception)
                {
                    // can't end receive
                }
            }

            if (response == null)
                throw new SocketException();

            return response;
        }


Собственно, пока сервер стабильно работает, то всё идет гладко, но стоит сервису запуститься до старта сервера или серверу упасть посередине работы, как в момент "возвращения" сервера приложение начинает выдавать что-то вроде
Логи работы

[INFO] [ML_VDS] Send handshake request
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[INFO] [ML_VDS] Server is available again
[INFO] [ML_VDS] Send handshake request
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[INFO] [ML_VDS] Server is available again
...
[INFO] [ML_VDS] Send handshake request
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[INFO] [ML_VDS] Server is available again
[INFO] [ML_VDS] Send handshake request
[INFO] [ML_VDS] Send full status request
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[INFO] [ML_VDS] Server is available again
[INFO] [ML_VDS] Send handshake request
[INFO] [ML_VDS] Server is available again
[INFO] [ML_VDS] Send handshake request
[ML_VDS] Server is online
[INFO] [ML_VDS] ChallengeToken is set up: 00-93-95-1E
[INFO] [ML_VDS] ChallengeToken is set up: 00-98-D8-8B
[INFO] [ML_VDS] Send full status request
[INFO] [ML_VDS] Send full status request
[INFO] [ML_VDS] Send full status request
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[ML_VDS] Server is offline
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[INFO] [ML_VDS] Send full status request
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
[WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
Unhandled exception. [WARNING] [ML_VDS] Server is unavailable. Waiting for reconnection...
Unhandled exception. Unhandled exception. Unhandled exception. Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at MCServerNotifier.Server.Unwatch() in /Users/maxlevs/RiderProjects/MCServerNotifier/MCServerNotifier/Server.cs:line 142
   at MCServerNotifier.Server.WaitForServerAlive(Int32 port) in /Users/maxlevs/RiderProjects/MCServerNotifier/MCServerNotifier/Server.cs:line 150
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_1(Object state)
   ...
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
System.NullReferenceException: Object reference not set to an instance of an object.
   at MCServerNotifier.Server.Unwatch() in  
   ...
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
System.NullReferenceException: Object reference not set to an instance of an object.
   at MCServerNotifier.Server.Unwatch() in  
   ...
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
System.NullReferenceException: Object reference not set to an instance of an object.
   at MCServerNotifier.Server.Unwatch() in 
   ...
   at System.Threading.ThreadPoolWorkQueue.Dispatch()

System.NullReferenceException: Object reference not set to an instance of an object.
   at MCServerNotifier.Server.Unwatch() in 
   ...
   at System.Threading.ThreadPoolWorkQueue.Dispatch()



Прошу знающих людей помочь разобраться в причине проблемы. Как правильно подходить к попыткам возобновить соединение после разрыва?
  • Вопрос задан
  • 173 просмотра
Решения вопроса 1
MaxLevs
@MaxLevs Автор вопроса
Решил вопрос, отказавших от использования в качестве проверки работы сервера TCP-соединение. (Кто может подсказать, насколько такая замена вообще могла быть жизнеспособной?).

Заменил код WaitForServerAlive() на следующий:
Код здесь

public async void WaitForServerAlive()
        {
            if(Debug)
                Console.WriteLine($"[WARNING] [{ServerName}] Server is unavailable. Waiting for reconnection...");
            
            IsOnline = false;
            await Unwatch();
            
            _mcQuery.InitSocket();
            
            Timer waitTimer = null;
            waitTimer = new Timer(async obj => {
                try
                {
                    await _mcQuery.GetHandshake();
                    
                    IsOnline = true;
                    Watch();
                    lock (_retryCounterLock)
                    {
                        RetryCounter = 0;
                    }
                    
                    waitTimer.Dispose();
                }
                catch (SocketException)
                {
                    if(Debug)
                        Console.WriteLine($"[WARNING] [{ServerName}] [WaitForServerAlive] Server doesn't response. Try to reconnect: {RetryCounter}");
                    
                    lock (_retryCounterLock)
                    {
                        RetryCounter++;
                        if (RetryCounter >= RetryMaxCount)
                        {
                            if(Debug)
                                Console.WriteLine($"[WARNING] [{ServerName}] [WaitForServerAlive] Recreate socket");
                            
                            RetryCounter = 0;
                            _mcQuery.InitSocket();
                        }
                    }
                }
            }, null, 500, 5000);
        }



Здесь каждые 5 секунд мы стучим на сервер, запрашивая новый токен. На получение токена даётся 5 попыток, после чего сокет пересоздаётся, чтобы избежать ситуаций, когда сервер поднят, a данные всё равно через сокет идти не хотят. Любой удачно переданный пакет сбрасывает счетчик. Собственно сам WaitForServerAlive() вызывается если 5 раз подряд возникла неудачная попытка получить данные с сервера, это переводит приложение в режим ожидания восстановления соединения.

Вышестоящий протокол подразумевает получение токена через хэндшейк-запрос, который возвращает результат всегда при корректно составленном запросе. Если сервер поднят, то мы получим токен. Обычно его достаточно получать раз в 30 секунд, чтобы запросы проходили. Для того, чтобы быстрее узнать о том, что сервер снова поднят, я отменяю обновление токена и вызываю похожий код раз в 5 секунд, до тех пор, пока не получу данные с сервера.
После этого можно считать что сервер снова жив и возвращаться к штатной работе.
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы