Разработка веб-сайта на паскале (backend)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
В этой статье я расскажу о том, зачем, почему и как я начал делать сайты на паскале: Delphi / FPC.
Вероятно, «сайт на паскале» ассоццируется с чем-то вроде:

writeln('Content-type: text/html');

Но нет, всё гораздо интереснее! Впрочем, исходный код реального сайта (почти весь) доступен на GitHub.

Зачем?


Вообще я ни разу не профессиональный веб-разработчик — я делаю игры. А игре, особенно онлайновой, нужен сайт. Поэтому так сложилось, что я стал делать ещё и сайты для своих игр. Используя CGI на Perl — в начале/середине 2000-х это было популярно. Всё было хорошо, пока не возникла проблема.

В 2013 году мы начали проводить онлайн-турниры по игре "Спектромансер", для этого на сайте игры я сделал турнирную страничку, где показывается кому с кем играть, текущие результаты и т.п. В момент старта турнира страничка у игроков обновилась и… не загрузилась. Люди нажимали F5, чем ещё больше усугубляли проблему. Оказалось, что даже 4-5 запросов в секунду к CGI-скрипту, запускаемому в виде отдельного Perl-процесса, ощутимо замедляют сервер, а >10 запросов в секунду делают его совсем недоступным.

Хорошо что этот стресс-тест состоялся во время репетиционного турнира: в дальнейшем я уже использовал для турниров обновляемую статическую страницу.

Почему?


Таким образом, когда возникла необходимость делать вот этот сайт для новой игры, возник вопрос — на чём? Тормозной CGI на Perl — не вариант. FastCGI на Perl? Не представляю как писать и отлаживать многопоточную программу на Perl, мне и с обычными-то скриптами проблем хватало. Node.js? Наверно это был бы наилучший выбор, если бы не некоторая неприязнь к JS. А поскольку сама игра и её сервер написаны на паскале (на самом деле Delphi, но FPC тоже годится), возникла идея — а не сделать ли сайт на этом же языке? Это упростит интеграцию с сервером игры. «Попытка — не пытка!» — подумал я, и решил попробовать.

Как?


В качестве интерфейса выбрал SimpleCGI (SCGI): он несколько проще FastCGI, а преимущества последнего для меня неактуальны — нет необходимости разносить бэкенд на разные сервера, всё крутится на одном сервере. Так что задача свелась к разработке некоего SCGI-фреймворка, обрабатывающего запросы от сервера и генерирующего в ответ HTML-страницы из неких заготовок, шаблонов. В результате получился вот такой модуль-фреймворк. Он состоит из следующих частей:

  • Главный цикл: принимает входящие соединения, считывает запросы и складывает их в очередь для обработки. Готовые ответы на обработанные запросы записывает в сокеты соединений и закрывает их.
  • Рабочие потоки (N штук): достают запросы из очереди, парсят их заголовки и вызывают для исполнения пользовательские обработчики. У каждого worker'а — своё собственное постоянное подключение к БД.
  • Система трансляции шаблонов: служит для генерации HTML-кода (или любого произвольного текста) путём рекурсивной трансляции шаблонов. Шаблоны грузятся из текстовых файлов.
  • Набор вспомогательных функций: предназначен для использования обработчиками запросов (аналогично модулю CGI.pm в Perl). Получение параметров, установка куки и т.п.

Шаблоны


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

Работает она так. В папке «templates» лежат файлы шаблонов: они загружаются при запуске процесса а также перезагружаются при изменении — таким образом можно изменять динамический контент не перезапуская процесс. В каждом файле может быть один или несколько шаблонов. Все вместе они образуют словарь (или хэш) шаблонов: {«имя»->«значение»}. Это статический словарь шаблонов — он общий для всех запросов и его содержимое неизменно (пока не изменится содержимое файлов). Есть ещё второй — динамический словарь, он создаётся пустым для каждого запроса и заполняется обработчиком динамическими данными — например из БД. Комбинируя статические и динамические данные и формируется итоговый результат.

