Перейти к основному содержимому

Поток пакетов

MinecraftClient.ReadPacketsAsync - тонкая обёртка вокруг одного цикла: клиент держит один MinecraftConnection, и метод раз за разом отдаёт управление его ReadPacketAsync. Очереди кадров нет, и буферизации вперёд нет: за один вызов транспорт читает ровно один кадр - длину, затем тело - и отдаёт его наружу как IncomingPacket. Где кончается кадр и как читается длина - в «Кадрах».

Поток пакетов не знает, в какой фазе клиент - handshaking, login, configuration или play: кадры через него идут одинаково во всех фазах. Фазы ведёт код приложения («Фаза и направление»).

Буфер приёма

Тело пакета - не собственная копия байт, а окно в буфер, который читатель арендует у ArrayPool<byte>. Буфер держится, пока не начался следующий ReadPacketAsync: тогда прежний буфер возвращается в пул, и данные, на которые смотрело старое IncomingPacket.Body, становятся чужими. При сжатии буферов два

  • под сжатые байты и под распакованные - но первый освобождается сразу после распаковки, и правило для Body не меняется.

Отсюда и правило из «Первого бота»: пакет разбирается сразу, Body через await не тащится. Если данные нужны дольше - например, лечь в очередь для другого потока - их копируют явно, Body.ToArray() или похожим способом; сам буфер для долгого хранения не годится.

var toKeep = new List<byte[]>();

await foreach (var packet in client.ReadPacketsAsync(token))
{
if (packet.Id == interestingId)
toKeep.Add(packet.Body.ToArray()); // копия, не окно
}

Куда дальше уходит разобранный пакет - какой метод обработчика он вызовет и что происходит с неизвестными id - описано в «Обработчиках и неизвестных пакетах».

Конец сеанса

Перечисление ReadPacketsAsync никогда не заканчивается тихо - оно всегда бросает исключение. Полная таблица «что произошло → какое исключение» - в «Отмене, ошибках, закрытии».

Отмена

Токен отмены не отменяет одно чтение: если чтение уже тронуло сокет, отмена закрывает соединение целиком, и цикл ReadPacketsAsync только останавливается. Полная картина - в «Отмене, ошибках, закрытии».

Отправка

SendAsync и SendRawAsync проходят через общий гейт - SemaphoreSlim(1, 1) внутри MinecraftClient. Каждый вызов сначала занимает гейт, потом пишет кадр через соединение, потом гейт отпускает. Если несколько задач шлют пакеты одновременно, кадры не перемешиваются: вызовы встают в очередь на гейте и уходят на сокет по одному, каждый целиком.

await Task.WhenAll(
client.SendAsync(new PlaySb.KeepAlivePacket(keepAliveId), pv).AsTask(),
client.SendRawAsync(customId, customBody).AsTask());

Чтение так не защищено: параллельный ReadPacketAsync - это InvalidOperationException, ошибка вызывающего кода, а не гонка данных («Отмене, ошибках, закрытии»). У ReadPacketsAsync обхода для этого нет: пока await foreach не отдал управление обратно, второе чтение той же связи начинать нельзя.

Закрытие

DisposeAsync закрывает соединение и отпускает буферы; порядок шагов и таблица исключений - в «Отмене, ошибках, закрытии».

Когда клиент лишний

Тот же поток пакетов доступен без MinecraftClient: MinecraftConnection читает и пишет по одному кадру поверх любого Stream, StreamingConnection делает то же пачками. Про оба - в «Соединении без клиента».

Дальше