Всем привет! Моя первая статья получилась... слабой. Постарался сделать работу над ошибками и заодно ответить на вопросы, возникшие в комментариях. Прошу дать мне второй шанс!

Первый блин комом)

О себе

Меня зовут Вячеслав. Я учусь в 9 классе... Сейчас кто-то может бросить статью и будет прав. Что я могу вам рассказать? Немного (или ничего?). Но дайте всё же мне шанс заинтересовать вас или хотя бы просто поделиться опытом создания своего первого довольно (для меня) крупного проекта на одного человека.

Цель

Цель проекта - упростить разработку веб серверов путём увеличения уровня абстракции. Т.е. мы уходим ещё дальше от низкоуровневых деталей (да и не очень "низко"), чтобы разработчику не требовались глубокие знания и опыт. Прекрасно подойдёт для новичков

Зачем это нужно?

Хороший вопрос. Возможно, большинству читателей такое упрощение не требуется - вы и так прекрасно всё понимаете и знаете, а значит и реализуете простой веб сервер быстро. И всё же, насколько быстро? Зачем вам тратить драгоценное время на написание кода на { подставьте свой любимый фреймворк/библиотеку }, если можно сделать тоже самое, но быстрее?

А ещё такое упрощение поможет непрограммистам реализовать достаточный для их цели веб сервер без найма знающих ИЛИ изучения темы самостоятельно. Вариант, конечно, не лучший, но почему бы и нет?

Почему netter?

Netter - проект с открытым исходным кодом и он в первую очередь учебный. Можно выделить следующие детали:

  • Кроссплатформенность: проект работает и на Windows, и на Linux (пока, к сожалению, без MacOS);

  • Гибкая настройка: плагины помогут вам детальнее настроить логику работы веб сервера;

  • Достаточно низкий уровень входа (особенно, если у вас есть опыт разработки на Rust / C++ / Java / Kotlin): синтаксис простой в освоении, хотя есть свои нюансы.

Главное не надо сравнивать проект с nginx / apache и т.д. - это крупные и мощные инструменты, соревнования с которыми изначально в планы не входили.

Подробнее про сам проект

Наконец перейдём к самому главному. Что же я хочу вам показать?

Сам по себе netter включает в себя несколько модулей: ядро, логгирование, сервис (служба/демон), клиент (пока только CLI, но в разработке GUI под десктоп) и крейт (библиотека) для написания плагинов в RDL. Далее подробнее про каждый:

Ядро

Ядро включает в себя полную обработку конфигурации веб сервера - специального языка Route Definition Language (далее - RDL. Не путайте с Report Definition Language), а также сам по себе код http сервера, который (после обработки конфига) получает необходимые ему ip, порт и, если есть, настройки TLS для защиты соединения.

RDL

RDL - интерпретируемый язык, схожий по синтаксису с Rust / C++, Kotlin (или Java). Расширение файлов для RDL - .rd. После получения ядром файла с этим расширением, содержимое файла обрабатывается лексером, парсером и, наконец, интерпретатором. Последний же передаёт нужные http серверу данные для запуска.

Подробнее про возможности языка и его синтаксис можно почитать в документации из репозитория.

Логгирование

Внутренний модуль для логов в терминал / файл. Ничего интересного здесь нет.

Сервис

Данный модуль включает в себя два по сути одинаковых кода, но со спецификой под разные ОС: Windows и Linux. Взаимодействие между клиентом и сервисом происходит по IPC. К сожалению, демон под Linux требует права root (не получилось у меня избежать проблем с путями и правами, когда демон работает не от root), поэтому взаимодействие осуществляется через sudo.

Плагины

