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

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

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

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

Кто владеет телом пакета​

Тело пакета - не собственная копия байт, а окно в блок, который читатель арендует у пула. Пакет держит ссылку на этот блок, и блок уходит обратно в пул, когда отпущена последняя ссылка. При сжатии блоков два - под сжатые байты и под распакованные - но первый уходит сразу после распаковки, и правило для Body не меняется.

ReadPacketAsync отдаёт ссылку вызывающему: пакет принадлежит ему, он же его и освобождает.

using var packet = await client.ReadPacketAsync(token);
Handle(packet.Id, packet.Body.Span);

ReadPacketsAsync не отдаёт, а одалживает: каждый пакет перечисления взят взаймы, настоящую ссылку цикл отпускает перед следующим шагом, и тело живо внутри одного витка.

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

var kept = new List<IncomingPacket>();

await foreach (var packet in client.ReadPacketsAsync(token))
{
if (packet.Id == interestingId)
kept.Add(packet.Retain());
}

Body.ToArray() по-прежнему копирует байты, и копия по-прежнему уместна, когда данные уходят в код, который про пул ничего не знает, - но Retain не выделяет памяти, так что держать сам пакет дешевле.

Куда дальше уходит разобранный пакет - какой метод обработчика он вызовет и что происходит с неизвестными 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 делает то же пачками. Про оба - в «Соединении без клиента».

Дальше​