Источник Хабрахабр.ru, Москва
Заголовок Реализация Minecraft Query протокола в .Net Core
Дата 20210223

Этим цветом    обозначаются известные системе слова и выражения, принимавшие участие в анализе данного текста, а таким    - идентифицированные, то есть соотнесенные с каким-либо объектом онтологической базы

============= Обработанный текст:

Реализация Minecraft Query протокола в .Net Core

Реализация Minecraft Query протокола в .Net Core

.NET ,

C#

Из песочницы

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• Телекоммуникации и связь » Компьютерная сеть » Интернет » DNS » Host (программа)

• Информационные технологии » Информационно-коммуникационные технологии » Информационные технологии и телекоммуникации » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)

• Информационные технологии » Информатика » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)

• Высокие технологии » Информационные технологии и телекоммуникации » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)
, int port) {
var mcQuery = new McQuery(host• Телекоммуникации и связь » Компьютерная сеть » Интернет » DNS » Host (программа)

• Информационные технологии » Информационно-коммуникационные технологии » Информационные технологии и телекоммуникации » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)

• Информационные технологии » Информатика » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)

• Высокие технологии » Информационные технологии и телекоммуникации » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » 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• Телекоммуникации и связь » Компьютерная сеть » Интернет » DNS » Host (программа)

• Информационные технологии » Информационно-коммуникационные технологии » Информационные технологии и телекоммуникации » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)

• Информационные технологии » Информатика » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)

• Высокие технологии » Информационные технологии и телекоммуникации » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)
, int queryPort) {
ServerName = serverName; _mcQuery = new McQuery(Dns• Телекоммуникации и связь » Компьютерная сеть » Интернет » DNS.GetHostAddresses(host• Телекоммуникации и связь » Компьютерная сеть » Интернет » DNS » Host (программа)

• Информационные технологии » Информационно-коммуникационные технологии » Информационные технологии и телекоммуникации » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)

• Информационные технологии » Информатика » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » Host (программа)

• Высокие технологии » Информационные технологии и телекоммуникации » Программирование » Платформы программирования » Операционные системы » UNIX » Утилиты Unix » 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); }

============= Итог: 4,5625 ; Телекоммуникации и связь#Компьютерная сеть#Серверы 4,2406 ; Телекоммуникации и связь#Компьютерная сеть#Интернет#DNS#Host (программа) 2,9411 ; Информационные технологии#Информационно-коммуникационные технологии #Информационные технологии и телекоммуникации#Программирование #Программное обеспечение#Конструкции Компьютерных Языков #Конструктор Объекта 2,7166 ; Информационные технологии#Информационно-коммуникационные технологии #Информационные технологии и телекоммуникации#Вычислительная техника #Компьютер#Порядок байтов


Copyright © 2007-2021 ООО «RelTeam»