feat(protocol): add initial parser for common message header

This commit is contained in:
Stanislav N Mikhailov
2026-07-18 21:58:32 +03:00
parent 359337bc10
commit 9f2d7f635e
5 changed files with 223 additions and 38 deletions
Generated
+21
View File
@@ -208,6 +208,7 @@ name = "nocloud-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"local-ip-address", "local-ip-address",
"thiserror",
"tokio", "tokio",
] ]
@@ -346,6 +347,26 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.3" version = "1.52.3"
+1
View File
@@ -13,3 +13,4 @@ panic = "abort"
[dependencies] [dependencies]
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
local-ip-address = "0.6" local-ip-address = "0.6"
thiserror = "2.0"
+60 -35
View File
@@ -4,6 +4,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpStream}; use tokio::net::{TcpStream};
use std::io; use std::io;
use crate::protocol::{parce_header, ParseError};
pub struct Connection { pub struct Connection {
id: u64, id: u64,
client_addr: SocketAddr, client_addr: SocketAddr,
@@ -25,48 +26,72 @@ impl Connection {
} }
} }
pub async fn run(mut self) -> io::Result<()> { pub async fn run(mut self) -> io::Result<()> {
println!( println!(
"Обработчик {} создан для {}", "Обработчик {} создан для {}",
self.id, self.id, self.client_addr
self.client_addr );
);
let mut buffer = [0_u8; 1024]; // Буфер для данных тела сообщения (теперь он не хранит заголовок)
let mut body_buffer = vec![0_u8; 1024];
loop { loop {
let bytes_read = self.stream.read(&mut buffer).await?; // 1. Создаем буфер СТРОГО под размер заголовка
let mut header_buffer = [0_u8; 20];
if bytes_read == 0 { // 2. Читаем из сети РОВНО 20 байт
// Если клиент закроет соединение до того, как пришлет 20 байт,
// read_exact вернет ошибку UnexpectedEof.
if let Err(e) = self.stream.read_exact(&mut header_buffer).await {
if e.kind() == io::ErrorKind::UnexpectedEof {
println!( println!(
"Обработчик {}: клиент {} отключился", "Обработчик {}: клиент {} отключился (или прислал неполный заголовок)",
self.id, self.id, self.client_addr
self.client_addr
); );
return Ok(()); return Ok(());
} }
return Err(e); // Другие системные ошибки ввода-вывода (например, обрыв связи)
self.message_count += 1;
let received =
String::from_utf8_lossy(&buffer[..bytes_read]);
println!(
"Обработчик {}: сообщение №{}: {:?}",
self.id,
self.message_count,
received
);
let response = format!(
"Обработчик {}: сообщение №{}: {}\n",
self.id,
self.message_count,
received
);
self.stream.write_all(response.as_bytes()).await?;
} }
// 3. Вызываем ваш парсер заголовка
let header = match parce_header(&header_buffer).await {
Ok(h) => h,
Err(ParseError::InvalidFormat) => {
println!("Обработчик {}: Критическая ошибка! Неверный формат заголовка.", self.id);
// В сетевых протоколах при неверном заголовке соединение обычно сразу рвут
return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid header format"));
}
// Если вы добавите новые ошибки, например FilenameTooLong:
Err(e) => {
println!("Обработчик {}: Ошибка парсинга: {}", self.id, e);
return Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string()));
}
};
self.message_count += 1;
println!("Обработчик {}: Успешно распарсен заголовок №{}: {:?}", self.id, self.message_count, header);
// 4. Используем данные из распарсенного заголовка!
// Теперь мы знаем точный размер тела сообщения благодаря header.body_size
let body_len = header.body_size() as usize;
// Подгоняем размер буфера под размер тела (чтобы не читать лишнего)
if body_len > body_buffer.len() {
body_buffer.resize(body_len, 0);
}
// Читаем из сети тело сообщения (ровно столько байт, сколько указано в заголовке)
self.stream.read_exact(&mut body_buffer[..body_len]).await?;
let received_body = String::from_utf8_lossy(&body_buffer[..body_len]);
println!("Обработчик {}: Получено тело сообщения: {:?}", self.id, received_body);
// 5. Отправляем ответ клиенту
let response = format!(
"Обработчик {}: Заголовок валиден (cmd={}). Тело принято.\n",
self.id, header.command()
);
self.stream.write_all(response.as_bytes()).await?;
} }
} }
}
+59 -2
View File
@@ -2,5 +2,62 @@
## 📌 Текущее состояние ## 📌 Текущее состояние
* **Где остановился:** Сервер успешно запускается и слушает сетевой сокет. * **Где остановился:** Сервер успешно запускается и слушает сетевой сокет.
* **Проблема:** Не удалось проверить эхо-ответ через PowerShell. Проверочная строка: smikhai@DebHOME:~$ printf '\x4E\x43\x4C\x44\x01\x01\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x03\xFB' \
* **Следующий шаг:** Найти рабочий способ отправки тестового запроса в PowerShell или использовать альтернативный инструмент. > | nc 192.168.1.213 32768
выдаёт ошибку парсера:
Сервер слушает 192.168.1.213:32768
Обработчик 1 создан для 192.168.1.170:50978
Обработчик 1: Критическая ошибка! Неверный формат заголовка.
Обработчик 1: ошибка: Invalid header format
что радует.
* **Проблема:**
* **Следующий шаг:**
## Чек-лист: исправление парсера общего заголовка
* [ ] Исправить имя функции `parce_header``parse_header`.
* [ ] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод.
* [ ] Убрать `.await` при вызове `parse_header`.
* [ ] Исправить порядок полей согласно протоколу:
* `bytes[0..4]``magic`;
* `bytes[4]``version`;
* `bytes[5]``command`.
* [ ] Проверять `magic` как четыре байта `NCLD`.
* [ ] Проверять версию как число `1`, а не ASCII-символ `b'1'`.
* [ ] Пока разрешать только версию протокола `1`.
* [ ] Исправить команды `80 | 81` на `0x80 | 0x81`.
* [ ] Записывать в `Header.version` значение из `bytes[4]`.
* [ ] Для первого теста отправлять заголовок с `body_size = 0`.
## Проверка исправлений
* [ ] Отправить корректный заголовок.
* [ ] Убедиться, что сервер вывел:
```text
Header {
version: 1,
command: 1,
flags: 0,
request_id: 17,
body_size: 0,
}
```
* [ ] Отправить заголовок с неправильным `magic`.
* [ ] Убедиться, что сервер вернул `InvalidFormat`.
* [ ] Отправить заголовок с неподдерживаемой версией.
* [ ] Убедиться, что сервер вернул `InvalidFormat`.
* [ ] Отправить заголовок с неизвестной командой.
* [ ] Убедиться, что сервер вернул `InvalidFormat`.
* [ ] Отправить заголовок с запрещённым флагом, например `0x0008`.
* [ ] Убедиться, что сервер вернул `InvalidFormat`.
## Следующая итерация — пока не выполнять
* [ ] Ограничить допустимый `body_size`.
* [ ] Не выделять буфер размером со всё тело сообщения.
* [ ] Начать отдельный разбор тела команды `UPLOAD`.
* [ ] Читать данные файла порциями, а не целиком в память.
+81
View File
@@ -0,0 +1,81 @@
use thiserror::Error;
#[derive(Debug)]
pub struct Header {
version: u8,
command: u8,
flags: u16,
request_id: u32,
body_size: u64,
}
#[derive(Error, Debug)]
pub enum ParseError {
#[error("Заголовок пустой или имеет неверный формат")]
InvalidFormat,
#[error("Длина имени файла ({actual}) превышает допустимый лимит в {max} байт")]
FilenameTooLong {
actual: u16,
max: u16,
}
}
pub async fn parce_header (bytes: &[u8;20]) -> Result<Header, ParseError> {
// Получаем флаги
let flags_raw = u16::from_be_bytes([bytes[6],bytes[7]]);
let request_id_raw = u32::from_be_bytes([bytes[8],bytes[9],bytes[10],bytes[11]]);
let body_size_raw = u64::from_be_bytes([bytes[12],bytes[13],bytes[14],bytes[15],bytes[16],bytes[17],bytes[18],bytes[19]]);
// Валидация номера версии, magic и команды
let [
b'1'..=b'3',
b'N', b'C', b'L', b'D',
_cmd @ (1..=4 | 80 | 81),
_, _,
..
] = bytes else {
return Err(ParseError::InvalidFormat);
};
// Валидация флагов
if (flags_raw & 0xFFF8) !=0 {
return Err(ParseError::InvalidFormat);
}
let local_header = Header{
version: bytes[0],
command: *_cmd,
flags: flags_raw,
request_id: request_id_raw,
body_size: body_size_raw,
};
Ok(local_header)
}
// Реализация публичных методов-геттеров для безопасного доступа к приватным полям
impl Header {
/// Возвращает версию протокола пакета
pub fn version(&self) -> u8 {
// Так как тип u8 реализует трейт Copy, значение просто копируется наружу
self.version
}
/// Возвращает код команды (например, 1, 2, 80 или 81)
pub fn command(&self) -> u8 {
self.command
}
/// Возвращает битовые флаги пакета (уже прошедшие валидацию)
pub fn flags(&self) -> u16 {
self.flags
}
/// Возвращает уникальный идентификатор запроса клиента
pub fn request_id(&self) -> u32 {
self.request_id
}
/// Возвращает размер тела сообщения в байтах
pub fn body_size(&self) -> u64 {
self.body_size
}
}