TCP echo server · 0.0.26
Первый сетевой срез использует IPv4 127.0.0.1, блокирующие операции и одного
обслуживаемого клиента. Приложение написано на Liza; небольшой C runtime
соединяет его с POSIX sockets или Winsock. Проверка сетевого runtime и машинного
кода формальными экспериментами V0–V4 не заявляется.
Запуск
По умолчанию сервер слушает 127.0.0.1:9000 и последовательно обслуживает клиентов
до остановки процесса. Он выводит фактически выбранный порт и LF в stdout.
--ephemeral выбирает свободный порт ОС. --once тоже выбирает свободный порт,
но обслуживает только одного клиента и затем нормально закрывает listener.
На Windows исполняемый файл получает расширение .exe.
Клиент для отдельного терминала:
import socket
with socket.create_connection(("127.0.0.1", 9000), timeout=5) as stream:
payload = b"Liza\x00\xff"
stream.sendall(payload)
stream.shutdown(socket.SHUT_WR)
received = bytearray()
while True:
chunk = stream.recv(2048)
if not chunk:
break
received.extend(chunk)
assert bytes(received) == payload
TCP — поток байтов: границы вызовов send, read и сообщений не совпадают.
Приложение не интерпретирует UTF-8 и возвращает любые байты, включая NUL.
Типы и операции
net.Listener и net.Stream — разные непрозрачные move-only ресурсы.
Их нельзя создать из числа, скопировать или потерять на нормальном пути.
Они используют существующие ownership analysis и Result.
Отдельный Address пока не вводится: адрес всегда loopback, порт имеет тип u16.
Порт 0 означает выбор свободного порта ОС, а не преобразование адресной строки.
Все операции имеют эффект io. Он включает потенциально неограниченное
ожидание. Эффект block, deadlines, readiness, IPv6, DNS, TLS и конкурентность
в этот срез не входят. ОС может выделять память для сокетов; контракт отсутствия
аллокаций относится к прикладным буферам, а не ко всему сетевому стеку.
| Вызов | Результат | Владение |
|---|---|---|
net.listen(port) |
Result<net.Listener,NetError> |
Новый listener при успехе |
net.local_port(listener) |
Result<u16,NetError> |
Временное заимствование |
net.accept(listener) |
Result<net.Stream,NetError> |
Listener сохраняется; новый stream при успехе |
net.read(stream, move buffer) |
Линейное произведение буфера и Result<u32,NetError> |
Stream заимствуется, буфер возвращается безусловно |
net.write(stream, data, start, count) |
Result<u32,NetError> |
Stream и View<u8> заимствуются |
net.close_stream(move stream) |
Result<u32,NetError> |
Потребляет stream даже при ошибке |
net.close_listener(move listener) |
Result<u32,NetError> |
Потребляет listener даже при ошибке |
Заимствование сокета ограничено вызовом. Не требуется создавать новый дескриптор
сокета после каждого чтения или записи. Перемещение сокета между обычными
функциями по-прежнему требует move.
Чтение в существующее размещение
Буфер имеет тип memory.Buffer<u8>. Он уже полностью инициализирован обычным
memory.alloc<u8>. read пишет непосредственно в его размещение и сохраняет
его адрес, длину и зависимость от региона. Объём памяти не меняется.
Передать буфер при живом readonly view нельзя.
Ok(n), гдеn > 0: первыеnбайтов заменены принятыми данными; хвост сохраняет прежнее содержимое.Ok(0): peer завершил отправку, EOF. Это не требует немедленного закрытия направления отправки сервером.Err(error): данных этим вызовом не принято; буфер остаётся доступным.- Нулевая длина буфера возвращает
InvalidRange, чтобыOk(0)однозначно означал EOF.
Один вызов читает не более min(buffer.len, INT_MAX) байтов. Ошибки сети не
вызывают language trap. При наличии данных может быть возвращён короткий read.
Запись и частичный внешний результат
net.write(stream, data, start, count) отправляет диапазон readonly view.
Проверки start <= data.len и count <= data.len-start выполняются без
переполнения; нарушение возвращает InvalidRange. Пустой диапазон даёт Ok(0).
Для непустого диапазона успешный результат положителен и не превышает count.
Если системный send неожиданно возвращает ноль для непустого диапазона,
runtime возвращает Err(NetError.Disconnected), а не Ok(0). Поэтому каждая
успешная итерация цикла досылки строго увеличивает смещение; ошибка завершает
цикл. Это гарантия прогресса между завершёнными вызовами, не ограничение
времени ожидания внутри блокирующего send.
Выполняется один send, не более INT_MAX байтов. Ok(n) означает, что локальный
сетевой стек принял n байтов, не что приложение на другом конце их прочитало.
Остаток отправляет приложение. Err не отменяет данные предыдущих успешных
вызовов и не делает запись сообщения атомарной. Сигнал SIGPIPE подавляется
для сетевой записи, а разрыв отражается обычной ошибкой.
Все аргументы вычисляются до вызова, по существующим правилам Liza. Никакой особой ленивой оценки нового сетевого API нет.
Ошибки и закрытие
NetError: AddressInUse, PermissionDenied, Interrupted, WouldBlock,
Disconnected, InvalidRange, Other. Отображение кодов ОС может различаться;
Other сохраняет ожидаемую ошибку без обещания точной переносимости errno.
Interrupted и WouldBlock отделены от EOF. В первом echo-приложении они
завершают текущую операцию с ошибкой; скрытого retry-цикла нет.
На нормальном пути сокеты закрываются явно. close потребляет ресурс и при
ошибке: повторное закрытие того же дескриптора не допускается. Runtime не
повторяет POSIX close после EINTR, поскольку повторный вызов может затронуть
переиспользованный дескриптор. Ошибка закрытия не обещает универсального
состояния внешнего ресурса на всех ОС. Trap и внешнее завершение процесса не
обещают выполнения пользовательского cleanup.
На Windows каждому успешно созданному сокету соответствует собственная ссылка WSAStartup/WSACleanup. Ошибка создания или accept освобождает промежуточные ресурсы. Listener использует SO_EXCLUSIVEADDRUSE; SO_REUSEADDR не включается. См. контракт Winsock.
Жизненный цикл echo
mainсоздаёт listener и сообщает порт. Ошибка вывода предотвращаетaccept.acceptсоздаёт один stream. Listener остаётся владельцем слушающего сокета.serveсоздаёт регион на 2048 байтов и один инициализированный буфер.exchangeчитает в этот же буфер. Линейное извлечение возвращает владение буфером независимо от результата I/O.- При ненулевом read создаётся lexical readonly view. Цикл записи двигает смещение на фактически принятое число байтов.
- Scope view заканчивается до следующего
read. - При EOF или ошибке буфер освобождается логически, stream закрывается,
затем
serveзакрывает регион. Следующий клиент получает новый регион.
Память приложения ограничена одним 2048-байтовым буфером плюс дескрипторы
и фиксированные метаданные. Аллокации происходят на соединение, не на сообщение.
Одновременно обслуживается один stream. listen запрашивает backlog 1, но
фактическая очередь и память сетевого стека управляются ОС; это не строгий
лимит числа ожидающих TCP-соединений. Медленный клиент блокирует остальных.
Коды выхода: 0 — успешный --once; 2 — неверные аргументы; 3 — ошибка
сети, памяти или обязательного вывода/закрытия. При ошибке клиента постоянный
сервер сообщает её в stderr и продолжает принимать клиентов. Управляемое
завершение постоянного сервера по сигналу — отдельный будущий этап.
Проверки и граница результата
tests/test_net.py проверяет O0/O2 через настоящий loopback TCP: пустой поток,
бинарные данные больше буфера, half-close, reset и последовательных клиентов.
Подмена только границы ОС воспроизводит короткие записи, ошибки чтения,
записи и закрытия, проверяя количество и порядок закрываемых сокетов. Отдельная подмена send
в настоящем runtime проверяет нулевой, короткий и полный результат при O0/O2;
пустой диапазон не вызывает send. Reset остаётся обязательным тестом
на всех трёх платформах.
Статические отрицательные тесты проверяют copy, потерю ресурса, повторное
закрытие, эффекты, несовместимые типы сокетов, живой view и преждевременное
закрытие региона после read. Структурная проверка адаптера LLVM исключает
аллокацию и копирование содержимого буфера в этом адаптере; это не общий
сертификат стоимости произвольных сетевых программ.
Набор включён в Linux/macOS compiler tests и Windows acceptance. Завершённость переносимой версии подтверждается результатами CI конкретного коммита. Verification Bundle развивается отдельно; сетевые операции не входят в область доказательств V0–V4.