Header и framing¶
Протокол может хранить длину перед flags или обходиться без идентификатора
сообщения. __header__ задаёт служебную часть перед полями модели:
from bytespec import Constructor, Flags, PayloadLength, ProtoModel, field
from bytespec.types import UInt8, VarUInt
class Packet(ProtoModel):
__constructor__ = 0x12
__header__ = (Constructor(1), PayloadLength(1), Flags(1))
number: UInt8
note: str | None = field(flag=0, prefix_length=1)
packet = Packet(number=7, note="A")
encoded = packet.encode()
assert Packet.decode(encoded) == packet
print(encoded.hex(" "))
12 03 01 07 01 41
Здесь 12 — constructor, 03 — длина всех полей, 01 — flags.
Body занимает три байта: 07 и строка 01 41. В длину не входит
ни один элемент header, даже если PayloadLength стоит перед Flags.
Без настройки используется (Constructor(2), Flags(8), PayloadLength(4)):
14 байт перед полями. По умолчанию __constructor__ = 1 и
__byte_order__ = ByteOrder.BIG.
Что можно включить в header¶
Элемент |
Значение |
|---|---|
|
Записывает |
|
Вычисляет число байтов body; при чтении ограничивает ими поля |
|
Вычисляет bitmap присутствующих optional-полей по |
encoding — 1, 2, 4 или 8 байт без знака, либо
аннотация VarUInt. Например, Constructor(VarUInt) кодирует число 300
как ac 02. VarInt, UInt16 и экземпляр codec вместо этих параметров
не поддерживаются. Фиксированные числа используют порядок байтов модели;
varint от него не зависит. Те же варианты доступны для всех трёх элементов.
class CompactPacket(ProtoModel):
__constructor__ = 300
__header__ = (Constructor(VarUInt), Flags(VarUInt), PayloadLength(VarUInt))
number: UInt8
note: str | None = field(flag=7, prefix_length=1)
compact = CompactPacket(number=7, note="A")
assert CompactPacket.decode(compact.encode()) == compact
print(compact.encode().hex(" "))
ac 02 80 01 03 07 01 41
Constructor 300 занимает два байта, bitmap с битом 7 — ещё два,
длина body 3 — один. Кодирование unsigned чисел здесь отличается от
VarInt, используемого по умолчанию для обычных int-полей.
Constructor — числовой идентификатор формата, а не вызов Python-конструктора. Библиотека проверяет его только после выбора класса приложением: реестра моделей и автоматического dispatch нет.
Изменить порядок¶
Переставьте элементы tuple; поля body при этом сохраняют свой порядок:
class Reordered(ProtoModel):
__constructor__ = 0x12
__header__ = (Flags(1), Constructor(1), PayloadLength(VarUInt))
number: UInt8
note: str | None = field(flag=0, prefix_length=1)
packet = Reordered(number=7, note="A")
assert Reordered.decode(packet.encode()) == packet
print(packet.encode().hex(" "))
01 12 03 07 01 41
Header задаётся при объявлении класса. Менять его элементы и настройки после создания схемы не следует. Правила переиспользования между классами: Наследование моделей.
Убрать ненужные элементы¶
Если optional-полей нет, bitmap не нужен:
class Reading(ProtoModel):
__header__ = (Constructor(1), PayloadLength(1))
value: UInt8
reading = Reading(value=7)
assert Reading.decode(reading.encode()) == reading
print(reading.encode().hex(" "))
01 01 07
Можно аналогично убрать Constructor: например,
__header__ = (PayloadLength(2),) для обязательных полей, когда тип
сообщения уже определён транспортом.
Без PayloadLength¶
Если граница сообщения определяется полями, общую длину можно не передавать:
class Reading(ProtoModel):
__header__ = (Constructor(1),)
value: UInt8
buffer = b"xx" + Reading(value=7).encode() + Reading(value=8).encode()
first, end = Reading.decode_from(buffer, 2)
second, end = Reading.decode_from(buffer, end)
print(first.value, second.value, end)
7 8 6
decode_from() возвращает конец последнего прочитанного поля. Он больше
не пропускает неизвестный хвост body и не знает отдельную границу сообщения:
codec видит весь переданный буфер, включая следующее сообщение. Для входа
с внешней границей передавайте только разрешённый срез. Проверки собственных
размеров полей остаются; decode() по-прежнему запрещает байты после
прочитанных полей. Prefix строки, bytes или списка не исчезает вместе с
PayloadLength.
Вообще без framing¶
Для записи структуры в уже существующий формат задайте пустой tuple:
from bytespec import ByteOrder
from bytespec.types import UInt16
class Point(ProtoModel):
__header__ = ()
__byte_order__ = ByteOrder.LITTLE
x: UInt16
y: UInt16
point = Point(x=1, y=256)
assert Point.decode(point.encode()) == point
print(point.encode().hex(" "))
01 00 00 01
Здесь ровно четыре байта полей. Модели без полей тоже поддерживаются: например, сообщение-подтверждение может содержать только framing.
class Ack(ProtoModel):
__constructor__ = 0x12
__header__ = (Constructor(1), PayloadLength(1))
class Empty(ProtoModel):
__header__ = ()
assert Ack.decode(Ack().encode()) == Ack()
assert Empty.decode(b"") == Empty()
print(Ack().encode().hex(" "))
print(Empty().encode())
12 00
b''
Ack записывает constructor и нулевую длину body. Empty без полей
и header записывает пустые bytes. Такой объект не подходит для элемента
списка: по нулю байтов нельзя восстановить количество элементов
(см. Требования к реализации).
Как записывается header вложенной модели¶
Вложенная модель использует свой порядок байтов и свой __header__,
но её Constructor автоматически пропускается: тип уже известен из
аннотации поля. Остальные элементы остаются в указанном порядке:
from bytespec import ByteOrder, Constructor, Flags, PayloadLength
from bytespec.types import UInt8, UInt16
class Reading(ProtoModel):
__constructor__ = 0x34
__byte_order__ = ByteOrder.LITTLE
__header__ = (PayloadLength(1), Constructor(1), Flags(1))
value: UInt16
class Frame(ProtoModel):
__header__ = ()
reading: Reading
tail: UInt8
reading = Reading(value=0x1234)
frame = Frame(reading=reading, tail=9)
assert Frame.decode(frame.encode()) == frame
print(reading.encode().hex(" "))
print(frame.encode().hex(" "))
02 34 00 34 12
02 00 34 12 09
Отдельная запись: длина 02, constructor 34, flags 00, value
34 12. Во вложенной записи отсутствует только constructor; 09 —
следующее поле внешней модели. Пустой header внешнего класса не убирает
framing вложенного. При стандартных настройках вложенный header занимает
12 байт: 8 байт flags и 4 байта длины.
Для list[Reading] действует то же правило:
class Batch(ProtoModel):
__header__ = ()
readings: list[Reading] = field(prefix_length=1)
batch = Batch(readings=[reading])
assert Batch.decode(batch.encode()) == batch
print(batch.encode().hex(" "))
04 02 00 34 12
04 — размер закодированного элемента вместе с оставшимся framing.
Вложенный __header__ = () убирает и его; пример структурных полей
ver/cmd/opcode/seq показан ниже. Собственные правила длины
вложенной модели сохраняются; без PayloadLength конец определяется
её известными полями.
Данные протокола остаются полями¶
ver, cmd, opcode и seq — значения приложения, а не вычисляемое
framing. Вынесите их в обычную вложенную модель:
class Header(ProtoModel):
__header__ = ()
ver: UInt8
cmd: UInt8
opcode: UInt16
seq: UInt16
class Envelope(ProtoModel):
__header__ = (PayloadLength(2),)
header: Header
payload: bytes = field(prefix_length=1)
packet = Envelope(header=Header(ver=1, cmd=2, opcode=18, seq=22), payload=b"OK")
assert Envelope.decode(packet.encode()) == packet
print(packet.encode().hex(" "))
00 09 01 02 00 12 00 16 02 4f 4b
00 09 — длина body; дальше шесть байтов Header, затем
02 4f 4b — bytes с собственным префиксом. Объект packet.header
содержит данные приложения; Envelope.__header__ описывает framing.
Вложенная модель сохраняет свой header без Constructor:
точный пример — Как записывается header вложенной модели.
Что проверяется в 0.1.0¶
При создании элемента проверяется поддерживаемое encoding. При сборке
модели Constructor требует обычный int (не bool), а Flags
проверяет вместимость bitmap. Например, бит 8 не помещается в один байт:
from bytespec import SchemaError
try:
class TooManyFlags(ProtoModel):
__header__ = (Flags(1),)
note: str | None = field(flag=8)
except SchemaError as error:
print(error)
Flags encoding provides 8 bits, model has field with flag 8
Номера flags всегда ограничены 0–63, в том числе для Flags(VarUInt).
Диапазон самого constructor проверяется при encode, с EncodeError;
неверный constructor в сообщении даёт DecodeError.
Для optional-полей требуется Flags в итоговом header. Это проверяется
при объявлении класса, в том числе для унаследованных полей:
try:
class MissingFlags(ProtoModel):
__header__ = (PayloadLength(1),)
note: str | None = field(flag=0)
except SchemaError as error:
print(error)
class WithFlags(ProtoModel):
__header__ = (Flags(1), PayloadLength(1))
note: str | None = field(flag=0)
try:
class WithoutFlags(WithFlags):
__header__ = (PayloadLength(1),)
except SchemaError as error:
print(error)
Found optional fields in model but no Flags presented in header
Found optional fields in model but no Flags presented in header
Задавайте каждый вид элемента не более одного раза: уникальность видов элементов header отдельно не проверяется. Изменённый header подкласса проверяется заново, даже если подкласс не объявляет собственных полей.
Далее — Codecs: свой формат и свои типы: как задать представление отдельного значения.