Веб-сервер

Веб-сервер — встроенный в Erlang/OTP сервер, способный принимать и отдавать информацию по протоколу HTTP/1.1.

Стандартное применение веб-сервера — обеспечить работу нашего сайта. Пользователь через программу-браузер (например, Firefox) делает запрос по определённому адресу. Если адрес соответствует нашему веб-серверу, он получает от браузера запрос, в ответ на который возвращает HTML-страницу. Эта страница может содержать изображения, и тогда браузер посылает ещё запрос(ы), чтобы получить эти изображения. После этого страница отображается в браузере.

Веб-сервер нужен не только для того, чтобы человек из любой точки планеты мог посмотреть информацию о нашей компании или поиграть в игру, которую мы разработали. Веб-серверы можно делать приватными, чтобы доступ к данным был лишь у определённого круга лиц или даже у одного человека. Вполне естественным является держать веб-сервер (бэкенд) на той же машине, где мы заходим на сайт через браузер (фронтенд).

Для Эрланга есть два основных способа организовать пользовательский интерфейс: через wx (порт GUI-библиотеки wxWidgets) и веб-технологии. Преимущество wx — в скорости отрисовки. У веб-технологий преимущество в универсальности. Не будет особых проблем, например, чтобы заставить браузер воспроизводить или записывать звуки. Веб-сервер (бэкенд) может быть на одной машине, а браузер (фронтенд) — на другой, причём с другой операционкой.

Очевидно, можно как-то подружить Эрланг и стандартные веб-серверы вроде Apache и Nginx. Но у Эрланга своя специфика, для него естественно быть запущенным в виде постоянно работающей Системы. Поэтому логичным и эффективным решением является иметь в составе Системы действующий веб-сервер, который мог бы обмениваться с другими акторами сообщениями-термами.

Наш первый веб-сервер

Прежде чем написать код веб-сервера, надо бы решить, что мы желаем увидеть в браузере. Пусть это будет парочка страниц. С одной страницы ссылка ведёт на другую, и на этой второй есть картинка.

Первая страница (index.html):

<html><head><title>Главная страница</title><meta charset='utf-8'>
</head><body>
<h1>Ура! Заработало!</h1>
<p>Наш веб-сервер работает. Он отдаёт html-файлы и рисунки.
<a href='shot.html'>Посмотреть скриншот</a>.</p>
</body></html>

Вторая страница (shot.html) содержит картинку-скриншот нашего браузера в формате PNG:

<html><head><title>Скриншот</title><meta charset='utf-8'>
</head><body>
<h1>Скриншот</h1>
<p>Это страница, включающая картинку.</p>
<p>
<img src="shot.png" width="62%" title="Скриншот" alt="Скриншот" />
</p>
</body></html>

Ну и третий файл — скриншот shot.png. Скоро мы его увидим.

Всё это сложим в один какой-нибудь новый каталог. Я назвал его www, но это не принципиально.

Код модуля (я его тоже назвал www):

-module(www).
-export([start/0]).

start() ->
    inets:start(),
    {ok,Pid} = inets:start(httpd,
                [{server_name, "alea"},
                 {document_root, "."},
                 {server_root, "."},
                 {port, 8000},
                 {mime_types,
                  [
                   {"html","text/html"},
                   {"htm","text/html"},
                   {"js","text/javascript"},
                   {"css","text/css"},
                   {"gif","image/gif"},
                   {"jpg","image/jpeg"},
                   {"jpeg","image/jpeg"},
                   {"png","image/png"}
                  ]}]),
    Pid.

Компилируем и получаем файл www.beam. Запускаем оболочку, а в ней www:start().

1> www:start().
<0.94.0>

Функция www:start() вернула пид актора-сервера, и это уже радует. Если бы сервер не получилось запустить, вылезла бы ошибка.

Поскольку в коде программы мы определили для нашего веб-сервера порт 8000, введём в адресной строке нашего браузера localhost:8000.

Главная страница сайта

Как мы видим, наш веб-сервер и правда работает. Возможно, мы бы хотели, чтобы главная страница нашего сайта выглядела иначе. Тут просто отображается пять файлов в том каталоге, который мы определили как корневой, а не страница index.html. Но для начала сойдёт.

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

Кликнув по index.html, мы попадаем уже на настоящую веб-страницу.

Заработало!

Кликнув по ссылке, можно перейти на вторую страницу.

Вторая страница

Если вернуться на главную страницу (где перечень файлов) и кликнуть по www.beam, этот файл будет скачан и сохранится на жёстком диске. Если кликнуть по www.erl, этот файл не скачается, а отобразится в браузере.

www.erl

В завершении мы остановим наш веб-сервер. Для этого надо знать пид.

3> inets:stop(httpd, <0.94.0>).
ok

Разбор кода

Первое, что бросается в глаза, — два раза, причём подряд, вызывается inet:start.

