From 1611d1c70b10a3d7c67022378c342177d9b5fe48 Mon Sep 17 00:00:00 2001 From: Stanislav N Mikhailov Date: Sat, 18 Jul 2026 17:22:53 +0300 Subject: [PATCH] added protocol.MD --- src/main.rs | 4 +- src/protocol.MD | 509 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 511 insertions(+), 2 deletions(-) create mode 100644 src/protocol.MD diff --git a/src/main.rs b/src/main.rs index a1f85df..41669ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -mod connection; -mod protocol; +mod connection; //модуль отвечающий за соединения +mod protocol; //модуль отвечающий за протокол use tokio::net::{TcpListener}; use tokio::io; diff --git a/src/protocol.MD b/src/protocol.MD new file mode 100644 index 0000000..7fe98dd --- /dev/null +++ b/src/protocol.MD @@ -0,0 +1,509 @@ +# Protocol.MD NoCloud Protocol 0.1 +## Задачи +1. Читать поля сообщения +2. Проверить их формат +2. Вернуть структуру Header c понятными полями заголовка: + a. version: u8 //Версия протокола + b. command: u8 //Тип сообщения + 0x01 UPLOAD клиент → сервер + 0x02 DOWNLOAD клиент → сервер + 0x03 DELETE клиент → сервер + 0x04 STAT клиент → сервер + 0x80 RESPONSE в обе стороны + 0x81 FILE_DATA сервер → клиент + c. flags: u16 //Дополнительные признаки + 0x0001 — разрешено перезаписать существующий файл + 0x0002 — передаётся контрольная сумма + 0x0004 — передача является возобновлением (докачка) + 0x0008 - 0x00FF - резерв + d. request_id:u32 //Номер запроса + c. body_size: u64 Размер тела после заголовка +3. И, если операция касается передячи файла - дополнительными полями следующими сразу за телом заголовка + a. file_size: u64 + b. name_size u16 + c. file_name UTF-8 + d. file_data u8 + +## Обрабатываемые запросы + +Я бы строил протокол в два этажа: + +1. Общий заголовок сообщения — позволяет понять, что за команда пришла. +2. Тело команды — устроено по-разному для загрузки, скачивания, ответа и т. д. + +Назовём черновик `NoCloud Protocol 0.1`. Он пока не обязан быть окончательным — наша задача сначала проверить логику. + +## 1. Правила транспортного уровня + +* Протокол работает поверх одного TCP-соединения. +* Позже TCP будет обёрнут в TLS/mTLS, но формат сообщений не изменится. +* Все целые числа передаются в big-endian. +* Один запрос обрабатывается целиком, затем начинается следующий. +* Файлы читаются и записываются порциями, но прикладных «блоков файла» пока нет. +* Клиент не отправляет следующий запрос, пока не получил ответ на предыдущий. + +Последнее ограничение сильно упрощает первую реализацию. `request_id` мы всё равно предусмотрим, чтобы позже разрешить несколько запросов в одном соединении. + +--- + +# 2. Общий заголовок сообщения + +Каждое сообщение начинается с одинаковых 20 байт: + +| Поле | Тип | Размер | Назначение | +| ------------ | --------: | -----: | --------------------------- | +| `magic` | `[u8; 4]` | 4 | Сигнатура `NCLD` | +| `version` | `u8` | 1 | Версия протокола | +| `command` | `u8` | 1 | Тип сообщения | +| `flags` | `u16` | 2 | Дополнительные признаки | +| `request_id` | `u32` | 4 | Номер запроса | +| `body_size` | `u64` | 8 | Размер тела после заголовка | + +Итого: + +```text +4 + 1 + 1 + 2 + 4 + 8 = 20 байт +``` + +В сетевом потоке: + +```text +┌────────┬─────────┬─────────┬───────┬────────────┬───────────┐ +│ magic │ version │ command │ flags │ request_id │ body_size │ +│ 4 байта│ 1 байт │ 1 байт │ 2 байта│ 4 байта │ 8 байт │ +└────────┴─────────┴─────────┴───────┴────────────┴───────────┘ +``` + +## Зачем нужны эти поля + +### `magic` + +```text +NCLD +``` + +В байтах: + +```text +4E 43 4C 44 +``` + +Позволяет серверу понять: + +> Передо мной действительно сообщение NoCloud, а не HTTP-запрос, мусор или поток со смещённой границей. + +Если первые четыре байта не `NCLD`, соединение закрывается. + +### `version` + +Первоначально: + +```text +version = 1 +``` + +Если через год изменим формат протокола, сервер сможет отличать старых клиентов от новых. + +### `command` + +Определяет, как интерпретировать тело сообщения. + +Первоначальный набор: + +| Код | Название | Направление | +| -----: | ----------- | --------------- | +| `0x01` | `UPLOAD` | клиент → сервер | +| `0x02` | `DOWNLOAD` | клиент → сервер | +| `0x03` | `DELETE` | клиент → сервер | +| `0x04` | `STAT` | клиент → сервер | +| `0x80` | `RESPONSE` | в обе стороны | +| `0x81` | `FILE_DATA` | сервер → клиент | + +На первом практическом этапе реализуем только: + +```text +UPLOAD +RESPONSE +``` + +Остальные пока просто резервируем. + +### `flags` + +В первой версии: + +```text +flags = 0 +``` + +Позже сюда можно поместить признаки: + +```text +0x0001 — разрешено перезаписать существующий файл +0x0002 — передаётся контрольная сумма +0x0004 — передача является возобновлением +``` + +Если все флаги равны нулю, они нам сейчас не мешают, но формат заголовка не придётся ломать позднее. + +### `request_id` + +Клиент назначает каждому запросу номер: + +```text +UPLOAD request_id = 17 +RESPONSE request_id = 17 +``` + +Благодаря этому клиент понимает, на какой запрос ответил сервер. + +Поначалу запросы будут строго последовательными, поэтому поле кажется избыточным. Но стоит оно всего четыре байта, а пригодится почти наверняка. + +### `body_size` + +Показывает, сколько байт идёт после общего заголовка. + +Это позволяет: + +* проверить допустимость размера; +* знать границу следующего сообщения; +* отбрасывать неизвестную команду; +* обнаруживать оборванную передачу. + +--- + +# 3. Команда `UPLOAD` + +После общего заголовка идёт тело загрузки: + +| Поле | Тип | Размер | +| ----------- | ----: | ---------------: | +| `file_size` | `u64` | 8 байт | +| `name_size` | `u16` | 2 байта | +| `file_name` | UTF-8 | `name_size` байт | +| `file_data` | байты | `file_size` байт | + +В потоке: + +```text +Общий заголовок, 20 байт +┌───────────┬───────────┬────────────┬─────────────┐ +│ file_size │ name_size │ file_name │ file_data │ +│ 8 байт │ 2 байта │ N байт │ M байт │ +└───────────┴───────────┴────────────┴─────────────┘ +``` + +Размер тела должен быть равен: + +```text +body_size = 8 + 2 + name_size + file_size +``` + +Например, клиент отправляет файл: + +```text +hello.txt +``` + +размером 1000 байт. Имя занимает 9 байт в UTF-8: + +```text +file_size = 1000 +name_size = 9 +body_size = 8 + 2 + 9 + 1000 = 1019 +``` + +## Как сервер это обрабатывает + +Сервер не создаёт структуру, содержащую весь `file_data`. Он действует последовательно: + +```text +прочитать общий заголовок + ↓ +убедиться, что command = UPLOAD + ↓ +прочитать file_size + ↓ +прочитать name_size + ↓ +проверить name_size + ↓ +прочитать имя + ↓ +проверить имя файла + ↓ +создать временный файл + ↓ +прочитать file_size байт порциями + ↓ +проверить успешное завершение + ↓ +переименовать временный файл в итоговый + ↓ +отправить RESPONSE +``` + +Буфер сервера может быть, например, 64 КиБ: + +```text +файл размером 10 ГБ + ↓ +прочитали до 64 КиБ + ↓ +записали на диск + ↓ +прочитали следующую порцию +``` + +Оперативная память не зависит от размера файла. + +## Почему сначала временный файл + +Допустим, загружается: + +```text +report.pdf +``` + +Сервер сначала создаёт что-то вроде: + +```text +report.pdf.nocloud-part-17 +``` + +Если клиент оборвал передачу на 70%, мы не получим повреждённый `report.pdf`, выглядящий как готовый файл. + +После успешной передачи: + +```text +report.pdf.nocloud-part-17 + ↓ rename +report.pdf +``` + +--- + +# 4. Ответ `RESPONSE` + +Сервер отвечает на запрос сообщением с тем же `request_id`. + +Тело ответа: + +| Поле | Тип | Размер | +| -------------- | ----: | ------------------: | +| `status` | `u16` | 2 байта | +| `message_size` | `u16` | 2 байта | +| `message` | UTF-8 | `message_size` байт | + +Например: + +```text +command = RESPONSE +request_id = 17 +body_size = 6 + +status = 0 +message_size = 2 +message = "OK" +``` + +Первоначальные статусы: + +| Код | Значение | +| --: | ----------------------- | +| `0` | Успех | +| `1` | Неизвестная команда | +| `2` | Некорректный заголовок | +| `3` | Недопустимое имя файла | +| `4` | Файл уже существует | +| `5` | Ошибка файловой системы | +| `6` | Не хватает места | +| `7` | Передача оборвана | +| `8` | Нет доступа | +| `9` | Неподдерживаемая версия | + +Текст `message` предназначен для человека и журналов: + +```text +"file already exists" +``` + +Программа принимает решение по числовому `status`, а не сравнивает строки. + +--- + +# 5. Команда `DOWNLOAD` + +Клиент запрашивает файл. + +Тело запроса: + +| Поле | Тип | Размер | +| ----------- | ----: | ---------------: | +| `name_size` | `u16` | 2 байта | +| `file_name` | UTF-8 | `name_size` байт | + +```text +body_size = 2 + name_size +``` + +Если файл не существует, сервер возвращает обычный `RESPONSE` с ошибкой. + +Если существует, сервер отвечает `FILE_DATA`. + +--- + +# 6. Ответ `FILE_DATA` + +Тело ответа: + +| Поле | Тип | Размер | +| ----------- | ----: | ---------------: | +| `file_size` | `u64` | 8 байт | +| `name_size` | `u16` | 2 байта | +| `file_name` | UTF-8 | `name_size` байт | +| `file_data` | байты | `file_size` байт | + +То есть загрузка и скачивание используют почти одинаковое представление файла. Отличается направление и код команды: + +```text +UPLOAD: +клиент → сервер + +FILE_DATA: +сервер → клиент +``` + +После успешного получения файла клиент может отправить серверу `RESPONSE` с тем же `request_id`. + +--- + +# 7. Ограничения, которые сервер обязан проверять + +Данным клиента доверять нельзя, даже когда позже появится mTLS. Известный клиент тоже может содержать ошибку. + +Я бы установил такие стартовые ограничения: + +```text +максимальная длина имени: 1024 байта +максимальный текст ошибки: 4096 байт +максимальный размер файла: задаётся настройкой сервера +version должна быть равна 1 +неизвестные flags запрещены +``` + +Имя файла не должно: + +* быть пустым; +* начинаться с `/` или `\`; +* содержать `..`; +* содержать нулевой байт; +* превращаться в абсолютный путь; +* позволять выйти из каталога хранилища. + +Иначе клиент сможет прислать: + +```text +../../etc/passwd +``` + +и попытаться записать файл за пределами хранилища. + +--- + +# 8. Контрольная сумма + +Она нужна, но я бы не добавлял её в самый первый эксперимент. + +TCP уже гарантирует, что доставленные байты не были незаметно переставлены или повреждены в пути. Хеш нужен на другом уровне: + +* проверить файл целиком; +* идентифицировать одинаковые файлы; +* обнаружить ошибку хранения; +* в будущем возобновлять и дедуплицировать передачи. + +Во второй итерации можно добавить флаг: + +```text +flags & 0x0002 != 0 +``` + +и после имени передавать: + +```text +hash_algorithm: u8 +hash_size: u8 +hash: [u8; hash_size] +``` + +Например SHA-256: + +```text +hash_algorithm = 1 +hash_size = 32 +hash = 32 байта +``` + +Но сначала надо добиться безошибочной передачи обычного файла. + +--- + +# 9. Как выглядит полная загрузка + +```text +Клиент + │ + │ Общий заголовок: + │ command = UPLOAD + │ request_id = 17 + │ body_size = ... + │ + │ file_size + │ name_size + │ file_name + │ file_data + ▼ +Сервер + │ + │ проверяет заголовок + │ создаёт временный файл + │ принимает данные порциями + │ переименовывает готовый файл + │ + │ RESPONSE: + │ request_id = 17 + │ status = 0 + ▼ +Клиент +``` + +Если передача оборвалась посередине, ответ уже отправить некому. Сервер удаляет или сохраняет временный файл для будущего возобновления — это мы решим отдельно. + +--- + +# 10. Что сознательно не входит в версию 0.1 + +Пока отсутствуют: + +* авторизация внутри протокола; +* каталоги и списки файлов; +* синхронизация изменений; +* время модификации; +* идентификатор файла; +* контрольная сумма; +* передача отдельных блоков; +* параллельные запросы; +* возобновление загрузки; +* разрешение конфликтов; +* сжатие. + +Первая реализация задумана только для: + +> Клиент отправляет один файл, сервер безопасно сохраняет его и возвращает подтверждение. + +После этого поверх уже работающей передачи буду строить настоящую синхронизацию. + + + + +