Pull to refresh

Comments 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 для генерации очередного числа. Принимайте такие вещи параметром.

Имеет ли смысл инициализировать Random, как статическое поле для всех экземпляров? Или лучше принимать параметром?


Например, как-нибудь так


private static Random _rnd;
private static Random Rnd
{
    get => _rnd ??= new Random();
    set => _rnd = value;
}

Статическое поле приведёт к нарушению потокобезопасности. Только параметром.

Будет ли выкладываться код в общий доступ?

Промазал по ветке. Чуть ниже скинул ссылку на репозиторий

Дошли руки поковыряться в коде парсера. Автор, использование 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, очередью и диким циклом:


ParseBasicState
        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 — наконец-то можно явно задать порядок байт!


Разбор "большого" пакета не сильно-то и сложнее:


ParseFullState
        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 и его получением/обновлением полностью автоматизирована.

+ Решен вопрос получения соответствующего ответа при отправке множества запросов на один сервер. Раньше при отправке двух типов запросов одновременно на ожидание полного ответа мог прийти базовый и наоборот, что приводило к ошибке парсинга.

Осталось только сам парсинг сделать "по-человечески", в чем я возьму на вооружение Ваш ответ.

Только не забудьте где требуется ref поставить, а то я код свой код без IDE писал.

А если пакет в "сыром" виде представляет из себя набор байт, считывать которые необходимо побитово, имеет ли смысл пытаться пользовать для этого System.Buffers или есть что-то более подходящее?

Например, если пакет имеет вид: первый бит для указания состояния (рабочее/не рабочее), следующие 23 бита - широта, затем идет 0, следующие 24 - долгота, затем идут 5 бит смещения, затем 12 бит высоты, и так далее.

Ну, напрямую System.Buffers тут не помогут, но и чего-то более подходящего я не помню.

Sign up to leave a comment.

Articles