Первая модель¶
Понадобятся Python 3.10+ и установленный bytespec:
pip install bytespec
или
uv add bytespec
Выполняйте блоки по порядку в одном файле user.py.
1. Описать данные¶
from bytespec import ProtoModel
class User(ProtoModel):
name: str
active: bool
Класс описывает два поля: строку name и логическое значение active.
Наследование от ProtoModel добавляет запись и чтение байтов. Для этих типов
дополнительные настройки не нужны.
2. Создать пользователя¶
user = User(name="Anna", active=True)
print(user.name, user.active)
Anna True
Это обычные атрибуты Python-объекта. Оба поля обязательны; передавайте значения по именам.
3. Записать в байты¶
encoded = user.encode()
print(type(encoded).__name__, len(encoded))
bytes 23
encode() вернул 23 байта. Их можно сохранить в файл или передать по сети.
Посмотрим на все байты сообщения:
print(encoded.hex(" "))
00 01 00 00 00 00 00 00 00 00 00 00 00 09 00 00 00 04 41 6e 6e 61 01
Как читать эту строку?
Каждая пара, например 04 или 6e, обозначает один байт в
шестнадцатеричной записи: после цифр 0–9 идут буквы a–f.
04 — число 4, 09 — 9, 41 — 65.
hex(" ") только показывает байты с пробелами; сами данные не меняются.
4. Прочитать обратно¶
decoded = User.decode(encoded)
print(decoded.name, decoded.active)
print(decoded == user, decoded is user)
Anna True
True False
Восстановились те же значения, но это новый объект. User.decode()
знает типы и порядок полей из класса User. Имена полей в сообщение
не записываются: получателю нужен тот же класс с теми же настройками.
Основной цикл уже готов: класс → объект → bytes → новый объект.
5. Связать код и байты¶
Поля занимают последние 9 байт. Перед ними bytespec добавил 14 служебных байт — заголовок (header). Его три части видны на схеме:
2 байта
Идентификатор сообщения.
8 байт
Наличие необязательных полей.
4 байта
Размер всех полей в байтах.
9 байт
name, затем active.
Заголовок: 2 + 8 + 4 = 14 байт. Всё сообщение: 14 + 9 = 23 байта. Настраивать заголовок для этого примера не требуется.
user = User(name="Anna", active=True)
encoded = user.encode()
assert User.decode(encoded) == user
Теперь можно пропустить известные 14 байт заголовка:
print(encoded[14:].hex(" "))
00 00 00 04 41 6e 6e 61 01
① Длина имени · 00 00 00 04
Число 4: следующие четыре байта принадлежат строке name.
Это префикс длины — число перед данными, обозначающее их размер.
② Имя · 41 6e 6e 61
Буквы A, n, n, a в кодировке UTF-8. Каждая занимает один байт. Для других букв размер может отличаться: например, «я» занимает два.
③ Активен · 01
True записывается как 01, False — как 00.
После имени читается ровно один байт.
В name входят четыре байта длины и четыре байта текста. active
занимает ещё один: 4 + 4 + 1 = 9. Так bytespec находит конец строки
и начало следующего поля.
Что находится в 14 байтах заголовка?
00 01— значение__constructor__ = 1по умолчанию. При чтении bytespec сверяет его с выбранным классом; сам класс не выбирает.Следующие восемь
00—Flags. Необязательных полей пока нет, поэтому все биты равны нулю.00 00 00 09—PayloadLength: девять байт полей после заголовка.
Payload — содержимое сообщения после заголовка, здесь name и
active вместе с префиксом строки. Заголовок в эту длину не входит.
Настройки для существующего протокола разобраны в Header и framing.
Почему длина 4 записана как 00 00 00 04?
Для длины строки по умолчанию выделено четыре байта. Старшие байты идут
первыми; такой порядок байтов (byte order) называется big-endian.
При little-endian то же число выглядело бы как 04 00 00 00.
Настраивать это нужно, когда вы описываете формат с другим порядком байтов.
Термины собраны в Основные понятия. Далее — Поля и значения по умолчанию: добавим значение
по умолчанию для active.