Комментарии 19
Зачем такая жесть с таймерами? Почему нельзя сделать просто?
private Task watchTask;
private CancellationTokenSource watchCts;
public void Watch() {
if (watchCts != null) return;
watchCts = new CancellationTokenSource();
watchTask = Task.Run(() => RunWatch(watchCts.Token));
}
public Task Unwatch() {
watchCts?.Cancel();
var result = watchTask;
watchCts = null;
watchTask = null;
return result ?? Task.Completed;
}
private async Task RunWatch(CancellationToken stopToken) {
while (true) {
TimeSpan delay;
try {
await mcQuery.GetHandshake();
var status = await mcQuery.GetFullStatus();
// …
delay = GettingStatusInterval;
} catch (SocketException) {
// …
delay = RetryInterval;
}
try {
await Task.Delay(delay, stopToken);
} catch (TaskCanceledException) {
return;
}
}
}
Мне самому не нравится, как я перемудрил с таймерами и как раз смотрел в сторону того, как это разрешить. Спасибо.
Однако, опять же, мне не нравится, что тут каждый раз для получения статуса запрашивается токен у сервера. Поэтому я его определил в отдельный таймер. Надо будет найти золотую середину.
Не верю, что кому-то нужно запрашивать статус сервера чаще чем раз в полминуты — а дольше, по вашим же словам, токен и не проживёт.
Но можно и проверку добавить:
var lastHandshake = DateTime.MinValue;
while (true) {
TimeSpan delay;
try {
var now = DateTime.UtcNow;
if (now - lastHandshake > TokenLivespan) {
lastHandshake = now;
await mcQuery.GetHandshake();
}
await mcQuery.GetFullStatus();
// …
} catch (SocketException) {
// …
}
// …
}
А ещё лучше — если mcQuery сам будет следить за временем жизни токена и при необходимости получать его, тогда прикладной код будет ещё проще.
Ну вот я, например, написал нотификатор входа/выхода пользователя на сервер. На мой взгляд, странно, когда пользователь вошел, а уведомление пришло через полминуты.
А ещё лучше — если mcQuery сам будет следить за временем жизни токена и при необходимости получать его, тогда прикладной код будет ещё проще.
Справедливо
Можно по-подробнее? Хотелось бы разобраться
Для начала, всем спискам надо бы указать начальную capacity, благо вы размеры всех пакетов-запросов знаете. Или же можно использовать System.Buffers.ArrayBufferWriter
, но это не обязательно.
Далее, если парсить ответы не через MemoryStream, а через массив с переменной-индексом — можно избавиться от всех временных массивов при парсинге. Ещё при желании можно воспользоваться структурой System.Buffers.SequenceReader
, если массив с индексом выглядит слишком олдскульно.
Ну а дальше надо просто ходить по коду и искать лишние копирования которых можно было бы избежать. Например, сессию в пакет можно записать вот так:
public void WriteTo(List<byte> list) {
list.AddRange(_sessionId);
}
Опять-таки, для генерации сессии LINQ не требуется:
rnd.NextBytes(sessionId);
for (int i=0; i<sessionId.Length; i++)
sessionId[i] &= 0x0f;
PS кстати, никогда не создавайте новый Random для генерации очередного числа. Принимайте такие вещи параметром.
Промазал по ветке. Чуть ниже скинул ссылку на репозиторий
Вынес в отдельный репозиторий для удобства.
Он уже. Но пока только как часть нотификатора. Репозиторий
Дошли руки поковыряться в коде парсера. Автор, использование System.Buffers упрощает код многократно! Могу уверенно заявить: отныне за использование MemoryStream для задачи разбора пакетов надо бить по рукам.
Для начала введём вспомогательный метод, который читает последовательность байт до нулевого и превращает её в строку:
private static string ReadString(SequenceReader<byte> reader)
{
if (!reader.TryReadTo(out ReadOnlySequence<byte> bytes, delimiter: 0, advancePastDelimiter: true))
throw new IncorrectPackageDataException("Zero byte not found", reader.Sequence.ToArray());
return Encoding.ASCII.GetString(bytes); // а точно ASCII? Может, Utf8?
}
Этого метода достаточно, чтобы без каких бы то ни было хитростей просто прочитать все поля протокола подряд. Вот код чтения ответа на базовый запрос (просто сравните с тем что там было раньше, со StringBuilder, очередью и диким циклом:
public static ServerBasicState ParseBasicState(byte[] data)
{
if (data.Length <= 5)
throw new IncorrectPackageDataException(data);
var reader = new SequenceReader<byte>(new ReadOnlySequence<byte>(data));
reader.Advance(5); // Skip Type + SessionId
var serverInfo = new ServerBasicState();
serverInfo.Motd = ReadString(reader);
serverInfo.GameType = ReadString(reader);
serverInfo.Map = ReadString(reader);
serverInfo.NumPlayers = int.Parse(ReadString(reader));
serverInfo.MaxPlayers = int.Parse(ReadString(reader));
if (!reader.TryReadLittleEndian(out short port))
throw new IncorrectPackageDataException(data);
serverInfo.HostPort = port;
serverInfo.HostIp = ReadString(reader);
return serverInfo;
}
Обратите внимание также на метод TryReadLittleEndian — наконец-то можно явно задать порядок байт!
Разбор "большого" пакета не сильно-то и сложнее:
private static readonly byte[] constant1 = new byte[] { 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00 };
private static readonly byte[] constant2 = new byte[] {0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00}
public static ServerFullState ParseFullState(byte[] data)
{
if (data.Length <= 5)
throw new IncorrectPackageDataException(data);
var reader = new SequenceReader<byte>(new ReadOnlySequence<byte>(data));
reader.Advance(5); // Read Type + SessionID
if (!reader.IsNext(constant1, advancePast: true))
throw new IncorrectPackageDataException(data);
var statusKeyValues = new Dictionary<string, string>();
while (!reader.IsNext(0, advancePast: true))
{
var key = ReadString(reader);
var value = ReadString(reader);
statusKeyValues.Add(key, value);
}
if (!reader.IsNext(constant2, advancePast: true)) // Padding: 10 bytes constant
throw new IncorrectPackageDataException(data);
var players = new List<string>();
while (!reader.IsNext(0, advancePast: true))
{
players.Add(ReadString(reader));
}
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;
}
}
Благодарю. Действительно, разбор пакетов выглядит проще. Я как раз обновил исходный код библиотеки, перелопатил архитектуру, теперь проблема с challengeToken и его получением/обновлением полностью автоматизирована.
+ Решен вопрос получения соответствующего ответа при отправке множества запросов на один сервер. Раньше при отправке двух типов запросов одновременно на ожидание полного ответа мог прийти базовый и наоборот, что приводило к ошибке парсинга.
Осталось только сам парсинг сделать "по-человечески", в чем я возьму на вооружение Ваш ответ.
А если пакет в "сыром" виде представляет из себя набор байт, считывать которые необходимо побитово, имеет ли смысл пытаться пользовать для этого System.Buffers или есть что-то более подходящее?
Например, если пакет имеет вид: первый бит для указания состояния (рабочее/не рабочее), следующие 23 бита - широта, затем идет 0, следующие 24 - долгота, затем идут 5 бит смещения, затем 12 бит высоты, и так далее.
Реализация Minecraft Query протокола в .Net Core