HTTP-сервер на сокетах

Принимаем подключения и обрабатываем каждое в своём потоке.

Код

подключить сеть;

// HTTP-сервер с простой маршрутизацией и параллельной обработкой —
// разные страницы для разных путей запроса, плюс честная страница
// "404 Не найдено" для всего остального. Каждое подключение обрабатывается
// в СВОЁМ собственном потоке (параллельно), поэтому медленный клиент не
// блокирует остальных — сервер продолжает принимать новые подключения,
// пока предыдущие ещё обрабатываются.

// AF_INET = 2, SOCK_STREAM = 1 (TCP), SOL_SOCKET = 1, SO_REUSEADDR = 2.
функция главная(): целое
    целое32 слушающий_сокет = создать_сокет(целое32(2), целое32(1), целое32(0));
    если (слушающий_сокет меньше целое32(0))
        вывести("не удалось создать сокет");
        вернуть 1;
    конец

    текст включено = выделить_память(4);
    записать_целое32(включено, 0, целое32(1));
    настроить_сокет(слушающий_сокет, целое32(1), целое32(2), включено, целое32(4));

    // "0.0.0.0" — слушаем на всех сетевых интерфейсах, не только localhost.
    текст адрес = построить_адрес_сервера("0.0.0.0", целое32(8080));
    целое32 итог_привязки = привязать_сокет(слушающий_сокет, адрес, целое32(16));
    если (итог_привязки меньше целое32(0))
        вывести("не удалось занять порт 8080 — возможно, уже занят");
        вернуть 1;
    конец

    слушать_сокет(слушающий_сокет, целое32(10));
    вывести("сервер слушает на http://0.0.0.0:8080");

    целое кадр = 0;
    пока (кадр меньше 20)
        целое32 клиент = принять_подключение(слушающий_сокет, нулевой_указатель_сети(), нулевой_указатель_сети());
        если (клиент не меньше целое32(0))
            // НОВОЕ (параллельная обработка): "параллельно" сам по себе не
            // захватывает обычные переменные из окружающего кода (см.
            // подробное объяснение в codegen.rs, fn сгенерировать_параллельно)
            // — единственное, что пересекает границу потока безопасно, это
            // канал. Поэтому номер сокета клиента передаётся именно через
            // него: отправляем ЗДЕСЬ, в родительском потоке, читаем внутри
            // потока — тот же приём, что уже в examples/многопоточность.код,
            // просто в обратную сторону (данные ВНУТРЬ потока, а не наружу).
            канал из целое32 канал_клиента = создать_канал();
            канал_клиента.отправить(клиент);

            параллельно
                целое32 сокет_клиента = канал_клиента.получить();
                обработать_подключение(сокет_клиента);
            конец
        конец
        кадр = кадр + 1;
    конец

    // Ждём, пока все запущенные потоки-обработчики закончат отвечать своим
    // клиентам — иначе главный поток мог бы дойти до закрытия слушающего
    // сокета раньше, чем последние подключения успели получить ответ.
    дождаться_все_потоки();
    закрыть_сокет(слушающий_сокет);
    вернуть 0;
конец

// НОВОЕ: вся обработка ОДНОГО подключения — читает запрос, определяет
// путь, формирует и отправляет ответ, закрывает сокет. Вынесена в
// отдельную функцию специально для того, чтобы её было удобно вызывать
// изнутри "параллельно" (см. главная()) — тело потока должно быть
// компактным, вся содержательная логика живёт здесь.
функция обработать_подключение(целое32 клиент): целое
    текст буфер_запроса = выделить_память(2048);
    получить_данные(клиент, буфер_запроса, 2047, целое32(0));

    текст путь = извлечь_путь(буфер_запроса);
    вывести("запрос: " + путь);

    текст ответ = обработать_запрос(путь);
    отправить_данные(клиент, ответ, длина(ответ), целое32(0));

    закрыть_сокет(клиент);
    освободить_память(буфер_запроса);
    вернуть 0;
конец

