TCP message server · срез 0.0.27
Следующий прикладной срез после TCP echo добавляет границы сообщений. Соединения по-прежнему блокирующие, обслуживается один клиент за раз. Новый синтаксис, runtime-примитивы, reactor и маршрутизация между клиентами не нужны.
Сервер возвращает каждое полностью принятое сообщение в том же формате:
| Поле | Формат |
|---|---|
| Длина | Ровно 4 байта, unsigned u32, big-endian |
| Тело | Ровно N произвольных байтов |
Длина не включает заголовок. В приложении 0 ≤ N ≤ 65536. Пустое сообщение допустимо и получает ответ из четырёх нулевых байтов. UTF-8, команды и JSON не подразумеваются. Протокол не имеет negotiation, authentication или TLS.
Запуск и клиент
python -m liza build examples/apps/liza_message.liza -O 2 -o build/liza-message
./build/liza-message
По умолчанию адрес — 127.0.0.1:9001. --ephemeral выбирает свободный порт,
--once выбирает свободный порт и нормально завершает сервер после одного
клиента. stdout содержит выбранный порт и LF. На Windows используется .exe.
Telnet не формирует этот бинарный заголовок; для проверки нужен framed client:
import socket
import struct
def receive_exact(stream, length):
result = bytearray()
while len(result) < length:
part = stream.recv(length - len(result))
if not part:
raise EOFError("Incomplete frame")
result.extend(part)
return bytes(result)
with socket.create_connection(("127.0.0.1", 9001), timeout=5) as stream:
payload = b"Hello Liza\x00\xff"
stream.sendall(struct.pack("!I", len(payload)) + payload)
length = struct.unpack("!I", receive_exact(stream, 4))[0]
assert length <= 65536
assert receive_exact(stream, length) == payload
stream.shutdown(socket.SHUT_WR)
Клиент должен читать ответы по мере отправки сообщений: бесконечная отправка без чтения может заполнить TCP-буферы и заблокировать обе стороны.
Библиотека framing
liza/stdlib/framing.liza — чистый код Liza. Парсер не выделяет память,
не копирует payload, не хранит view и не зависит от времени жизни входного
буфера. Он возвращает индексы сегмента payload, который приложение должно
обработать до переиспользования входных байтов.
| Операция | Контракт |
|---|---|
framing.create(limit) |
Независимый FrameState, готовый к заголовку |
framing.feed(state, data, start, count) |
Result<FrameStep,FrameError> для диапазона View<u8> |
framing.finish(state) |
Ok(0) только на границе кадров; иначе ошибка усечённого входа |
framing.header(length) |
Четыре байта big-endian для любого u32 |
FrameState — копируемое значение с четырьмя u32: лимит, накопленная длина,
число байтов заголовка и число принятых байтов тела. Поля приватны. feed
возвращает новую версию состояния; исходная версия остаётся независимой.
Лимит библиотеки может быть любым u32, но сервер фиксирует 64 КиБ.
FrameStep содержит:
| Поле | Смысл |
|---|---|
state |
Следующее состояние; после завершения кадра готово к следующему заголовку |
next |
Абсолютный индекс первого ещё не обработанного байта текущего view |
payload_start, payload_count |
Доступный сегмент тела в текущем view |
body_offset |
Позиция этого сегмента в теле сообщения |
length |
Длина тела после полного заголовка; до его завершения равна нулю |
complete |
Полностью получен один кадр, включая случай N = 0 |
Один feed завершается не позже конца текущего кадра. Если next меньше конца
входного диапазона, вызывающий код продолжает с возвращённым состоянием и
оставшимися байтами. Поэтому за одно чтение могут завершиться несколько кадров.
Для непустого допустимого входа успешный вызов всегда продвигает next.
Пустой диапазон не создаёт кадр и сохраняет состояние. Проверки диапазона
предшествуют чтению и используют count <= data.len-start, исключая
переполнение суммы индексов. За один вызов читается не более четырёх байтов
заголовка; payload описывается диапазоном без сканирования его содержимого.
FrameError:
InvalidRange— передан недопустимый диапазон view.TooLarge— полный заголовок объявляет длину больше лимита.TruncatedHeader— EOF после 1–3 байтов нового заголовка.TruncatedBody— EOF после полного заголовка, до завершения тела.
Ошибка — Result, не trap. При Err новая версия состояния не возвращается;
сервер завершает соединение, не пытаясь найти следующий заголовок в недостоверном
потоке. Независимый вызывающий код может начать новый разбор через create.
Сборка сообщения и стоимость
На соединение создаётся один регион ёмкостью 67584 байта: 2048 для чтения и
65536 для тела. Оба буфера инициализируются один раз. Они переиспользуются до
закрытия соединения; per-message аллокаций и reserve нет. Служебные значения
и заголовок имеют фиксированный размер. Память сетевого стека ОС — отдельно.
Сегмент тела копируется из входного буфера в соответствующий диапазон буфера сообщения. Всего это N байтов на сообщение; весь буфер не копируется на каждом чтении. Передача тела по сети использует readonly view и точную длину N. Байты за пределами N не отправляются, в том числе после более длинного сообщения. Это не zero-copy: хранение полного кадра нужно, чтобы не выдавать ответ на усечённое сообщение.
До полного кадра ответ не начинается. Затем отправляются заголовок и тело,
каждый с циклом досылки. Успешный net.write непустого диапазона гарантирует
положительный прогресс; I/O-ошибка завершает соединение. Ошибка отправки уже
полного кадра может оставить у клиента частичный ответ — сетевой вывод не
атомарен и не откатывается.
EOF на границе кадров — нормальное завершение. При ошибке последующего кадра предыдущие ответы не отменяются; это не гарантия их доставки приложению peer. Сервер не обещает сохранить соединение после ошибки протокола.
Исходник
| examples/apps/liza_message.liza | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
exchange передаёт владение входным буфером в net.read и получает его обратно
через линейное произведение. View входа живёт только в обработке одного чтения.
View тела живёт только при отправке полного кадра; до следующего memory.set
его scope завершается. На нормальном выходе оба буфера освобождаются логически,
stream закрывается, затем закрывается регион. Listener остаётся в main.
Ошибки приложения и ограничения
Коды завершения в --once: 0 — чистый EOF на границе кадров; 2 — неверные
аргументы; 3 — I/O, выделение памяти или закрытие; 4 — некорректный кадр.
Ошибка закрытия имеет приоритет над протокольной ошибкой. В stderr выводится
Invalid frame либо Connection I/O failed. Постоянный сервер после ошибки
клиента принимает следующего; ошибка обязательного вывода или accept завершает
цикл приёма. Все сообщения диагностики заканчиваются LF.
Один медленный клиент блокирует остальных. Тайм-ауты, readiness, конкурентность, управляемая остановка по сигналу и строгая квота backlog отсутствуют. Следующий этап уже реализован отдельно — неблокирующий reactor. Message Router добавляет адресную маршрутизацию поверх него. Сам blocking-пример сохранён без изменений. Verification Bundle развивается независимо.
Проверки
tests/test_framing.py проверяет библиотеку и приложение при O0/O2:
- Все 16384 разбиения 15-байтового потока из трёх сообщений, включая пустое и бинарное; точные длины, смещения, полезные байты и прогресс курсора.
- Пустые чанки, приватность и независимость состояния, диапазоны
u32, big-endian кодирование и ошибки завершения заголовка/тела. - Реальный loopback: по одному байту, несколько кадров одним потоком,
сообщение ровно 65536 байтов, отклонение 65537 и
0xFFFFFFFF. - EOF на каждом префиксе кадра, отсутствие преждевременного ответа, следующий клиент после ошибки предыдущего.
- Подмена границы ОС: запись по одному байту, ошибки read/write/close, усечённый заголовок; проверяются точные отправленные байты и однократное закрытие stream и listener.
Набор включён в Linux/macOS compiler tests и Windows acceptance. Это тестовая проверка выбранных сценариев, не формальное доказательство для всех потоков. Парсер, сетевой runtime и машинный код не добавляются к области V0–V4.