added protocol.MD
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
||||
mod connection;
|
||||
mod protocol;
|
||||
mod connection; //модуль отвечающий за соединения
|
||||
mod protocol; //модуль отвечающий за протокол
|
||||
|
||||
use tokio::net::{TcpListener};
|
||||
use tokio::io;
|
||||
|
||||
+509
@@ -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
|
||||
|
||||
Пока отсутствуют:
|
||||
|
||||
* авторизация внутри протокола;
|
||||
* каталоги и списки файлов;
|
||||
* синхронизация изменений;
|
||||
* время модификации;
|
||||
* идентификатор файла;
|
||||
* контрольная сумма;
|
||||
* передача отдельных блоков;
|
||||
* параллельные запросы;
|
||||
* возобновление загрузки;
|
||||
* разрешение конфликтов;
|
||||
* сжатие.
|
||||
|
||||
Первая реализация задумана только для:
|
||||
|
||||
> Клиент отправляет один файл, сервер безопасно сохраняет его и возвращает подтверждение.
|
||||
|
||||
После этого поверх уже работающей передачи буду строить настоящую синхронизацию.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user