Пример декларации шаблона:

#NEWSFEED_ITEM:
<div class=NewsHeader>
 <a href='/$LANG_LC/forum/thread/$NEWS_ID'><IF_NEWS_PINNED>[TOP]  </IF_NEWS_PINNED>$NEWS_DATE   $NEWS_TITLE</a>
</div>
<div class=NewsText>$NEWS_TEXT
 <div align=right>
  <a href='/$LANG_LC/forum/thread/$NEWS_ID'>$COMMENTS</a>
 </div>
</div>

Это статический шаблон записи в ленте новостей с именем NEWSFEED_ITEM, внутри он содержит включения нескольких других шаблонов, например NEWS_TEXT — динамический шаблон, содержащий текст новости, загруженный из БД. Трансляция заключается в том, что все подстроки вида $ИМЯ_ШАБЛОНА рекурсивно заменяются на значение этого шаблона.

Здесь можно также заметить псевдотэг для условной трансляции: <IF_ИМЯ_ШАБЛОНА> — в процессе трансляции такие тэги удаляются а их содержимое оставляется либо также удаляется — в зависимости от значения указанного шаблона. Я специально выбрал такой формат условий — в виде HTML-тэгов, чтобы при редактировании в текстовом редакторе работала подсветка синтаксиса и чтобы было легко видеть парный тэг.

Код формирования ленты новостей, использующий этот шаблон, выглядит примерно так:


    result:='';
    // Для каждой новости выполняем трансляцию шаблона NEWSFEED_ITEM и складываем всё в строку result
    for i:=0 to n-1 do begin
      id:=StrToIntDef(sa[i*c],0);
      title:=sa[i*c+1];
      cnt:=StrToIntDef(sa[i*c+2],1)-1;
      flags:=StrToIntDef(sa[i*c+3],0);
      // запрашиваем текст и дату новости
      db.Query('SELECT msg,created FROM messages WHERE topic=%d ORDER BY id LIMIT 1', 
        [id]);
      if db.lastErrorCode<>0 then continue;
      text:=db.Next;
      date:=db.NextDate;
      // Заполняем динамические шаблоны (словарь temp)
      temp.Put('NEWS_ID',id,true);
      temp.Put('NEWS_DATE',FormatDate(date,true),true);
      temp.Put('NEWS_TITLE',title,true);
      temp.Put('NEWS_PINNED',flags and 4>0,true);
      comLink:='$LNK_READ_MORE | ';
      if cnt>0 then comLink:=comLink+inttostr(cnt)+' $LNK_COMMENTS'
        else comLink:=comLink+'$LNK_LEAVE_COMMENT';
      temp.Put('NEWS_TEXT',text,true);
      temp.Put('COMMENTS',comLink,true);
      // Выполняем трансляцию шаблона
      result:=result+BuildTemplate('#NEWSFEED_ITEM');
    end;

Локализация


Шаблоны также удобно использовать для локализации. Для этого используется глобальная (в контексте запроса) переменная clientLang. Работает это так: если обработчик запроса выясняет, что клиенту нужна страница на русском языке — он записывает в clientLang значение «RU», после чего транслятор шаблонов, обнаружив в тексте $ИМЯ_ШАБЛОНА, всегда пытается сперва применить $ИМЯ_ШАБЛОНА_RU. Таким образом, для локализации нужно всего лишь для каждого шаблона с текстом создать его вариант для другого языка:

#TITLE_NEWS:News
#TITLE_NEWS_RU:Новости

Пример использование фреймворка


Пример кода простого сайта:

program website;
uses SysUtils, SCGI;

// Обработчик запроса главной страницы
function IndexPage:AnsiString; stdcall;
 begin
   result:=FormatHeaders('text/html')+BuildTemplate('#INDEX.HTM');
 end;

