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