[.NET, C#] Реализация Minecraft Query протокола в .Net Core
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Minecraft Server Query – это простой протокол, позволяющий получить актуальную информацию о состоянии сервера путём отправки пары-тройки незамысловатых UDP-пакетов.На вики есть подробное описание этого протокола с примерами реализации на разных языках. Однако меня поразило, насколько куцые реализации для .Net существуют на данный момент. Поискав некоторое время, я наткнулся на несколько репозиториев. Предлагаемые решения либо содержали банальные ошибки, либо имели урезанный функционал, хотя, казалось бы, куда ещё больше урезать-то.Так было принято решение написать свою реализацию.Скажи мне, кто ты...Для начала, посмотрим, что из себя представляет сам протокол Minecraft Query. Согласно вики, мы имеем в распоряжении 3 вида пакетов запросов и, соотвественно, 3 вида пакетов ответа:
- Handshake
- BasicStatus
- FullStatus
Первый тип пакета используется для получения ChallengeToken, необходимого для формирования других двух пакетов. Привязывается он к IP-адресу отправителя на 30 секунд. Смысловая нагрузка оставшихся двух ясна из названий.Стоит отметить, что хотя последние два запроса отличаются друг от друга лишь выравниванием на концах пакетов, присылаемые ответы отличаются способом представления данных. Для примера, вот так выглядит ответ BasicStatus
Ответ на запрос BasicStatusА вот так – FullStatus
Ответ на запрос FullStatusВсе данные, помимо тех, что хранятся в short, представлены в big-endian. А для поля SessionId, которое постоянно в рамках одного клиент-сервер соединения, должно выполняться условие SessionId & 0x0F0F0F0F == SessionId.В общем виде запрос выглядит так
Запрос в общем видеБолее подробно об этом об этом можно почитать на вики.И я скажу тебе, как тебя распарситьДля начала, определимся, что мы хотим получить на выходе. Готовая библиотека должна предоставлять API для отправки любого из 3 видов пакетов и получения результата в распаршеном виде. При этом, я хочу больше свободы в плане поддержания жизнеспособности сокетов и обновления ChallengeToken. Если я буду запрашивать состояние сервера каждые 3 секунды, то я не хочу, чтобы вместо одного пакета запроса отправлялось два: хэндшейк и состояние. И наоборот, если я опрашиваю сервер раз в час, зачем мне слать запросы каждые 30 секунд? Поэтому работа с библиотекой будет происходить в "ручном" режиме.Итак, определившись, можем уже представить, как будет выглядеть архитектура классов. Я вижу работу примерно таким образом
public static async Task<ServerState> DoSomething(IPAddress host, int port) {
var mcQuery = new McQuery(host, port);
mcQuery.InitSocket();
await mcQuery.GetHandshake();
return await mcQuery.GetFullStatus();
}
Здесь создаётся разовое соединение. Для долгоживущего потребуется проверять состояние сокета и инициализировать заново (об этом в конце статьи).Для того, чтобы пакет отправить, его надо для начала сформировать. Этим будет заниматься класс Request.
public class Request
{
// Набор констант для формирования пакета
private static readonly byte[] Magic = { 0xfe, 0xfd };
private static readonly byte[] Challenge = { 0x09 };
private static readonly byte[] Status = { 0x00 };
public byte[] Data { get; private set; }
private Request(){}
public byte RequestType => Data[2];
public static Request GetHandshakeRequest(SessionId sessionId)
{
var request = new Request();
// Собираем пакет
var data = new List<byte>();
data.AddRange(Magic);
data.AddRange(Challenge);
data.AddRange(sessionId.GetBytes());
request.Data = data.ToArray();
return request;
}
public static Request GetBasicStatusRequest(SessionId sessionId, byte[] challengeToken)
{
if (challengeToken == null)
{
throw new ChallengeTokenIsNullException();
}
var request = new Request();
var data = new List<byte>();
data.AddRange(Magic);
data.AddRange(Status);
data.AddRange(sessionId.GetBytes());
data.AddRange(challengeToken);
request.Data = data.ToArray();
return request;
}
public static Request GetFullStatusRequest(SessionId sessionId, byte[] challengeToken)
{
if (challengeToken == null)
{
throw new ChallengeTokenIsNullException();
}
var request = new Request();
var data = new List<byte>();
data.AddRange(Magic);
data.AddRange(Status);
data.AddRange(sessionId.GetBytes());
data.AddRange(challengeToken);
data.AddRange(new byte[] {0x00, 0x00, 0x00, 0x00}); // Padding
request.Data = data.ToArray();
return request;
}
}
Здесь всё просто. Храним все константы внутри класса и формируем пакет в трёх статических методах. Можно ещё заметить класс SessionId, который может давать как байтовое, так и строковое представление по необходимости.
public class SessionId
{
private readonly byte[] _sessionId;
public SessionId (byte[] sessionId)
{
_sessionId = sessionId;
}
// Случайный SessionId
public static SessionId GenerateRandomId()
{
var sessionId = new byte[4];
new Random().NextBytes(sessionId);
sessionId = sessionId.Select(@byte => (byte)(@byte & 0x0F)).ToArray();
return new SessionId(sessionId);
}
public string GetString()
{
return BitConverter.ToString(_sessionId);
}
public byte[] GetBytes()
{
var sessionId = new byte[4];
Buffer.BlockCopy(_sessionId, 0, sessionId, 0, 4);
return sessionId;
}
}
Дождавшись ответа сервера, мы получаем последовательность байт, которые хотим привести к человекочитаемому виду. Для этого служит класс Response, который представляет набор "парсеров" в виде статических полей.
public static class Response
{
public static byte ParseType(byte[] data)
{
return data[0];
}
//
public static SessionId ParseSessionId(byte[] data)
{
if (data.Length < 1) throw new IncorrectPackageDataException(data);
var sessionIdBytes = new byte[4];
Buffer.BlockCopy(data, 1, sessionIdBytes, 0, 4);
return new SessionId(sessionIdBytes);
}
public static byte[] ParseHandshake(byte[] data)
{
if (data.Length < 5) throw new IncorrectPackageDataException(data);
var response = BitConverter.GetBytes(int.Parse(Encoding.ASCII.GetString(data, 5, data.Length - 6)));
if (BitConverter.IsLittleEndian)
{
response = response.Reverse().ToArray();
}
return response;
}
public static ServerBasicState ParseBasicState(byte[] data)
{
if (data.Length <= 5)
throw new IncorrectPackageDataException(data);
var statusValues = new Queue<string>();
short port = -1;
data = data.Skip(5).ToArray(); // Skip Type + SessionId
var stream = new MemoryStream(data);
var sb = new StringBuilder();
int currentByte;
int counter = 0;
while ((currentByte = stream.ReadByte()) != -1)
{
if (counter > 6) break;
// Парсим нормер порта
if (counter == 5)
{
byte[] portBuffer = {(byte) currentByte, (byte) stream.ReadByte()};
if (!BitConverter.IsLittleEndian)
portBuffer = portBuffer.Reverse().ToArray();
port = BitConverter.ToInt16(portBuffer); // Little-endian short
counter++;
continue;
}
// Парсим параметры-строки
if (currentByte == 0x00)
{
string fieldValue = sb.ToString();
statusValues.Enqueue(fieldValue);
sb.Clear();
counter++;
}
else sb.Append((char) currentByte);
}
var serverInfo = new ServerBasicState
{
Motd = statusValues.Dequeue(),
GameType = statusValues.Dequeue(),
Map = statusValues.Dequeue(),
NumPlayers = int.Parse(statusValues.Dequeue()),
MaxPlayers = int.Parse(statusValues.Dequeue()),
HostPort = port,
HostIp = statusValues.Dequeue(),
};
return serverInfo;
}
// "Секции" пакета резделены константными последовательностями байт,
// это можно испльзовать для проверки, что мы всё сделали правильно
public static ServerFullState ParseFullState(byte[] data)
{
var statusKeyValues = new Dictionary<string, string>();
var players = new List<string>();
var buffer = new byte[256];
Stream stream = new MemoryStream(data);
stream.Read(buffer, 0, 5); // Read Type + SessionID
stream.Read(buffer, 0, 11); // Padding: 11 bytes constant
var constant1 = new byte[] {0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00};
for (int i = 0; i < constant1.Length; i++)
Debug.Assert(constant1[i] == buffer[i], "Byte mismatch at " + i + " Val :" + buffer[i]);
var sb = new StringBuilder();
string lastKey = string.Empty;
int currentByte;
while ((currentByte = stream.ReadByte()) != -1)
{
if (currentByte == 0x00)
{
if (!string.IsNullOrEmpty(lastKey))
{
statusKeyValues.Add(lastKey, sb.ToString());
lastKey = string.Empty;
}
else
{
lastKey = sb.ToString();
if (string.IsNullOrEmpty(lastKey)) break;
}
sb.Clear();
}
else sb.Append((char) currentByte);
}
stream.Read(buffer, 0, 10); // Padding: 10 bytes constant
var constant2 = new byte[] {0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00};
for (int i = 0; i < constant2.Length; i++)
Debug.Assert(constant2[i] == buffer[i], "Byte mismatch at " + i + " Val :" + buffer[i]);
while ((currentByte = stream.ReadByte()) != -1)
{
if (currentByte == 0x00)
{
var player = sb.ToString();
if (string.IsNullOrEmpty(player)) break;
players.Add(player);
sb.Clear();
}
else sb.Append((char) currentByte);
}
ServerFullState fullState = new()
{
Motd = statusKeyValues["hostname"],
GameType = statusKeyValues["gametype"],
GameId = statusKeyValues["game_id"],
Version = statusKeyValues["version"],
Plugins = statusKeyValues["plugins"],
Map = statusKeyValues["map"],
NumPlayers = int.Parse(statusKeyValues["numplayers"]),
MaxPlayers = int.Parse(statusKeyValues["maxplayers"]),
PlayerList = players.ToArray(),
HostIp = statusKeyValues["hostip"],
HostPort = int.Parse(statusKeyValues["hostport"]),
};
return fullState;
}
}
Таким образом мы получаем полный инструментарий для формирования пакетов, их отправки, получения ответов и извлечения из них необходимой информации.Долгоживущие приложения на основе библиотекиВернёмся к том, о чем я говорил выше. Это можно реализовать таким образом. Код взят из моего нотификатора пользовательской активности. Здесь каждые 5 секунд запрашивается FullStatus, поэтому имеет смысл обновлять ChallengeToken периодически сразу после истечения предыдущего. Всего приложение имеет 2 режима работы: штатный и режим восстановления соединения.В штатном режиме приложение по таймерам обновляет токен и запрашивает FullStatus. При обнаружении упавшего сервера/оборванного соединения/etc (5 попыток передачи) приложение переходит в режим восстановления соединения и при удачной попытке получения сообщения снова возвращается в штатный режим.Для начала напишем конструктор и два метода для запуска прослушивания сервера и окончания.
public StatusWatcher(string serverName, string host, int queryPort)
{
ServerName = serverName;
_mcQuery = new McQuery(Dns.GetHostAddresses(host)[0], queryPort);
_mcQuery.InitSocket();
}
public async Task Unwatch()
{
await UpdateChallengeTokenTimer.DisposeAsync();
await UpdateServerStatusTimer.DisposeAsync();
}
public async void Watch()
{
// Обновляем challengetoken по таймеру каждые 30 секунд
UpdateChallengeTokenTimer = new Timer(async obj =>
{
if (!IsOnline) return;
if(Debug)
Console.WriteLine($"[INFO] [{ServerName}] Send handshake request");
try
{
var challengeToken = await _mcQuery.GetHandshake();
// Если всё ок, говорим, что мы в онлайне и сбрасываем счетчик попыток
IsOnline = true;
lock (_retryCounterLock)
{
RetryCounter = 0;
}
if(Debug)
Console.WriteLine($"[INFO] [{ServerName}] ChallengeToken is set up: " + BitConverter.ToString(challengeToken));
}
// Если что-то не так, увеличиваем счетчик неудачных попыток
catch (Exception ex)
{
if (ex is SocketException || ex is McQueryException || ex is ChallengeTokenIsNullException)
{
if(Debug)
Console.WriteLine($"[WARNING] [{ServerName}] [UpdateChallengeTokenTimer] Server doesn't response. Try to reconnect: {RetryCounter}");
if(ex is McQueryException)
Console.Error.WriteLine(ex);
lock (_retryCounterLock)
{
RetryCounter++;
if (RetryCounter >= RetryMaxCount)
{
RetryCounter = 0;
WaitForServerAlive(); // Переходим в режим восстановления соединения
}
}
}
else
{
throw;
}
}
}, null, 0, GettingChallengeTokenInterval);
// По таймеру запрашиваем текущее состояние
UpdateServerStatusTimer = new Timer(async obj =>
{
if (!IsOnline) return;
if(Debug)
Console.WriteLine($"[INFO] [{ServerName}] Send full status request");
try
{
var response = await _mcQuery.GetFullStatus();
IsOnline = true;
lock (_retryCounterLock)
{
RetryCounter = 0;
}
if(Debug)
Console.WriteLine($"[INFO] [{ServerName}] Full status is received");
OnFullStatusUpdated?.Invoke(this, new ServerStateEventArgs(ServerName, response));
}
// По аналогии с предыдущим
catch (Exception ex)
{
if (ex is SocketException || ex is McQueryException || ex is ChallengeTokenIsNullException)
{
if(Debug)
Console.WriteLine($"[WARNING] [{ServerName}] [UpdateServerStatusTimer] Server doesn't response. Try to reconnect: {RetryCounter}");
if(ex is McQueryException)
Console.Error.WriteLine(ex);
lock (_retryCounterLock)
{
RetryCounter++;
if (RetryCounter >= RetryMaxCount)
{
RetryCounter = 0;
WaitForServerAlive();
}
}
}
else
{
throw;
}
}
}, null, 500, GettingStatusInterval);
}
Осталось только реализовать ожидание восстановления соединения. Для этого нам достаточно убедиться, что мы получили хоть какой-то ответ от сервера. Для этого мы можем воспользоваться тем же запросом хэндшейка, который не требует наличия действующего ChallengeToken.
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();
}
// Пересоздаем сокет каждые 5 (настраивается) неудачных соединений
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);
}
===========
Источник:
habr.com
===========
Похожие новости:
- [.NET, C#] IQueryable порождает сильную связанность (перевод)
- [.NET] First touch of Kafka
- [Python, .NET, История IT] Языку программирования Python исполнилось 30 лет
- [Программирование, .NET, ASP, C#] Реализуем глобальную обработку исключений в ASP.NET Core приложении (перевод)
- [.NET] Как изменить формат данных JSON на Snake Case в ASP.NET Core Web API
- [.NET, PowerShell, Visual Studio, C#, F#] Работаем с notebook в VS Code с помощью расширения «dotnet interactive»
- [.NET, IT-инфраструктура, C#, DevOps] ProcInsp — веб-диспетчер задач для Windows
- [.NET, C#, Разработка под Linux, Разработка под Windows] Путешествие в unmanaged code: туда и обратно
- [.NET, C#] Делаем фильтры «как в экселе» на ASP.NET Core
- [Информационная безопасность, Программирование, .NET, C#, Разработка под Windows] Как следить (наблюдать) за компьютером. Часть 1 — делаем скриншоты пользователей
Теги для поиска: #_.net, #_c#, #_.net_core, #_.net, #_udp, #_setevoe_programmirovanie (сетевое программирование), #_minecraft, #_.net, #_c#
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:54
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Minecraft Server Query – это простой протокол, позволяющий получить актуальную информацию о состоянии сервера путём отправки пары-тройки незамысловатых UDP-пакетов.На вики есть подробное описание этого протокола с примерами реализации на разных языках. Однако меня поразило, насколько куцые реализации для .Net существуют на данный момент. Поискав некоторое время, я наткнулся на несколько репозиториев. Предлагаемые решения либо содержали банальные ошибки, либо имели урезанный функционал, хотя, казалось бы, куда ещё больше урезать-то.Так было принято решение написать свою реализацию.Скажи мне, кто ты...Для начала, посмотрим, что из себя представляет сам протокол Minecraft Query. Согласно вики, мы имеем в распоряжении 3 вида пакетов запросов и, соотвественно, 3 вида пакетов ответа:
Ответ на запрос BasicStatusА вот так – FullStatus Ответ на запрос FullStatusВсе данные, помимо тех, что хранятся в short, представлены в big-endian. А для поля SessionId, которое постоянно в рамках одного клиент-сервер соединения, должно выполняться условие SessionId & 0x0F0F0F0F == SessionId.В общем виде запрос выглядит так Запрос в общем видеБолее подробно об этом об этом можно почитать на вики.И я скажу тебе, как тебя распарситьДля начала, определимся, что мы хотим получить на выходе. Готовая библиотека должна предоставлять API для отправки любого из 3 видов пакетов и получения результата в распаршеном виде. При этом, я хочу больше свободы в плане поддержания жизнеспособности сокетов и обновления ChallengeToken. Если я буду запрашивать состояние сервера каждые 3 секунды, то я не хочу, чтобы вместо одного пакета запроса отправлялось два: хэндшейк и состояние. И наоборот, если я опрашиваю сервер раз в час, зачем мне слать запросы каждые 30 секунд? Поэтому работа с библиотекой будет происходить в "ручном" режиме.Итак, определившись, можем уже представить, как будет выглядеть архитектура классов. Я вижу работу примерно таким образом public static async Task<ServerState> DoSomething(IPAddress host, int port) {
var mcQuery = new McQuery(host, port); mcQuery.InitSocket(); await mcQuery.GetHandshake(); return await mcQuery.GetFullStatus(); } public class Request
{ // Набор констант для формирования пакета private static readonly byte[] Magic = { 0xfe, 0xfd }; private static readonly byte[] Challenge = { 0x09 }; private static readonly byte[] Status = { 0x00 }; public byte[] Data { get; private set; } private Request(){} public byte RequestType => Data[2]; public static Request GetHandshakeRequest(SessionId sessionId) { var request = new Request(); // Собираем пакет var data = new List<byte>(); data.AddRange(Magic); data.AddRange(Challenge); data.AddRange(sessionId.GetBytes()); request.Data = data.ToArray(); return request; } public static Request GetBasicStatusRequest(SessionId sessionId, byte[] challengeToken) { if (challengeToken == null) { throw new ChallengeTokenIsNullException(); } var request = new Request(); var data = new List<byte>(); data.AddRange(Magic); data.AddRange(Status); data.AddRange(sessionId.GetBytes()); data.AddRange(challengeToken); request.Data = data.ToArray(); return request; } public static Request GetFullStatusRequest(SessionId sessionId, byte[] challengeToken) { if (challengeToken == null) { throw new ChallengeTokenIsNullException(); } var request = new Request(); var data = new List<byte>(); data.AddRange(Magic); data.AddRange(Status); data.AddRange(sessionId.GetBytes()); data.AddRange(challengeToken); data.AddRange(new byte[] {0x00, 0x00, 0x00, 0x00}); // Padding request.Data = data.ToArray(); return request; } } public class SessionId
{ private readonly byte[] _sessionId; public SessionId (byte[] sessionId) { _sessionId = sessionId; } // Случайный SessionId public static SessionId GenerateRandomId() { var sessionId = new byte[4]; new Random().NextBytes(sessionId); sessionId = sessionId.Select(@byte => (byte)(@byte & 0x0F)).ToArray(); return new SessionId(sessionId); } public string GetString() { return BitConverter.ToString(_sessionId); } public byte[] GetBytes() { var sessionId = new byte[4]; Buffer.BlockCopy(_sessionId, 0, sessionId, 0, 4); return sessionId; } } public static class Response
{ public static byte ParseType(byte[] data) { return data[0]; } // public static SessionId ParseSessionId(byte[] data) { if (data.Length < 1) throw new IncorrectPackageDataException(data); var sessionIdBytes = new byte[4]; Buffer.BlockCopy(data, 1, sessionIdBytes, 0, 4); return new SessionId(sessionIdBytes); } public static byte[] ParseHandshake(byte[] data) { if (data.Length < 5) throw new IncorrectPackageDataException(data); var response = BitConverter.GetBytes(int.Parse(Encoding.ASCII.GetString(data, 5, data.Length - 6))); if (BitConverter.IsLittleEndian) { response = response.Reverse().ToArray(); } return response; } public static ServerBasicState ParseBasicState(byte[] data) { if (data.Length <= 5) throw new IncorrectPackageDataException(data); var statusValues = new Queue<string>(); short port = -1; data = data.Skip(5).ToArray(); // Skip Type + SessionId var stream = new MemoryStream(data); var sb = new StringBuilder(); int currentByte; int counter = 0; while ((currentByte = stream.ReadByte()) != -1) { if (counter > 6) break; // Парсим нормер порта if (counter == 5) { byte[] portBuffer = {(byte) currentByte, (byte) stream.ReadByte()}; if (!BitConverter.IsLittleEndian) portBuffer = portBuffer.Reverse().ToArray(); port = BitConverter.ToInt16(portBuffer); // Little-endian short counter++; continue; } // Парсим параметры-строки if (currentByte == 0x00) { string fieldValue = sb.ToString(); statusValues.Enqueue(fieldValue); sb.Clear(); counter++; } else sb.Append((char) currentByte); } var serverInfo = new ServerBasicState { Motd = statusValues.Dequeue(), GameType = statusValues.Dequeue(), Map = statusValues.Dequeue(), NumPlayers = int.Parse(statusValues.Dequeue()), MaxPlayers = int.Parse(statusValues.Dequeue()), HostPort = port, HostIp = statusValues.Dequeue(), }; return serverInfo; } // "Секции" пакета резделены константными последовательностями байт, // это можно испльзовать для проверки, что мы всё сделали правильно public static ServerFullState ParseFullState(byte[] data) { var statusKeyValues = new Dictionary<string, string>(); var players = new List<string>(); var buffer = new byte[256]; Stream stream = new MemoryStream(data); stream.Read(buffer, 0, 5); // Read Type + SessionID stream.Read(buffer, 0, 11); // Padding: 11 bytes constant var constant1 = new byte[] {0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00}; for (int i = 0; i < constant1.Length; i++) Debug.Assert(constant1[i] == buffer[i], "Byte mismatch at " + i + " Val :" + buffer[i]); var sb = new StringBuilder(); string lastKey = string.Empty; int currentByte; while ((currentByte = stream.ReadByte()) != -1) { if (currentByte == 0x00) { if (!string.IsNullOrEmpty(lastKey)) { statusKeyValues.Add(lastKey, sb.ToString()); lastKey = string.Empty; } else { lastKey = sb.ToString(); if (string.IsNullOrEmpty(lastKey)) break; } sb.Clear(); } else sb.Append((char) currentByte); } stream.Read(buffer, 0, 10); // Padding: 10 bytes constant var constant2 = new byte[] {0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00}; for (int i = 0; i < constant2.Length; i++) Debug.Assert(constant2[i] == buffer[i], "Byte mismatch at " + i + " Val :" + buffer[i]); while ((currentByte = stream.ReadByte()) != -1) { if (currentByte == 0x00) { var player = sb.ToString(); if (string.IsNullOrEmpty(player)) break; players.Add(player); sb.Clear(); } else sb.Append((char) currentByte); } ServerFullState fullState = new() { Motd = statusKeyValues["hostname"], GameType = statusKeyValues["gametype"], GameId = statusKeyValues["game_id"], Version = statusKeyValues["version"], Plugins = statusKeyValues["plugins"], Map = statusKeyValues["map"], NumPlayers = int.Parse(statusKeyValues["numplayers"]), MaxPlayers = int.Parse(statusKeyValues["maxplayers"]), PlayerList = players.ToArray(), HostIp = statusKeyValues["hostip"], HostPort = int.Parse(statusKeyValues["hostport"]), }; return fullState; } } public StatusWatcher(string serverName, string host, int queryPort)
{ ServerName = serverName; _mcQuery = new McQuery(Dns.GetHostAddresses(host)[0], queryPort); _mcQuery.InitSocket(); } public async Task Unwatch() { await UpdateChallengeTokenTimer.DisposeAsync(); await UpdateServerStatusTimer.DisposeAsync(); } public async void Watch() { // Обновляем challengetoken по таймеру каждые 30 секунд UpdateChallengeTokenTimer = new Timer(async obj => { if (!IsOnline) return; if(Debug) Console.WriteLine($"[INFO] [{ServerName}] Send handshake request"); try { var challengeToken = await _mcQuery.GetHandshake(); // Если всё ок, говорим, что мы в онлайне и сбрасываем счетчик попыток IsOnline = true; lock (_retryCounterLock) { RetryCounter = 0; } if(Debug) Console.WriteLine($"[INFO] [{ServerName}] ChallengeToken is set up: " + BitConverter.ToString(challengeToken)); } // Если что-то не так, увеличиваем счетчик неудачных попыток catch (Exception ex) { if (ex is SocketException || ex is McQueryException || ex is ChallengeTokenIsNullException) { if(Debug) Console.WriteLine($"[WARNING] [{ServerName}] [UpdateChallengeTokenTimer] Server doesn't response. Try to reconnect: {RetryCounter}"); if(ex is McQueryException) Console.Error.WriteLine(ex); lock (_retryCounterLock) { RetryCounter++; if (RetryCounter >= RetryMaxCount) { RetryCounter = 0; WaitForServerAlive(); // Переходим в режим восстановления соединения } } } else { throw; } } }, null, 0, GettingChallengeTokenInterval); // По таймеру запрашиваем текущее состояние UpdateServerStatusTimer = new Timer(async obj => { if (!IsOnline) return; if(Debug) Console.WriteLine($"[INFO] [{ServerName}] Send full status request"); try { var response = await _mcQuery.GetFullStatus(); IsOnline = true; lock (_retryCounterLock) { RetryCounter = 0; } if(Debug) Console.WriteLine($"[INFO] [{ServerName}] Full status is received"); OnFullStatusUpdated?.Invoke(this, new ServerStateEventArgs(ServerName, response)); } // По аналогии с предыдущим catch (Exception ex) { if (ex is SocketException || ex is McQueryException || ex is ChallengeTokenIsNullException) { if(Debug) Console.WriteLine($"[WARNING] [{ServerName}] [UpdateServerStatusTimer] Server doesn't response. Try to reconnect: {RetryCounter}"); if(ex is McQueryException) Console.Error.WriteLine(ex); lock (_retryCounterLock) { RetryCounter++; if (RetryCounter >= RetryMaxCount) { RetryCounter = 0; WaitForServerAlive(); } } } else { throw; } } }, null, 500, GettingStatusInterval); } 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(); } // Пересоздаем сокет каждые 5 (настраивается) неудачных соединений 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); } =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:54
Часовой пояс: UTC + 5