inets:start(),
{ok,Pid} = inets:start(httpd,

inets:start/0 и inets:start/2 это две разные функции. Первая запускает приложение Inets. Вторая — запускает веб-сервер. Без первого шага не получится второго.

Динамические веб-страницы

Есть два основных подхода к тому, как отдавать веб-контент пользователю. Первый вариант — хранить на сервере определённые файлы (например, html или jpg). Когда браузер делает запрос (например, получить http://karmanov.su/index.html) веб-сервер берёт этот файл (index.html) и отдаёт его. Второй вариант — генерировать отдаваемые файлы. Их как таковых нет на сервере, но они могут быть сгенерированы, включая изображения. Например, если сервер получит запрос на http://karmanov.su/time, он запустит программу time, которая и сгенерирует веб-контент. Это может быть, например, html-страница или изображение.

В первые годы существования интернета преобладал первый подход (статический). Сайты были в основном информационные, и от запроса к запросу выглядели одинаково. Потом их стали вытеснять динамические веб-страницы. Если, например, мы зарегистрированы на сайте и оплатили какой-то контент, он нам полностью отображается. Если не оплатили — только часть. С помощью динамических веб-страниц можно реализовать какой-нибудь чат или онлайн-игру, где соперником выступает человек на другом конце света. Можно купить что-нибудь в онлайн-магазине. И так далее.

Встроенный в Эрланг http-сервер (inets) поддерживает два механизма создания динамических веб-страниц. Первый это знаменитый CGI, который есть, наверно, во всех известных веб-серверах. Программа может быть написана на любом языке программирования. Веб-сервер её запускает, результат работы отдаёт пользователю. Для этого придётся создать в операционке новый процесс, и поэтому это довольно дорогостоящая операция. Большой плюс CGI в том, что можно найти где-нибудь на просторах сети какой-нибудь готовый перл-скрипт, сохранить его на сервере, и он будет уже работать.

Второй вариант это родной для inets — ESI (Erlang Server Interface). Он позволяет запускать функции из модулей, которые мы написали на Эрланге. Этот вариант куда более легковесный, нежели CGI, потому что нет нужды создавать новый процесс в операционке. Мы будем последовательны и остановимся именно на этом варианте.

Для исполнения Эрланг-функций на веб-сервере используется модуль mod_esi. При старте сервера этот модуль должен быть загружен. Для этого можно указать опцию:

{modules, [mod_esi]},

Но мы пока не будем этого делать. mod_esi и так по умолчанию загружается — вместе с ещё несколькими. Если мы укажем именно эту строку, то будет загружен только один модуль mod_esi, а другие — нет.

Чтобы ESI заработал, нам нужно в опциях указать лишь erl_script_alias. Добавим её в наш пример (см. выше) где-нибудь между портом и mime_types:

{port, 8000},
{erl_script_alias, {"/esi", [my_esi, io]}},
{mime_types,

"/esi" это часть урла, видя которую веб-сервер поймёт, что ему предстоит запустить определённую Эрланг функцию из определённого модуля. my_esi это модуль, который мы скоро напишем и функции которого будут запускаться на веб-сервере. Конечно, вместо “/esi” или my_esi можно написать что-нибудь иное.

-module(my_esi).
-export([vremya/3]).

vremya(Sid, Env, In) ->
    {Hour,Min,Sec} = time(),
    Time = io_lib:format("~p:~p:~p", [Hour,Min,Sec]),
    mod_esi:deliver(Sid, [Time]).

Вот эту функцию vremya, которая отдаёт строку Time, мы и попробуем запустить через механизм ESI. Эта функция должна принимать три аргумента: ид сессии, переменные окружения и входящие данные. Отдавать назад данные надо с помощью mod_esi:deliver/2. При этом надо указать ид сессии, чтобы веб-серверу было понятно, на какой запрос даётся ответ.

Скомпилируем этот модуль. Работающая Система, конечно, должна видеть получившийся beam-файл. Мы внесли изменение в www.erl, поэтому его тоже надо не забыть скомпилировать. Запускаем веб-сервер. В адресной строке браузера вводим:

http://localhost:8000/esi/my_esi:vremya

/esi/ это то, что мы указали в опциях как часть урла, после которого идёт указание модуля и файла. my_esi:vremya это модуль и функция, которые мы только что написали и скомпилировали. Всё очень изящно и просто.

В ответ нам в браузере будет написано что-то вроде “23:34:44” (как было в моём случае). Ура! У нас получилось создать динамическую страницу.

Чтобы вывести кириллицу, надо будет строку пропустить через функцию unicode:characters_to_binary/1. Попробуем это, а заодно глянем, как выглядит ид сессии и что содержится в Env и In.

-module(my_esi).
-export([vremya/3]).

vremya(Sid, Env, In) ->
    {Hour,Min,Sec} = time(),
    Time = io_lib:format("<html><head><title>Время</title>
      <meta charset='utf-8'></head><body><h1>Время</h1>~p:~p:~p
      <hr>~p<hr>~p<hr>~p</body></html>", [Hour,Min,Sec,Sid,Env,In]),
    mod_esi:deliver(Sid, [unicode:characters_to_binary(Time)]).

Мы желаем посмотреть, что содержится в In (аргумент-запрос, передаваемый из браузера). Поэтому и в адресной строке введём что-то вроде такого:

http://localhost:8000/esi/my_esi:vremya?myname=alex

У меня результат был следующим:

Динамическая страница

Кириллица (а точнее юникод) работает. В Env — стандартные данные об окружении, которые нам могут пригодиться в дальнейшем при обработке запросов. В In строка “myname=alex”. Причём этот аргумент-запрос можно увидеть и в Env.


Copyright © 2025 Алексей Карманов