added protocol.MD

This commit is contained in:
Stanislav N Mikhailov
2026-07-18 17:22:53 +03:00
parent e4e0bf33f0
commit 1611d1c70b
2 changed files with 511 additions and 2 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
mod connection; mod connection; //модуль отвечающий за соединения
mod protocol; mod protocol; //модуль отвечающий за протокол
use tokio::net::{TcpListener}; use tokio::net::{TcpListener};
use tokio::io; use tokio::io;
+509
View File
@@ -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
Пока отсутствуют:
* авторизация внутри протокола;
* каталоги и списки файлов;
* синхронизация изменений;
* время модификации;
* идентификатор файла;
* контрольная сумма;
* передача отдельных блоков;
* параллельные запросы;
* возобновление загрузки;
* разрешение конфликтов;
* сжатие.
Первая реализация задумана только для:
> Клиент отправляет один файл, сервер безопасно сохраняет его и возвращает подтверждение.
После этого поверх уже работающей передачи буду строить настоящую синхронизацию.