Практические задачи¶
Выберите задачу и начните с готовых байтов. Примеры не требуют подключения к сети; все пакеты уже находятся в памяти.
Прочитать пакет устройства из TCP¶
Устройство прислало 10 00 04 01 02 4f 4b. По его протоколу это:
один байт команды, два байта длины полей, версия и данные с однобайтовой длиной.
Опишем ответ команды 0x10:
from bytespec import Constructor, PayloadLength, ProtoModel, field
from bytespec.types import UInt8
class Reply(ProtoModel):
__constructor__ = 0x10
__header__ = (Constructor(1), PayloadLength(2))
version: UInt8
data: bytes = field(prefix_length=1)
received = bytes.fromhex("10 00 04 01 02 4f 4b")
reply = Reply.decode(received)
print(reply.version, reply.data)
assert reply.encode() == received
1 b'OK'
① Команда · 10
Значение __constructor__ = 0x10. Сообщение другой команды не пройдёт проверку Reply.
② Длина полей · 00 04
Четыре байта после заголовка: версия, длина данных и два байта OK.
③ Версия · 01
UInt8 читает один байт и возвращает обычное целое число 1.
④ Данные · 02 4f 4b
Длина 2, затем байты OK. data остаётся bytes, а не строкой.
Здесь received уже содержит полный пакет. Один вызов recv() в TCP
может вернуть часть пакета или несколько пакетов сразу: сначала накопите
нужные байты. bytespec разбирает буфер, а не управляет сокетом.
Прочитать несколько сообщений из одного буфера¶
В буфер попали два ответа предыдущего формата. Продолжаем с классом Reply:
buffer = received + received
first, offset = Reply.decode_from(buffer, 0)
print(first.data, offset)
second, offset = Reply.decode_from(buffer, offset)
print(second.data, offset)
assert offset == len(buffer)
b'OK' 7
b'OK' 14
Первый ответ занимает байты 0–6. Возвращённое смещение 7
указывает начало второго. После второго получаем 14 — конец буфера.
decode() принимает ровно одно сообщение; для нескольких нужен decode_from().
Правила работы с остатком буфера: Файлы и несколько сообщений.
Прочитать формат без заголовка bytespec¶
Датчик отправляет ровно два байта: температуру 23 и заряд 87. Добавлять служебные байты нельзя:
from bytespec import ProtoModel
from bytespec.types import UInt8
class SensorReading(ProtoModel):
__header__ = ()
temperature: UInt8
battery: UInt8
received = bytes.fromhex("17 57")
reading = SensorReading.decode(received)
print(reading.temperature, reading.battery)
assert reading.encode() == received
23 87
__header__ = () убирает все элементы заголовка. Первый байт сразу
принадлежит temperature, второй — battery. Для такого фиксированного
формата границы определяются размерами полей. Другие варианты: Header и framing.
Прочитать число в little-endian¶
Устройство хранит счётчик 258 в двух байтах 02 01, младший байт первым.
Укажем этот порядок байтов:
from bytespec import ByteOrder, ProtoModel
from bytespec.types import UInt16
class DeviceCounter(ProtoModel):
__header__ = ()
__byte_order__ = ByteOrder.LITTLE
value: UInt16
received = bytes.fromhex("02 01")
counter = DeviceCounter.decode(received)
print(counter.value)
assert counter.encode() == received
258
Число равно 2 + 1 * 256. При стандартном big-endian те же два байта
означали бы 513. Настройка действует на числа фиксированного размера и
префиксы этой модели; байты текста она не переставляет.
Сохранить собственный тип приложения¶
В приложении идентификатор представлен объектом UserId, а устройство
ожидает четыре байта целого числа. Codecs: свой формат и свои типы показывает полный пример:
класс UserId, его кодек и подключение через field(codec=...).
Кодек отвечает только за преобразование отдельного поля.
Собрать вложенное сообщение¶
Теперь соединим уже знакомые возможности: модель автора, список тегов и необязательную заметку. Этот пример можно выполнить целиком.
from bytespec import ProtoModel, field
from bytespec.types import UInt32
class User(ProtoModel):
id: UInt32
name: str
class Message(ProtoModel):
author: User
text: str
tags: list[str] = field(default_factory=list)
note: str | None = field(flag=0)
message = Message(
author=User(id=42, name="Anna"),
text="Hello!",
tags=["news"],
)
encoded = message.encode()
decoded = Message.decode(encoded)
print(decoded.author.name) # Anna
print(decoded.text) # Hello!
print(decoded.tags) # ['news']
print(decoded.note) # None
Создание и чтение остались такими же, как у первой модели. Список тегов
восстановился как список, автор — как User, а пропущенная заметка —
как None. Для этих структур не понадобились отдельные вызовы сериализации.
Изменим заметку и запишем сообщение ещё раз:
decoded.note = "Updated"
updated = Message.decode(decoded.encode())
print(updated.note) # Updated
Полезно попробовать этот пример с пустым tags и с note="":
оба значения будут сохранены. Только note=None убирает optional-поле
из сообщения.
Теперь можно попробовать модель на одном пакете своего протокола. Header и framing показывает, как настроить служебные байты; Проверка значений модели — как проверить связи между полями; Обработка ошибок — как обработать ошибки. Если нужно читать несколько сообщений из одного буфера, откройте Файлы и несколько сообщений.