begin
 SetCurrentDir(ExtractFileDir(ParamStr(0)));
 SCGI.Initialize; // Загрузка конфига
 AddHandler('/',IndexPage); // Устанавливаем обработчик для запроса '/'
 SCGI.RunServer; // запускаем рабочие потоки и главный цикл
end.

Итого


Описываемый фреймворк я написал в процессе создания реального сайта astralheroes.com в конце 2015 года. Как это обычно бывает, первый блин вышел немножко комом — код получился несколько сумбурным и запутанным, следующий сайт получается уже лучше. Тем не менее, и процессом и результатом я доволен: сайт работает хорошо, легко отлаживается и обновляется.

Выводы:

  • Я ожидал, что по сравнению с компактным Perl код сайта сильно раздуется, но нет — та же функциональность, написанная на паскале, занимает лишь примерно вдвое больше, чем на Perl. Но при этом выглядит более понятно.
  • Радует отладка! Perl — замечательный язык, если нужно написать что-то в пределах 100 строк, такое, что не требует отладки. Но как только нужно сделать что-то более-менее сложное — отладка превращается в кошмар. В Delphi же заниматься отладкой легко и удобно.
  • Часть функционала сайта осталась на Perl. Потому что во-первых, часть функций осталась неизменной с предыдущего сайта, поэтому нет смысла переписывать то, что уже написано и исправно работает. А во-вторых, некоторые некритичные к скорости вещи гораздо проще реализовать на Perl, если там для этого есть готовая библиотека, а на паскале её нет.
  • Работать с шаблонами довольно удобно: они позволяют структурировать сайт, разбить его на отдельные блоки, избегать дублирования текста. И еще упрощают локализацию.
  • Радует производительность. Ведь я экономлю время не только на запуске процессов, загрузке библиотек, подключении к БД (что само по себе немаловажно), но и имею возможность сохранять контекст, глобальные данные и использовать их для обработки множества запросов. Например, для реализации поиска по форуму используется глобальный индекс, который постоянно доступен в памяти — не нужно ничего грузить из БД. Данные рейтинга игроков также кэшируются.

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



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

Так где же исходники сайта?


Исходники на GitHub: github.com/Cooler2/ApusEngineExamples

Обратите внимание, что в репозитории есть подмодуль, поэтому клонировать лучше с параметром "--recursive".

Проект сайта находится в файле: «AH-Website\Backend\src\website.dpr»

Это не совсем полная копия действующего сайта: понятно, что я не могу опубликовать содержимое БД с данными игроков, я также не публикую CGI-скрипты, поскольку они не имеют отношения к описываемой теме. Тем не менее, проект собирается, запускается и работает, полностью демонстрируя работу фреймворка.

Публикация кода сайта, а также кода движка, который он использует, стала возможной благодаря поддержке, которую я получил на Patreon. Выражаю благодарность всем поддержавшим, и призываю присоединиться — впереди ещё много интересного :)

Спасибо за внимание!
Источник: https://habr.com/ru/post/491272/


Интересные статьи

Интересные статьи

Предыстория Предыдущая статья В предыдущей статье я писал о том, как сконфигурировать ваше приложение с помощью Sitri, однако, упустил момент с локальной разработкой, так как согласит...
Стэнфордский университет, США — один из лучших в мире в области информатики (Computer Science). Он щедро делится своими курсами, и одним из самых популярных и успешных курсов является...
За последние пару недель разве что ленивый не посоветовал окружающим, как правильно и без потерь перейти на удалёнку. Мы не будем вам ничего советовать. А просто расскажем, как мы наладили удален...
Это — первое, что ищут взглядом, когда переключаются между вкладками браузера. Только что мы дали одно из возможных описаний того, что называется «фавиконом». Пространство на экране, которое зани...
Согласно многочисленным исследованиям поведения пользователей на сайте, порядка 25% посетителей покидают ресурс, если страница грузится более 4 секунд.