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_сервер.код.