...

пятница, 6 марта 2020 г.

Разработка веб-сайта на паскале (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. Выражаю благодарность всем поддержавшим, и призываю присоединиться — впереди ещё много интересного :)

Спасибо за внимание!

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий