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

Кадры: где кончается пакет

TCP отдаёт непрерывный поток байт. Отправитель мог вызвать Write три раза подряд, а получатель увидит эти байты одним куском, двумя или пятью - границ вызовов протокол не сохраняет. Чтобы понять, где кончается один пакет и начинается следующий, перед телом пакета нужна длина. Это и есть кадр: служебная обвязка вокруг пакета, которая делает границу видимой на приёмной стороне - в протоколе она описана на странице Packet format. McProtoNet.Transport.Framing собирает и разбирает эту обвязку.

Кадр без сжатия

Без сжатия кадр - это длина пакета как VarInt, за которой идёт сам пакет: VarInt-идентификатор и тело. PacketWriteExtensions пишет его в две строки:

writer.WriteVarInt(packet.Length);
writer.Write(packet);

Читающая сторона идёт в обратном порядке: сначала VarInt длины, байт за байтом, затем ровно столько байт тела, сколько эта длина назвала - ни байтом больше.

Кадр со сжатием

Когда у PacketStreamReader/PacketStreamWriter включён CompressionThreshold, после длины кадра появляется второй VarInt. Пакет короче порога идёт как есть, и этот VarInt - просто 0. Пакет не короче порога сжимается целиком (идентификатор и тело вместе) через libdeflate, и тогда второй VarInt несёт несжатый размер - он нужен, чтобы выделить буфер под распаковку заранее:

writer.WriteVarInt(compressedLength + uncompressedSize.GetVarIntLength());
writer.WriteVarInt(uncompressedSize);
writer.Write(rented.AsSpan(0, compressedLength));

Первый VarInt здесь - длина всего, что после него: размер несжатого значения плюс сжатые байты. Читающая сторона смотрит на второй VarInt: 0 или отрицательное значит «пакет не сжат, дальше его сырые байты», положительное - «дальше zlib, распаковать в буфер именно такого размера». Если после распаковки байт вышло больше или меньше заявленного, кадр считается повреждённым.

Кто этим занимается

PacketWriteExtensions - статический класс с кирпичами кадра: одни и те же методы WritePacket работают поверх IBufferWriter<byte>, PipeWriter и обычного Stream, синхронно и асинхронно. StreamingConnection собирает из них кадры пачками через BufferedPacketReader и PacketBatch

  • про это разбор в «Соединении без клиента». PooledBufferWriter - вспомогательный буфер из пула: PacketStreamWriter использует его, когда включён шифр, чтобы собрать кадр целиком в памяти перед тем, как прогнать его через PacketCipher
  • шифру нужен весь кадр сразу, кусками его не отдать.

Транспорт про содержимое пакета не знает ничего: кадр несёт номер пакета и байты, а что это за пакет и какие у него поля - решает пакетный слой.

Ограничения и ошибки

Длина кадра - от 1 до 32 МиБ (BufferedPacketReader.MaxFrameLength). Ноль, отрицательное значение или превышение потолка - InvalidDataException от ThrowHelper.ThrowInvalidFrameLength. VarInt длины не может занимать больше пяти байт - если занимает, это ThrowVarIntTooLong. С несжатым размером та же проверка потолка: если он выходит за 32 МиБ, кадр отклоняется до попытки выделить буфер. Провал распаковки или несовпадение итогового размера с заявленным несжатым размером - тоже InvalidDataException, с разными сообщениями (ThrowDecompressFailed, ThrowDecompressSizeMismatch).

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

PacketStreamReader и PacketStreamWriter не читают и не пишут два кадра одновременно - параллельный ReadPacketAsync/WritePacketAsync поверх незавершённого первого получает InvalidOperationException, как и везде в транспорте (там же). По той же причине Cipher и CompressionThreshold меняются только между кадрами - попытка сменить их посреди чтения или записи тоже бросает InvalidOperationException. ConnectionAbortedException к этому слою отношения не имеет - это исключение уровня соединения, разобрано в «Потоке пакетов».

Где это трогает приложение напрямую

Обычно кадры не видно: соединение прячет их за IncomingPacket/OutgoingPacket. Но когда MinecraftConnection не нужен - например, для рукопожатия до логина или для короткого протокольного разговора без буферизации - PacketStreamReader и PacketStreamWriter дают тот же формат кадра напрямую поверх любого Stream, по одному пакету за вызов. Тело пакета - окно в буфер, которое живёт до следующего чтения; разбирать его нужно сразу, не через await («Буфер приёма»).

Дальше