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