А это уже момент интереснее. Так как основная цель проекта - простота, в RDL включить весь функционал не получится (вообще получится, но всё равно же найдётся специфическая задача, которую не получится реализовать на этом языке). И на выручку идут плагины: динамические библиотеки (.dll / .so). На момент публикации есть всего 1 библиотека для написания плагинов - под Rust. Идея в том, что вам нужно банально вызвать макрос в верху файла (чтобы инициализировать входную точку в плагин раньше, чем функции, которые должны быть перенесены в RDL) и указывать атрибут #[netter_plugin] у каждой функции, которая должна быть доступна в языке. Под капотом происходят преобразования в unsafe extern "C", так что готовьтесь к этому морально (хотя в большинстве случаев проблем быть не должно). После этого, вы собираете код в динамическую либу, а в файле конфигурации импортируете эту самую либу. Всё.

Опять же, подробнее про реализацию на практике можете почитать в документации.

Реальная цель, которую мне удалось реализовать с помощью этого проекта

Удалось совместить полезное с приятным:

Задача: реализовать "пакетный менеджер" для управления (скачивания на ваше устройство / загрузка на сервер) плагинами.

Реализация (пока не до конца готово и в репозитории код не лежит, но почему бы сейчас не показать возможности netter на реальном примере?):

Нам надо принимать файлы, верно? Вот тут я долго я думал над тем, как это сделать, т.к. мне никогда ранее не приходилось работать с файлами по http (да и ftp...). Я пришёл к такому решению: отправлять содержимое файла в теле POST запроса в кодировке base64. Вероятно, не самое лучшее решение, но почему бы и нет? А ещё мы будем принимать параметры запроса: название плагина, его версию и автора. На сервер плагин будет храниться в формате pkg_name@pkg_version.dll (для примера указал .dll, но расширение может быть и .so).

Получаем такой код плагина

use std::io::Write;
use base64::Engine;
use netter_plugger::{generate_dispatch_func, netter_plugin};

generate_dispatch_func!();

const PACKAGE_DIR: &str = "your/abs/path/dll";

#[netter_plugin]
fn write_file_base64(base64_data: String, author: String, version: String, name: String) -> Result<String, String> {
    let decoded_data = base64::engine::general_purpose::STANDARD
        .decode(base64_data.as_bytes())
        .map_err(|e| format!("Ошибка декодирования Base64: {}", e.to_string()))?;

    let mut target_path = std::path::PathBuf::from(PACKAGE_DIR);
    target_path.push(&author);

    std::fs::create_dir_all(&target_path)
        .map_err(|e| format!("Ошибка создания директории [{}]: {}", target_path.display(), e.to_string()))?;

    #[cfg(windows)] let filename = format!("{}@{}.dll", name, version);
    #[cfg(unix)] let filename = format!("{}@{}.so", name, version);

    target_path.push(filename);
    let mut file = std::fs::File::create(&target_path)
        .map_err(|e| format!("Ошибка создания/открытия файла [{}]: {}", target_path.display(), e.to_string()))?;

    file.write_all(&decoded_data)
        .map_err(|e| format!("Ошибка записи в файл [{}]: {}", target_path.display(), e.to_string()))?;

    Ok(format!("Файл успешно записан по пути: {}", target_path.display()))
}

И такой код RDL

config {
    type = "http";
    host = "127.0.0.1";
    port = 9094;
};

import "your/abs/path/to/file.dll" as FileSystem; // вместо "FileSystem"
// вы можете указать любой другой алиас

global_error_handler(e) {
    Response.body("Сервер поймал ошибку: " + e);
    Response.status(500);
    Response.send();
};

route "/packages/upload" POST {
    log_info("Получен запрос на /packages/upload");

    val pkg_name = Request.get_params("name");
    val pkg_version = Request.get_params("version");
    val pkg_author = Request.get_params("author");

    log_info("Загрузка пакета: " + pkg_name + "@" + pkg_version + " от " + pkg_author);

    val file_base64_content = Request.body_base64();

    if (file_base64_content == "") {
        log_error("Ошибка загрузки: тело запроса (содержимое файла) пустое.");
        Response.status(400);
        Response.body("Ошибка: тело запроса (содержимое файла) пустое.");
        Response.send();
    };

    log_info("Вызов FileSystem::write_file_base64 с параметрами: author=" + pkg_author + ", version=" + pkg_version + ", name=" + pkg_name);
    val save_result = FileSystem::write_file_base64(file_base64_content, pkg_author, pkg_version, pkg_name)?;

    Response.body(save_result);
    Response.status(201);
    Response.send();
};

