Компьютерные сети: от IPC к C/Winsock
ByteBox/1: собрать курс в один проверяемый протокол
Финальная C/Winsock2 практика: точный 20-byte header, bounded binary payload, CRC, request correlation, concurrent clients и black-box harness.
ByteBox/1: собрать курс в один проверяемый протокол
За процессом A весь курс путешествовало Привет: шесть символов и 12 UTF-8
bytes. Теперь вы проектируете границу сами. ByteBox/1 не добавляет это слово в
каталог: interoperability требует ровно четыре frozen объекта.
Задача
Результат — C11 implementation под Windows: TCP server, command-line client,
encoder/decoder, CRC-32, tests и evidence. Server предоставляет read-only
операции LIST и GET. Он не читает arbitrary files и не поддерживает upload,
authentication, compression, TLS или custom crypto.
Каждый request и response начинается с header ровно 20 bytes. Его единственная нормативная compact notation:
struct.Struct("!4sBBHIII")
! означает big-endian network byte order без native padding.
| Offset | Size | Field | ByteBox/1 contract |
|---|---|---|---|
| 0 | 4 | magic | ASCII BTBX, bytes 42 54 42 58 |
| 4 | 1 | version | 1 |
| 5 | 1 | opcode | одно из нормативных значений ниже |
| 6 | 2 | status | big-endian; 0 в request и success response |
| 8 | 4 | request_id | big-endian; request 1..0xffffffff, response echo |
| 12 | 4 | payload_len | big-endian, диапазон 0..65536 |
| 16 | 4 | payload_crc32 | big-endian CRC payload; 0 для empty payload |
Frozen wire contract
Opcodes и направления фиксированы:
| Message | Opcode |
|---|---|
LIST request | 0x01 |
GET request | 0x02 |
LIST_RESPONSE | 0x81 |
GET_RESPONSE | 0x82 |
ERROR response | 0xff |
Success response использует matching response opcode и status=0. ERROR
использует nonzero status, echo исходного request_id, payload_len=0,
payload_crc32=0 и не содержит body.
| Status | Value | Meaning |
|---|---|---|
BAD_REQUEST | 1 | zero ID, nonzero request status или malformed known-opcode payload |
NOT_FOUND | 2 | valid name отсутствует в fixed catalog |
UNSUPPORTED_VERSION | 3 | magic верный, version не 1 |
BAD_OPCODE | 4 | unknown или response opcode пришёл как client request |
FRAME_TOO_LARGE | 5 | declared payload_len > 65536 |
BAD_CHECKSUM | 6 | CRC bounded payload не совпал |
INTERNAL_ERROR | 7 | valid request нельзя выполнить по внутренней причине |
CRC — CRC-32/ISO-HDLC только от payload, без header: poly=0x04c11db7,
init=0xffffffff, reflected input/output, xorout=0xffffffff.
CRC("123456789")=0xcbf43926, CRC empty payload равен 0. Это механизм
controlled integrity/conformance, не authentication и не encryption.
Две операции и ровно четыре объекта
Empty LIST request возвращает records, отсортированные по raw ASCII name.
LIST payload начинается с u16 item_count, затем для каждого item идут
u16 name_len, name bytes, u32 content_len, u32 content_crc32. Для
нормативного каталога item_count=4, а payload занимает 82 bytes. Parser обязан
потребить ровно payload_len: trailing bytes — protocol error.
| Name | Exact content | Length | CRC-32 |
|---|---|---|---|
empty.bin | empty bytes | 0 | 0x00000000 |
hello.txt | ASCII Hello from ByteBox!\n | 20 | 0x848664cd |
numbers.txt | ASCII lines 0\n through 9\n | 20 | 0x560e61b9 |
pattern.bin | byte at offset i is i % 251 | 65,536 | 0x7faa50d3 |
GET request payload — u16 name_len и ровно name_len ASCII bytes. Length
имени от 1 до 64; regex — ^[a-z0-9][a-z0-9._-]{0,63}$; NUL terminator и
trailing fields запрещены. Malformed schema даёт BAD_REQUEST, а корректное
missing.bin — NOT_FOUND. Success GET_RESPONSE содержит raw object bytes;
имя и вторая длина в body не повторяются.
Validation order — часть безопасности
Порядок нельзя переставить ради удобства. Server сначала читает ровно 20 header
bytes или обнаруживает EOF, затем проверяет magic, декодирует fields, проверяет
version и только потом сравнивает payload_len с 65536. Storage создаётся
лишь после bound check. Далее server читает ровно bounded body, проверяет CRC,
request ID/status/opcode/schema, выполняет operation и отправляет весь response
через partial-write loop.
Этот порядок задаёт observable outcomes:
- wrong magic — немедленный close без response;
- unsupported version —
ERROR/3, затем close, declared body не читается; - length
65537—ERROR/5, затем close, body не читается и allocation по недоверенной длине не происходит; - CRC mismatch —
ERROR/6, затем close; - clean EOF между frames — спокойный close без response;
- EOF внутри header/body — close без выполнения request;
- valid request — matching response и connection остаётся открытым.
Malformed known request, bad opcode и not found получают соответственно status
1, 4, 2; после response server MAY продолжить или закрыть connection.
Malformed client не должен завершить listening process.
C/Winsock checkpoint
Начните decoder с byte array и явных offsets:
#define BYTEBOX_HEADER_SIZE 20u
#define BYTEBOX_MAX_PAYLOAD 65536u
uint8_t raw[BYTEBOX_HEADER_SIZE];
enum recv_exact_result rr = recv_exact(s, raw, sizeof(raw));
if (rr == RECV_CLEAN_EOF) {
close_connection(s);
return;
}
if (rr != RECV_OK || memcmp(raw, "BTBX", 4) != 0) {
close_connection(s);
return;
}
uint8_t version = raw[4];
uint8_t opcode = raw[5];
uint16_t status_net;
uint32_t request_id_net, payload_len_net, crc_net;
memcpy(&status_net, raw + 6, sizeof(status_net));
memcpy(&request_id_net, raw + 8, sizeof(request_id_net));
memcpy(&payload_len_net, raw + 12, sizeof(payload_len_net));
memcpy(&crc_net, raw + 16, sizeof(crc_net));
uint16_t status = ntohs(status_net);
uint32_t request_id = ntohl(request_id_net);
uint32_t payload_len = ntohl(payload_len_net);
uint32_t expected_crc = ntohl(crc_net);
После decode сначала обработайте version != 1, затем
payload_len > BYTEBOX_MAX_PAYLOAD. Только после этого читайте/выделяйте body.
Binary payload нельзя измерять strlen или печатать через %s.
recv_exact различает три outcomes: все requested bytes; clean EOF до первого
byte нового frame; truncation/error после начала frame. Один positive recv
может вернуть меньше requested bytes. recv == 0 — EOF, не «пустой packet».
send_all повторяет send, потому что successful send вправе принять меньше
bytes, чем запросил caller.
Для server baseline возможны две модели из главы «Один сервер — много
клиентов». Сильный вариант — один event-loop thread на select() или
WSAPoll() и отдельное state каждого connection: reading header, reading
bounded body, validating, writing response. Минимальный вариант —
thread-per-connection с ограниченным пулом: каждый client обслуживается
линейным blocking кодом, а лимит пула и CAPACITY_REJECT сверх него играют
роль resource bound. На обеих моделях partial request клиента A не должен
остановить client B: в event loop это дают non-blocking sockets и
per-connection state, в пуле — то, что blocked recv_exact занимает только
поток своего клиента. Ограничьте MAX_CLIENTS (или размер пула) и
per-connection queued output; при заполнении output queue приостановите чтение
новых requests этого peer или закройте slow connection по documented policy.
Concurrency — observable contract
Выбор модели (event loop или bounded thread pool) не меняет contract: harness проверяет наблюдаемое поведение, а не архитектуру. Server обязан обслуживать не менее 16 одновременных connections на classroom reference image. Harness проверяет не размер accept queue, а progress: client A держит partial valid GET, client B отправляет complete GET и должен получить response до того, как A дошлёт остаток.
Load scenario открывает 16 clients; каждый выполняет не менее 25 alternating LIST/GET exchanges с distinct request IDs и fragmented writes. Cross-connection payload или ID mix-up — failure.
Slow-reader scenario отправляет на одном connection 64 valid GET requests для
pattern.bin и не читает responses. После этого восемь normal clients делают
LIST и GET hello.txt; они должны завершиться за 5 seconds. Server может
throttle/close slow peer, но не может расти без bound, упасть или остановить
остальных.
Предсказание → harness → evidence
До первого полного запуска запишите outcomes ключевых fault cases:
| Input | Prediction |
|---|---|
| GET по одному byte header/body | один корректный GET_RESPONSE, тот же ID |
| LIST+GET одним write | два ordered correlated responses |
| wrong magic | quiet close, затем fresh LIST к server успешен |
version 2 без declared body | empty ERROR/3, close без чтения body |
declared length 65537 | empty ERROR/5, close до allocation/body |
| bounded body с bad CRC | empty ERROR/6, close; listening server жив |
valid missing.bin | empty ERROR/2 с echoed ID |
Запуск полного black-box набора из repository root:
python3 -B docs/curriculum/computer-networks/harness/bytebox_harness.py \
--host 127.0.0.1 \
--port 9090
Harness имеет ровно 14 default scenarios: valid-exchange,
fragmented-request, coalesced-frames, wrong-magic,
unsupported-version, bad-request, bad-opcode, crc-mismatch,
oversize-declaration, mid-frame-disconnect, unknown-file,
concurrent-clients, concurrent-load, slow-reader. Exit code 0 означает,
что все выбранные scenarios прошли; 1 — conformance failure.
Сохраните transcript, harness version/seed, commit, exact commands, compiler/Windows SDK и exit code. Green server harness не доказывает student client: BBX-15 отдельно проверяет обе interoperability directions.
Частая ошибка: happy path принимают за protocol
Server, который один раз вернул hello.txt, ещё не решил TCP framing. Harness
специально разбивает frame по одному byte и объединяет два frames в одном write.
Если код предполагает «один send — один recv», оба case ломают parser.
Вторая ошибка — «улучшить» frozen contract: добавить JSON mode, пятый object, upload, TLS field или новый mandatory opcode. Это не bonus, а потеря interoperability. ByteBox/1 меняется только новой version и синхронным обновлением spec, reference implementations, harness и grading.
Итого и следующий шаг
- ByteBox/1 — exact wire contract:
BTBX, version1, 20-byte!4sBBHIIIheader и payload не больше 65,536 bytes. - Parser сначала проверяет immutable header facts и bound, затем читает bounded body, проверяет CRC и application schema.
- TCP stream требует incremental state, partial I/O loops и явные EOF semantics.
- Concurrency считается по progress normal clients при partial/slow peers, а не по числу accepted sockets.
- Submission — это implementation плюс воспроизводимое evidence; рассказ не заменяет harness behavior.
После harness повторите диагностику на своём server: свяжите request ID, socket endpoints, fault scenario и response.
Нормативные источники
docs/curriculum/computer-networks/capstone-bytebox.md— единственный source of truth ByteBox/1docs/curriculum/computer-networks/harness/README.md— команды и exact harness scenarios- RFC 9293 — Transmission Control Protocol
- Microsoft Learn — recv function
- Microsoft Learn — send function
- Microsoft Learn — WSAPoll function
- Microsoft Learn — Getting started with Winsock