// НОВОЕ: маршрутизация — какую страницу показать для какого пути.
// Простая цепочка если/иначе — для небольшого числа маршрутов этого
// достаточно, без отдельной "таблицы маршрутов" (список пар путь+функция),
// которая понадобится, когда страниц станет действительно много.
// ⚠️ Честное, узкое ограничение: маршрут ниже — "/about" (ASCII), не
// кириллический "/о-языке" — HTTP-клиенты (браузеры, curl) всегда
// URL-кодируют не-ASCII символы в пути запроса (например, "о" становится
// "%d0%be"), а декодирования "%XX -> байт" этот сервер пока не делает —
// сравнение с сырым кириллическим текстом просто не совпало бы. Для v1
// (демонстрация самого протокола) проще и честнее держать маршруты на
// ASCII, чем добавлять URL-декодирование — но заголовки, тело страниц
// остаются полностью на русском, ограничение касается только адресной
// строки.
функция обработать_запрос(текст путь): текст
    если (путь == "/")
        вернуть сформировать_ответ(целое32(200), "OK",
            "<html><body><h1>Сфера</h1><p>Язык программирования на кириллице.</p>" +
            "<p><a href=\"/about\">о языке</a></p></body></html>");
    конец
    если (путь == "/about")
        вернуть сформировать_ответ(целое32(200), "OK",
            "<html><body><h1>О языке</h1>" +
            "<p>Компилируемый язык с русским синтаксисом, компилятор написан на Rust, " +
            "генерирует машинный код через LLVM.</p>" +
            "<p><a href=\"/\">на главную</a></p></body></html>");
    конец
    вернуть сформировать_ответ(целое32(404), "Not Found",
        "<html><body><h1>404</h1><p>Страница не найдена.</p></body></html>");
конец

// НОВОЕ: собирает полный HTTP-ответ (заголовки + тело) для ЛЮБОГО кода
// состояния — убирает дублирование, которое было бы при повторении одних
// и тех же трёх строк заголовков в каждой ветке маршрутизации отдельно.
функция сформировать_ответ(целое32 код, текст статус, текст тело): текст
    вернуть "HTTP/1.1 " + целое_в_текст(целое(код)) + " " + статус +
        "\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " +
        целое_в_текст(длина(тело)) + "\r\nConnection: close\r\n\r\n" + тело;
конец

// Извлекает путь запроса ("GET /путь HTTP/1.1" -> "/путь") — находит
// первый и второй пробелы в первой строке, берёт то, что между ними.
// Если что-то пошло не так (запрос пустой, формат неожиданный), просто
// возвращает "/" вместо падения — безопаснее ошибиться в сторону главной
// страницы, чем уронить сервер на одном плохо сформированном запросе.
функция извлечь_путь(текст запрос): текст
    результат из целого первый_пробел = найти(запрос, " ");
    если ( не первый_пробел.успешно)
        вернуть "/";
    конец
    целое начало_пути = первый_пробел.значение + 1;

    результат из текста остаток_результат = подстрока(запрос, начало_пути, длина(запрос) - начало_пути);
    если ( не остаток_результат.успешно)
        вернуть "/";
    конец
    текст остаток = остаток_результат.значение;

    результат из целого второй_пробел = найти(остаток, " ");
    если ( не второй_пробел.успешно)
        вернуть "/";
    конец

    результат из текста путь_результат = подстрока(остаток, 0, второй_пробел.значение);
    если (путь_результат.успешно)
        вернуть путь_результат.значение;
    конец
    вернуть "/";
конец

Как запустить

Сохраните код в файл, например http_сервер.код, и выполните:

запустить http_сервер.код

Как это работает

  • подключить сеть; даёт сокеты: создать, привязать к порту, слушать и принимать подключения.
  • Каждое подключение обрабатывается в блоке параллельно, поэтому медленный клиент не блокирует остальных.
  • Номер сокета клиента передаётся в поток через канал.
  • Для настоящего сайта удобнее готовая библиотека веб.код: см. урок про веб-сервер.

Исходник лежит в репозитории: examples/веб/http_сервер.код.