Мне кажется, это довольно интуитивно понятная реализация, хотя мне и пришлось поломать голову из-за отсутствия опыта:)

Хорошо, мы разобрались с загрузкой плагина на сервер. Как теперь его скачивать на клиента? Да собственно таким же образом:

Плагин

#[netter_plugin]
fn read_file_base64(author: String, name_with_version_and_ext: String) -> Result<String, String> {
    let mut target_path = std::path::PathBuf::from(PACKAGE_DIR);
    target_path.push(&author);
    target_path.push(&name_with_version_and_ext);

    if !target_path.exists() {
        return Err(format!("Файл [{}] не найден", target_path.display()));
    }
    if !target_path.is_file() {
        return Err(format!("Путь [{}] существует, но не является файлом", target_path.display()));
    }

    let content = std::fs::read(&target_path)
        .map_err(|e| format!("Ошибка чтения файла [{}]: {}", target_path.display(), e.to_string()))?;

    let base64_data = base64::engine::general_purpose::STANDARD
        .encode(&content);

    Ok(base64_data)
}

RDL

route "/packages/download" GET {
    log_info("Получен запрос на /packages/download");

    val pkg_name = Request.get_params("name");
    val pkg_version = Request.get_params("version");
    val pkg_author = Request.get_params("author");

    val file_name = pkg_name + "@" + pkg_version + ".dll";

    val file = FileSystem::read_file_base64(pkg_author, file_name)?;

    Response.body_base64(file);
    Response.headers("Content-Type", "application/octet-stream");
    Response.headers("Content-Disposition", "attachment; filename=" + file_name);
    Response.status(200);
    Response.send();
};

Тут всё ещё проще!

Заключение

В заключении мне хочется сказать, что на проект было потрачено много сил. Надеюсь, что не впустую и кому-то он покажется полезным.

Если у вас есть какие-то замечания, вопросы или предложения, буду очень рад обратной связи! Особенное спасибо тем, кто будет писать замечания обосновано, а не просто "проект бесполезен". Понимаю, что эта работа может показаться ненужным изобретением велосипеда, но помните, что netter в первую очередь - учебный проект. Я не рассчитываю на использование его в крупных проектах, но ожидаю, что кому-то я смог помочь или хотя бы вдохновить.

Спасибо за прочтение этой статьи! Хорошего всем дня!

Github

Комментарии (3)


  1. Proscrito
    13.05.2025 00:07

    Раз есть такой проект, значит это кому-то нужно. Хотя мне, глубоко погрязшему во грехе дотнета, это понять сложно. Но файлы отправлять в бейс64 это за гранью добра и зла, это нечестивый хак, которого даже Содом не позволял себе с Гоморрой. Для настоящего воина света это западло.

    Однако, отрадно видеть, что молодежь тяготеет к развитию и знаниям, а не распрямляет извилины тик-током. Достойно похвалы.


    1. bjfssd757 Автор
      13.05.2025 00:07

      Спасибо за комментарий!

      Как бы вы предложили отправлять файлы? Принмать файлы с помощью multipart/form-data?


  1. stay_protected
    13.05.2025 00:07

    Меня зовут Вячеслав. Я учусь в 9 классе... Сейчас кто-то может бросить статью и будет прав. Что я могу вам рассказать? Немного (или ничего?). Но дайте всё же мне шанс заинтересовать вас или хотя бы просто поделиться опытом создания своего первого довольно (для меня) крупного проекта на одного человека.

    я представил как чел сорок лет учится в девятом классе