Кадры: где кончается пакет
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 («Буфер приёма»).
Дальше
- Поток пакетов - как из кадров складывается поток
- Сжатие и шифрование - что кадр здесь только упоминает
- Чтение и запись примитивов - откуда берётся VarInt