Pull to refresh

Полноценный веб-сайт на C++ и немного диванной аналитики

Reading time 20 min
Views 126K
Но зачем?
Тут должна быть картинка про троллейбус

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

Думаю, многим знакома такая разновидность веб-форумов, как имиджборды. Да-да, вы правильно поняли — именно на примере имиджборды я расскажу об опыте создания сайта на C++. Что же сподвигло меня заняться столь сомнительной пользы проектом? Левая пятка. В этом случае действительно никаких особых причин не было. Просто проснулся однажды утром и понял — хочу. Но это все лирика.

На Хабре хватает статей о веб-сайтах на C++: например, с использованием FastCGI или CppCMS. Но все это — HelloWorld'ы и туториалы. Я же вам расскажу о полноценном (пусть и не идеальном с точки зрения архитектуры и чистоты кода) проекте, постараюсь осветить различные тонкости.

Дисклеймер: тела методов помещены в объявлении классов для сокращения объема текста.

Подготовка


Начнем с инструментов, которые нам потребуются. Стоит сразу заметить, что я использую для своих проектов Qt и даже написал небольшую библиотеку, расширяющую возможности этого замечательного фреймворка. Qt и эта библиотека (называется, к слову, BeQt — Beyond Qt) использовались мною и в этот раз, но на них я останавливаться не буду, так как статья о другом.

Веб-фреймворк


Итак, прежде всего, конечно же, нужно решить, какой веб-фреймворк использовать. Выбор невелик, но он все же есть:

О первых двух я могу сказать только то, что мне они как-то не приглянулись. Это не значит, что данные фреймворки плохи — я с ними не работал и не знаю.
Wt я пробовал пару лет назад. Я тогда соблазнился схожей с Qt архитектурой и интересной его особенностью: можно было, ничего не зная о веб-приложениях и HTML, создавать иерархию виджетов, которая затем без всяких шаблонов рендерилась в этот самый HTML. С вебом я тогда был знаком на уровне «написать в блокноте HTML-страницу с title'ом и двумя параграфами», то есть, можно сказать, был не знаком вовсе. Однако быстро выяснилось, что для написания чего-то сложнее пресловутого HelloWorld'а нужны таки шаблоны, да еще и какие-то сторонние скрипты (.js), которые надо качать с «левых» сайтов. Поэтому знакомство с Wt весьма скоро закончилось.
Но через некоторое время, когда я уже познакомился с вебом, JavaScript, CSS, AJAX-запросами, — тогда я решил еще раз заняться темой «веб-сайт на C++». Вновь погуглив, я наткнулся на CppCMS. Этот фреймворк мне приглянулся сразу: документация (пусть и скудная местами) — есть, туториалы — есть (причем очень хорошие, примеры весьма практичные, а не притянутые за уши), кросс-платформенность — есть, шаблоны — есть (теперь я уже понимал, что без них никуда). Что еще нужно для счастья? Скачал, скомпилировал, подключил к проекту, проверил, порадовался.

ORM-фреймворк


Дальше встает вопрос о хранении данных. В Qt есть модуль QtSql, но он предполагает довольно низкий уровень абстракции — вручную пишем запросы, вручную обрабатываем результаты. Мне же хотелось ORM-решения. Нагуглил я, не считая разных недоделок и заброшенных проектов, следующее (прошу прощения, если пропустил какой-то стоящий фреймворк — никто не всеведущ):

LiteSQL мне не понравился тем, что на сайте я не нашел документацию, а нагуглил только что-то с XML-конфигурацией (уж простите, но XML глаза бы мои не видели). QxOrm я отверг по случайности и глупости: зашел на сайт, увидел скриншот графического дизайнера связей и закрыл страницу. Уже некоторое время спустя, прочитав статью на Хабре, я еще раз решил присмотреться к этому фреймворку, и он мне понравился, но сделанного не воротишь. Поспешишь, как говорится…
ODB мне более-менее понравился, хоть и не нашлось документации по классам, зато имелись достаточно подробные примеры. Кроме того, ODB поддерживает типы Qt, такие как QString, QDateTime и другие. Это очень удобно — не нужно преобразовывать туда-сюда. На этом фреймворке и остановил свой выбор.
Минутка юмора
Тут должна быть картинка про ODB

Подсветка синтаксиса


Что мне не нравится в большинстве крупных имиджборд — нет возможности толком вставлять код. Либо нельзя совсем (из-за особенностей разметки некоторые символы съедаются, делая шрифт заключенного между ними текста курсивным или подчеркнутым), либо можно, но без подсветки синтаксиса. Поэтому я еще до начала проекта решил, что такая возможность у меня обязательно должна присутствовать. Полагаю, что подобная фича пригодится на многих IT-сайтах.
Первое, что я нагуглил — hilite.me — онлайн-сервис, позволяющий превращать переданный в запросе текст в HTML, содержащий тот же текст, но отформатированный с подсветкой синтаксиса. Для работы с этим сайтом я сперва хотел воспользоваться QtNetwork, а конкретно — QNetworkAccessManager. Однако, поскольку CppCMS обрабатывает запросы в отдельных потоках, вне QThread, использовать этот механизм не получилось (сигналы и слоты вне QEventLoop не работают, а блокирующего API у QNetworkReply нет). Пришлось цеплять новые библиотеки: libcurl (тут, насколько мне известно, без вариантов) и cURLpp — обертка над libcurl для C++ (был еще вариант curlplusplus, но я выбрал cURLpp).
Тут я вынужденно забегу вперед. Сначала я порадовался, что подсветка работает как надо, но затем я стал замечать, что очень уж долго все это дело работает: пока соединится с сервисом, пока тот обработает запрос, пока пришлет ответ… Ну его. И вновь я напряг гугл. Вот что нашлось в этот раз: Source-highlight. Помимо консольной утилиты этот проект предоставляет также библиотеку с той же функциональностью (название говорит само за себя). То что нужно! (Замечу, однако, что, хотя библиотека и выполняет свою работу без нареканий, но набор файлов-определений для разных языков пришлось качать отдельно, и в память эти файлы никак не загрузить — библиотека может работать с ними, только если они лежакт где-то в настоящей файловой системе, а не запакованы, скажем, в исполняемый файл вместе с другими ресурсами.)

Скажи мне, каковы твои Magic bytes, и я скажу тебе, кто ты


Речь об определении типа файла. Имиджборды — они ведь от слова image в смысле «картинка», а не имидж, а значит, в самой их основе лежит прикрепление изображений к сообщениям. И не только изображений — последнее время все большую популярность набирает формат WebM. Но кто из нас не пытался хоть раз сделать что-то не по правилам? Рано или поздно кому-нибудь придет в голову прикрепить вместо картинки архив или что-то еще. Информации, передаваемой клиентом о MIME-типе файла, тоже верить нельзя, поэтому нужно проверять его тип каким-то независимым инструментом. Таким, как libmagic, например. Но обратите внимание на дату последнего обновления. Поэтому лучше использовать вот эту реализацию — она поддерживает больше новых форматов, в том числе упоминавшийся WebM.
Конечно, Magic bytes не дают никаких гарантий, что остальное содержимое файла соответствует формату, но тут уж ничего не поделаешь. Разве что нанять отдел китайцев (не в обиду этим трудягам будь сказано), которые будут проверять вручную.

Еще бы подключить все это...


Вот мы с выбором инструментов и разобрались. А как цеплять к проекту и использовать? В Qt, на счастье, есть не самая плохая (хоть и сама по себе весьма странная) система сборки — qmake.
Вот как выглядит фрагмент файла проекта (.pro/.pri), отвечающий за подключение библиотек:
Скрытый текст
isEmpty(BEQT_PREFIX) {
    mac|unix {
        BEQT_PREFIX=/usr/share/beqt
    } else:win32 {
        BEQT_PREFIX=$$(systemdrive)/PROGRA~1/BeQt
    }
}
include($${BEQT_PREFIX}/share/beqt/depend.pri)

isEmpty(CPPCMS_PREFIX) {
    mac|unix {
        CPPCMS_PREFIX=/usr
    } else:win32 {
        error(CppCMS path is not specified)
    }
}

INCLUDEPATH *= $${CPPCMS_PREFIX}/include
DEPENDPATH *= $${CPPCMS_PREFIX}/include
LIBS *= -L$${CPPCMS_PREFIX}/lib/ -lcppcms -lbooster

isEmpty(ODB_PREFIX) {
    mac|unix {
        ODB_PREFIX=/usr
    } else:win32 {
        error(ODB path is not specified)
    }
}

INCLUDEPATH *= $${ODB_PREFIX}/include
DEPENDPATH *= $${ODB_PREFIX}/include
LIBS *= -L$${ODB_PREFIX}/lib/ -lodb -lodb-sqlite

!isEmpty(ODB_QT_PREFIX) {
    INCLUDEPATH *= $${ODB_QT_PREFIX}/include
    DEPENDPATH *= $${ODB_QT_PREFIX}/include
    LIBS *= -L$${ODB_QT_PREFIX}/lib/ -lodb-qt
} else {
    LIBS *= -L$${ODB_PREFIX}/lib/ -lodb-qt
}

isEmpty(LIBCURL_PREFIX) {
    mac|unix {
        LIBCURL_PREFIX=/usr
    } else:win32 {
        error(libcurl path is not specified)
    }
}

INCLUDEPATH *= $${LIBCURL_PREFIX}/include
DEPENDPATH *= $${LIBCURL_PREFIX}/include
LIBS *= -L$${LIBCURL_PREFIX}/lib/ -lcurl

isEmpty(CURLPP_PREFIX) {
    mac|unix {
        CURLPP_PREFIX=/usr
    } else:win32 {
        error(cURLpp path is not specified)
    }
}

INCLUDEPATH *= $${CURLPP_PREFIX}/include
DEPENDPATH *= $${CURLPP_PREFIX}/include
LIBS *= -L$${CURLPP_PREFIX}/lib/ -lcurlpp

isEmpty(BOOST_PREFIX) {
    mac|unix {
        BOOST_PREFIX=/usr
    } else:win32 {
        BOOST_PREFIX=$$(systemdrive)/Boost
    }
}

INCLUDEPATH *= $${BOOST_PREFIX}/include
DEPENDPATH *= $${BOOST_PREFIX}/include
LIBS *= -L$${BOOST_PREFIX}/lib/ -lboost_regex

isEmpty(SRCHILITE_PREFIX) {
    mac|unix {
        SRCHILITE_PREFIX=/usr
    } else:win32 {
        error(GNU Source-highlight path is not specified)
    }
}

INCLUDEPATH *= $${SRCHILITE_PREFIX}/include
DEPENDPATH *= $${SRCHILITE_PREFIX}/include
LIBS *= -L$${SRCHILITE_PREFIX}/lib/ -lsource-highlight

isEmpty(LIBMAGIC_PREFIX) {
    mac|unix {
        LIBMAGIC_PREFIX=/usr
    } else:win32 {
        error(libmagic path is not specified)
    }
}

INCLUDEPATH *= $${LIBMAGIC_PREFIX}/include
DEPENDPATH *= $${LIBMAGIC_PREFIX}/include
LIBS *= -L$${LIBMAGIC_PREFIX}/lib/ -lmagic

isEmpty(SQLITE_PREFIX) {
    mac|unix {
        SQLITE_PREFIX=/usr
    } else:win32 {
        error(SQLite path is not specified)
    }
}

INCLUDEPATH *= $${SQLITE_PREFIX}/include
DEPENDPATH *= $${SQLITE_PREFIX}/include
LIBS *= -L$${SQLITE_PREFIX}/lib/ -lsqlite3

mac|unix {
    isEmpty(LORD_PREFIX):LORD_PREFIX=/usr
} else:win32 {
    isEmpty(LORD_PREFIX):PREFIX=$$(systemdrive)/PROGRA~1/ololord
}


Помимо упомянутых библиотек можно заметить также Boost и SQLite, которые имеются среди их зависимостей. Останавливаться на этих библиотеках не стану — я их напрямую не использовал. Про SQLite скажу коротко: я не первый раз работаю с этой базой, и так как у меня нет необходимости размещать БД на отдельном сервере, то я выбрал ее из-за простоты.
Конечно, каждый выбирает инструменты под себя, и тот набор, что описан здесь, не является рекомендацией. Выбирайте то, что больше нравится (имей я возможность вернуться в прошлое — выбрал бы QxOrm вместо ODB).

За работу


Пути


(Русские «пути» и «маршруты» как-то не звучат по сравнению с английским «routes», но что поделать.) В CppCMS каждый запрос обрабатывает отдельное «приложение» — класс-наследник cppcms::application. Каждый путь задается регулярным выражением, которому сопоставляется функция-обработчик, например:
class MyApplication : public cppcms::application
{
public:
    explicit MyApplication(cppcms::service &service) :
        cppcms::application(service)
    {
        dispatcher().assign("/file/\\d+", &MyApplication::handleFile, this, 1);
        mapper().assign("/file", "/file/{1}");
    }
    void handleFile(std::string fileNo)
    {
        //тут обрабатываем запрос
    }
};

cppcms::service — штука, которая отвечает за создание новых экземпляров cppcms::application, к ней мы еще вернемся. А пока рассмотрим две тонкости: приоритет путей и ситуацию, когда URL заканчивается слешем, либо нет.
Имиджборда содержит список досок по адресу "/[a-z]+" (упрощено для наглядности). То есть, например, «site.com/b». А если набрать «site.com/b/»? Все ли будет хорошо? Нет, ничего хорошего не ждите. CppCMS не создает автоматически alias'ов со слешем на конце. И правильно делает. Но, тем не менее, иногда такие alias'ы нужны, и о них не стоит забывать (и добавлять вручную).
Не стоит также забывать и о том, что пути имеют приоритет в соответствии с порядком их добавления: чем раньше добавлен путь, тем выше его приоритет. Поэтому, если написать так:
dispatcher().assign("/.+", &MyApplication::handleAnything, this, 1);
dispatcher().assign("/page", &MyApplication::handlePage, this);

то страница «site.com/page» окажется недоступна, так как ее URL подходит под регулярное выражение "/.+", и обработчик для него установлен с более высоким приоритетом. Правильно было бы назначить боработчики в обратном порядке. Важно помнить об этом моменте.
Теперь к реальному примеру. Как работа с путями организована у меня? Прежде всего представим, что потребовалось добавить какую-то свою странцу со своим URL. Править исходники? Увольте. Вводим поддержку плагинов-фабрик, которые создают список структур, в каждой из которых содержится ругулярное выражение, соответствующая функция-обработчик, количество аргументов (чтобы вызвать нужный метод) и приоритет. Если пути совпадают с уже имеющимися по умолчанию, то пути из плагинов их перезаписывают. Полный код можно посмотреть тут (осторожно, лапша): класс-наследник cppcms::application, пути, интерфейс плагина.
Тут как раз используется моя надстройка над Qt, позволяющая автоматически загружать плагины определенного типа (проверка реализована с помощью отдельного интерфейса) из нескольких папок: общесистемных и пользовательских (например, "/usr/lib/app/plugins" и "/home/user/.app/lib/app/plugins"). Не будем останавливаться на этом.

cppcms::service и конфигурация


Как уже говорилось выше, cppcms::service отвечает за создание новых экземпляров cppcms::application, которые уже в свою очередь обрабатывают запросы, поступающие на сервер. Чтобы cppcms::service мог создавать экземпляры вашего потомка cppcms::application, его нужно зарегистрировать:
service.applications_pool().mount(cppcms::applications_factory<MyApplication>());

cppcms::service блокирует текущий тред и запускает по мере необходимости новые треды, в которых работают экземпляры cppcms::application. Чтобы не мешать работе QCoreApplication (основной класс Qt в консольных приложениях), я запускаю cppcms::service в отдельном треде:
class OlolordWebAppThread : public QThread
{
private:
    const cppcms::json::value Conf;
    cppcms::service *mservice;
    bool mshutdown;
public:
    explicit OlolordWebAppThread(const cppcms::json::value &conf, QObject *parent = 0) :
         QThread(parent), Conf(conf)
    {
        mshutdown = false;
        mservice = 0;
    }
    void shutdown()
    {
        if (!mservice)
            return;
        mshutdown = true;
        mservice->shutdown();
    }
protected:
    void run()
    {
        while (!mshutdown) {
            try {
                cppcms::service service(Conf);
                mservice = &service;
                service.applications_pool().mount(cppcms::applications_factory<OlolordWebApp>());
                service.run();
            } catch(std::exception const &e) {
                qDebug() << e.what();
            }
            mservice = 0;
        }
    }
};

Обратите внимание на три момента: обработку исключений, константу const cppcms::json::value Conf и метод run. В отличие от Qt, CppCMS повсеместно использует исключения. Я не люблю исключения и придерживаюсь философии Qt, когда вместо
try {
    int x = doSomething();
} catch (const Exception &e) {
    //обрабатываем ошибку
}

используется
bool ok = false;
int x = doSomething(&ok);
if (!ok) {
    //обрабатываем ошибку
}

Тем не менее, не забывайте об исключениях — они в CppCMS периодически выбрасываются, и их нужно вовремя перехватывать и обрабатывать.
Почему используется переопределенный метод run вместо рекомендуемого подхода с классом-работником (worker)? Потому, что, если обернуть cppcms::service в потомка QObject, поместить этот класс-работник в тред и вызвать слот, который, в свою очередь, вызовет cppcms::service::run, мы получим только лишь лишние обертки: метод cppcms::service::run все равно заблокирует QEventLoop, потому что он о нем ничего не знает и использует внутри банальный цикл for(;;). Иными словами, приведенный ниже код ничем не будет отличаться от приведенного выше — использовать сигналы и слоты или прервать тред вызовом QThread::quit не получится, так как QEventLoop будет заблокирован.
class Worker : public QObject
{
public:
    cppcms::service service;
public slots:
    void start()
    {
        //остальной код пропущен для краткости
        service.run();
    }
}

int main()
{
    QThread t;
    Worker *w = new Worker;
    w->moveToThread();
    t.start();
    QMetaObject::invokeMethod(w, "start", Qt::QueuedConnection);
}

Для лучшего понимания можно почитать о системе сигналов и слотов, а также о event loop'ах в Qt.
Что же касается const cppcms::json::value Conf, то это — представление JSON-объекта, в данном случае содержащего конфигурацию сервера. Пример конфигурации:
{
    "service": {
        "api": "http",
        "port": 80,
        "ip": "0.0.0.0"
    }
}

«api» — принимает значения «fastcgi», «scgi», либо «http», указывает, является ли приложение самостоятельным (со своим HTTP-сервером), или работает под управлением *CGI.
«port» и «ip» — порт и адрес, которые приложение слушает. «0.0.0.0» означает любой адрес (иначе говоря, сервер будет отвечать на запросы со всех адресов).
Подробности — тут. Хотя рекомендуется использовать *CGI, для моего случая это было бы лишь ненужным усложнением.
Пару слов о том, как получить cppcms::json::value, скажем, из файла. Нужно воспользоваться методом load, но он принимает в качестве параметра std::istream, поэтому я написал вспомогательную функцию:
Скрытый текст
cppcms::json::value readJsonValue(const QString &fileName, bool *ok)
{
    bool b = false;
    QString s = BDirTools::readTextFile(fileName, "UTF-8", &b);
    if (!b)
        return cppcms::json::value();
    cppcms::json::value json;
    std::stringstream in(toStd(s));
    if (json.load(in, true))
        return bRet(ok, true, json);
    else
        return bRet(ok, false, cppcms::json::value());
}

Нужно это все для того, чтобы можно было считывать настройки не только из файла на диске, но и из встроенных в приложение ресурсов (Qt Resource System).

Теперь, наконец, запускаем наше приложение:
int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv); //главный класс Qt
    cppcms::json::value conf = Tools::readJsonValue("/path/to/conf/file", &ok);
    OlolordWebAppThread t(conf);
    t.start();
    int ret = app.exec(); //запуск QEventLoop
    t.shutdown(); //этот метод является потокобезопасным (thread-safe), проблем нет
    t.wait(10 * BeQt::Second); //ожидание, так как тред может завершиться не моментально, если еще остались необработанные запросы
    return ret;
}


Контроллеры и шаблоны


Вот мы и подошли к самому интересному — рендерингу ответов на запросы. Не буду останавливаться на малополезных примерах типа
void MyApplication::main(std::string /*url*/)  
{  
    response().out() << "<html>\n<body>\n<h1>Hello World</h1>\n</body>\n</html>\n";
}

Мы ведь хотим сайт покруче, верно? В CppCMS, чтобы отрендерить страницу, требуется две вещи — шаблон и связанный с ним контроллер (слово «контроллер» я стал использовать своевольно, но, как мне кажется, оно здесь подходит). Шаблон — это файл с содержимым в виде смеси HTML и специального языка CppCMS. Контроллер — класс (или структура) C++, содержащая переменные и функции, на которые ссылается шаблон. Но меньше слов, больше примеров:
//page.h

namespace Content 
{

struct Page : public cppcms::base_content
{
    std::string message;
    std::string pageTitle;
};

}

<!-- page.tmlp -->
<% c++ #include "page.h" %>
<% skin my_skin %>
<% view page uses Content::Page %>
<% template render() %>
<html>
    <head>
        <title><%= pageTitle %></title>
    </head>
    <body>
        <h1><%= message %></h1>
    </body>
</html>
<% end template %>
<% end view %>
<% end skin %>

Здесь мы видим контроллер Content::Page с переменными message и pageTitle, а также шаблон с именем page, использующий контроллер Content::Page. Можно также задавать различные скины (skin), но, поскольку я с ними не имел дела, ничего сказать не могу (и вообще считаю, что внешним видом лучше управлять через CSS, хоть эта технология мне и не нравится).
Обратите внимание, что заголовочный файл, соответствующий контроллеру, должен быть включен директивой
<% c++ #include "page.h" %>

Эта директива позволяет вставлять в месте, где она появляется, произвольный код C++. Для вставки же значения переменной контроллера используется специальная конструкция
<%= variableName %>

Строчка
<% template render() %>

Объявляет функцию, которую затем можно использовать в различных местах:
<% template hr() %>
<hr />
<% end template %>

<% template render() %>
<!-- тут код -->
<% include hr() %>
<!-- еще код -->
<% include hr() %>
<!-- и еще -->
<% end template %>

В результате содержимое функции hr появится во всех местах, куда она была включена. Функции могут содержать внушительное количество кода, который иначе пришлось бы копипастить, что не есть хорошо. Функции также могут принимать параметры.
Также шаблоны поддерживают условные операторы, цикл foreach и еще некоторые возможности. Описания всего этого хватило бы на отдельную статью, поэтому ограничусь ссылкой на документацию и парой замечаний.
Во-первых, шаблоны можно наследовать друг от друга, переопределяя функции (они всегда объявляются как виртуальные). Собственно, в первом примере мы и переопределили функцию render базового шаблона cppcms::base_content.
Во-вторых, не так чтобы замечание, скорее маленькая подсказка: если вам надо вывести в шаблоне значение численной переменной, увеличенное на какое-то число, поможет вот такой странный код (других способов, как я понял, нет):
<% c++ out() << (content.variable + 1); %>

Покажу также, как вызывается рендеринг шаблона из приложения:
void MyApplication::handlePage()
{
    Content::Page c; //c - потому что controller
    c.pageTitle = "My page";
    c.message = "Yarrr!";
    render("page", c);
}

Внутри функция render рендерит (кто бы мог поудмать) шаблон, получает HTML в виде текста и пишет его в response().out().
Ну и напоследок — как запустить генерацию шаблонов (из .pro/.pri файла):
CPPCMS_PROCESSING_COMMAND=$${CPPCMS_PREFIX}/bin/cppcms_tmpl_cc
mac|unix {
    CPPCMS_TEMPLATES=$$files($${PWD}/template/*)
} else:win32 {
    CPPCMS_TEMPLATES=$$files($${PWD}\\template\\*)
}

for(CPPCMS_TEMPLATE, CPPCMS_TEMPLATES) {
    CPPCMS_TEMPLATES_STRING=$${CPPCMS_TEMPLATES_STRING} \"$${CPPCMS_TEMPLATE}\"
}

CPPCMS_PROCESSING_COMMAND=$${CPPCMS_PROCESSING_COMMAND} $${CPPCMS_TEMPLATES_STRING} -o \"$${PWD}/compiled_templates.cpp\"

win32:CPPCMS_PROCESSING_COMMAND=$$replace(CPPCMS_PROCESSING_COMMAND, "/", "\\")

system(python $${CPPCMS_PROCESSING_COMMAND})

SOURCES += compiled_templates.cpp

Используется специальная утилита, а также интерпретатор Python (версии 2.x), полученный файл включается в проект. Предполагается, что файлы шаблонов лежат в подпапке template и имеют расширение .tmpl.

Хранение


Почти любой веб-сайт работает с данными, которые надо как-то размещать в долговременной памяти (читай — на диске). Обычно для этого используются реляционные СУБД, такие, как, скажем, MySQL или SQLite. Весьма удобно использовать механизм, который бы позволял прозрачно для разработчика превращать объекты языка программирования в данные БД и наоборот. Такой механизм называется Object-relational Mapping (ORM), и в моем случае используется его реализация в виде фреймворка ODB.
Как это выглядит? Например, вот так:
PRAGMA_DB(object table("posts"))
class Post
{
public:
    PRAGMA_DB(id auto)
    quint64 id_;
    PRAGMA_DB(not_null)
    QString board_;
    PRAGMA_DB(not_null)
    quint64 number_;
    PRAGMA_DB(not_null)
    QDateTime dateTime_;
    QString text_;
    PRAGMA_DB(not_null)
    QLazySharedPointer<Thread> thread_;
public:
    explicit Post()
    {
        //
    }
private:
    friend class odb::access;
};

Здесь мы объявляем класс Post, представляющий сообщение (или пост) на форуме (или доске). О том, что этот объект должен сохраняться в БД и загружаться из нее, говорит строка PRAGMA_DB(object table(«posts»)) (присутствует также необязательный параметр table(«posts»), явно задающий имя таблицы, иначе бы использовалось имя по умолчанию «Post»). Макрос PRAGMA_DB разворачивается в #pragma db, далее добавляются его аргументы. Макрос используется, чтобы компилятор не выдавал предупреждений, встречая незнакомый синтаксис #pragma.
Если над переменной класса добавить такой же макрос, можно сообщить ODB дополнительную информацию об этой переменной — то, как она должна храниться в БД (например, что переменная является идентификатором и соответствующее поле должно быть объявлено как PRIMARY KEY). Увы, произвольные constraint'ы указать нет возможности, а очень хотелось бы, скажем, объявить для таблицы что-то вроде UNIQUE(fieldOne, fieldTwo, fieldThree), то есть указать уникальность по нескольким полям, а не по одному.
Можно в качестве типа переменной использовать другой класс, помеченный PRAGMA_DB. Также, как вы заметили, можно указывать в качестве типа переменной классы Qt. Для этого требуется библиотека odb-qt. Наконец, переменная, чей тип обернут в QLazySharedPointer, не инициализируется сразу при запросе к БД, а подгружается отдельным запросом позднее, если потребуется (Lazy fetch).
Необходимо также объявить odb::access дружественным классом.
А вот так выглядит сохранение и загрузка объектов (в случае SQLite):
try {
    odb::database *db = new odb::sqlite::database("/path/to/db",
        SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); //создаем соединение с БД
    odb::transaction t = odb::transaction(db.begin()); //начинаем транзакцию
    //создаем схему
    t->execute("PRAGMA foreign_keys=OFF");
    odb::schema_catalog::create_schema(*db);
    t->execute("PRAGMA foreign_keys=ON");
    Post p;
    //инициализируем переменные поста
    db->perist(p); //сохраняем пост в базе
    odb::result<Post> r(db->query<Post>()); //запрашиваем список всех постов
    for (odb::result_iterator<Post> i = r.begin(); i != r.end(); ++i) {
        //делаем что-то с полученным списком постов, например:
        i->dateTime_ = QDateTime::currentDateTimeUtc(); //задаем время
        db->update(*i); //и сохраняем изменения
    }
    t.commit(); //завершаем транзакцию
} catch (const odb::exception &e) {
    //обрабатываем исключение
}

По-хорошему надо еще позаботиться об удалении указателя на odb::database, но я не стал перегружать код (используйте для этого scoped pointer).
Стоит заметить, что функция odb::schema_catalog::create_schema не проверяет, есть ли уже таблицы в базе, то есть выполняет CREATE TABLE ... вместо CREATE TABLE IF NOT EXISTS ..., так что проверять нужно вручную. Естественно, ни о каком автоматическом создании схемы и речи не идет — все вручную. И это еще один камень в огород ODB. Впрочем, не считая некоторых костылей, библиотека со своей задачей справляется.
ODB, как и CppCMS, требуется свой мета-компилятор, обрабатывающий PRAGMA_DB и генерирующий код C++. Запускается он так:
mac|unix {
    ODB_PROCESSING_COMMAND=$${ODB_PREFIX}/bin/odb
    ODB_TEMPLATES=$$files($${PWD}/*.h)
} else:win32 {
    ODB_PROCESSING_COMMAND=$${ODB_PREFIX}/bin/odb.exe
    ODB_TEMPLATES=$$files($${PWD}\\*.h)
}

for(ODB_TEMPLATE, ODB_TEMPLATES) {
    ODB_TEMPLATES_STRING=$${ODB_TEMPLATES_STRING} \"$${ODB_TEMPLATE}\"
}

ODB_PROCESSING_COMMAND=$${ODB_PROCESSING_COMMAND} -d sqlite --generate-query --generate-schema --profile qt
ODB_PROCESSING_COMMAND=$${ODB_PROCESSING_COMMAND} -I \"$${QMAKE_INCDIR_QT}\"
ODB_PROCESSING_COMMAND=$${ODB_PROCESSING_COMMAND} -I \"$${QMAKE_INCDIR_QT}/QtCore\" $${ODB_TEMPLATES_STRING}

win32:ODB_PROCESSING_COMMAND=$$replace(ODB_PROCESSING_COMMAND, "/", "\\")

system($${ODB_PROCESSING_COMMAND})

HEADERS += $$files($${PWD}/*.hxx)
SOURCES += $$files($${PWD}/*.cxx)

Компилятор поставляется отдельно от библиотеки в виде бинарника. При запуске нужно указать список файлов, тип БД (в моем случае SQLite), а также то, что используется профиль Qt и что нужно сгенерировать схему. Плюс указываются пути к заголовочным файлам Qt. Компилятор генерирует .hxx и .cxx файлы, добавляя суффикс "-odb" к исходным именам.
Теперь пару слов о моем проекте. Поскольку ODB требует активной транзакции при любой операции, я решил обернуть связку odb::database и odb::transaction в один класс Transaction, который хранит указатель на активную в данный момент транзакцию и соответствующее ей соединение с БД. Одновременно в каждом треде может существовать не более одной активной транзакции. При создании экземпляра класса-обертки (Transaction), если транзакции еще нет, создается новое соединение и начинается транзакция, если же активная транзакция уже есть, увеличивается внутренний счетчик. При разрушении Transaction настоящая odb::transaction не будет завершена до тех пор, пока счетчик не обнулится. То есть, пока мы не дойдем до дна стека, где находится созданный первым экземпляр Transaction, мы будем находиться внутри одной и той же транзакции, ссылающейся на одно и то же соединение с базой. Очень удобно. Исходники тут: 1, 2. Примеры использования: 1, 2.
Для ленивых урезанный пример под спойлером
bool createPost(CreateThreadParameters &p, QSharedPointer<Thread> thread)
{
    try {
        Transaction t; //этот объект на вершине стека
        Post post(p, thread);
        t->persist(post);
        t.commit();
        return true;
    } catch (const odb::exception &e) {
        qDebug() << e.what();
        return false;
    }
}

bool createThread(CreateThreadParameters &p)
{
    try {
        Transaction t; //этот объект на дне стека
        QSharedPointer<Thread> thread(new Thread(p));
        t->persist(thread);
        if (!createPost(p, thread))
            return bfalse;
        t.commit();
        return true;
    } catch (const odb::exception &e) {
        qDebug() << e.what();
        return false;
    }
}

Оба объекта Thread ссылаются на одну и ту же транзакцию и на одно и то же соединение с базой.


Подсветка синтаксиса и проверка типа файлов


Тут каких-то особых замечаний нет, приведу просто код:
QString mimeType(const QByteArray &data, bool *ok)
{
    if (data.isEmpty())
        return bRet(ok, false, QString());
    magic_t magicMimePredictor;
    magicMimePredictor = magic_open(MAGIC_MIME_TYPE);
    if (!magicMimePredictor)
        return bRet(ok, false, QString());
    if (magic_load(magicMimePredictor, 0)) {
        magic_close(magicMimePredictor);
        return bRet(ok, false, QString());
    }
    QString result = QString::fromLatin1(magic_buffer(magicMimePredictor, (void *) data.data(), data.size()));
    return bRet(ok, !result.isEmpty(), result);
}

QString highlight(const QString &code, const QString &lang)
{
    std::istringstream in(Tools::toStd(code));
    std::ostringstream out;
    try {
        srchilite::SourceHighlight sourceHighlight("html.outlang");
        sourceHighlight.setDataDir("/path/to/definition/files");
        sourceHighlight.highlight(in, out, lang.toLatin1().data() + ".lang");
    } catch (const srchilite::ParserException &e) {
        qDebug() << e.what();
        return "";
    } catch (const srchilite::IOException &e) {
        qDebug() << e.what();
        return;
    } catch (const std::exception &e) {
        qDebug() << e.what();
        return;
    }
    return + QString::fromLocal8Bit(out.str());
}

При определении типа файла самую малость играемся с типом указателей, при подсветке синтаксиса указываем путь к папке с файлами определений синтаксиса и язык исходников.

Я бы хотел рассказать еще о многом, например, как я реализовывал поддержку разных видов капчи и прикрепление более одного файла, обработку статического содержимого, сохранение файлов, кеширование… список можно продолжить. Но, боюсь, в рамках одной статьи я это сделать не смогу, увы мне. Если это будет кому-то интересно, то я обязательно расскажу обо всем перечисленном и много о чем еще в следующих частях.

Ну а теперь — обещанная диванная аналитика. Сперва, признаться, я поддался общему мнению о том, что C++ не подходит для веба. При этом я особо не задумывался, почему именно. Раз все так говорят, то, наверное, так и есть. Это было несколько лет назад, когда я понятия не имел, для чего нужен JavaScrpt, что такое AJAX и почему надо использовать CSS.
Но шло время, я поковырял Django, Ruby on Rails, поработал какое-то время с Java, создавая один крупный веб-сайт, набрался опыта, изучил новые технологии. И я понял, что, на самом то деле, какой бы язык ни использовался в бэкэнде, фронтенд все равно будет представлять из себя те же HTML, CSS и JavaScript. Как ни крути, надо писать шаблоны страниц, создавать стили, программировать более сложное поведение на JS. И все это вообще никак не связано с бэкэндом.
Велика ли разница между, скажем, Thymeleaf (Java) и CppCMS при работе с шаблонами? Не слишком. Все тот же язык шаблонов, только синтаксис немного отличается. Рендерится же все точно так же, вызовом функции рендера из кода. И контроллеры есть и там, и там, не важно как они называются.
А хранение данных? Чем ODB принципиально отличается от Hibernate? Да, возможности местами скромнее, но значит ли это, что ODB вовсе не подходит для ORM? Я так не считаю.
Ну и так далее. Фронтенд остается фронтендом, в бэкенде же мы на любом языке делаем все то же самое. Так есть ли разница? Получается, что нет. На Java или Python работа с БД не будет чем-то принципиально отличаться от работы с БД на C++, это же касается и того, что модно называть «бизнес-логикой», то есть основной логикой приложения. Все те же проверки, условные операторы, иерархия классов/функций, только синтаксис у каждого языка свой.
Не получится, использовав Python, избавиться от необходимости писать на JS AJAX-запросы, или перестать обращаться к базе. Таких чудес не бывает. Кто-то, возможно, скажет, что работать с БД на %имя_языка% проще, чем на C++, и будет отчасти прав, но лишь отчасти: чудес, повторюсь, не бывает, если надо получить из базы объект, для этого надо написать нечто вроде Object o = db->query("..."); — на любом языке.
То есть, получается, что ответ на вопрос «Но зачем?» остается тем же: «А почему нет?», меняется лишь его смысл. Таковы мои наблюдения, основанные на личном опыте написания веб-приложений на различных языках (C++, Java, в меньшей степени — Python, Ruby). И это не призыв к холивару, а желание его самым мирным образом разрешить.

За сим откланиваюсь, а также оставляю ссылку на исходники проекта: github.com/ololoepepe/ololord
Ну и, разумеется, приглашаю к обсуждению. Писали ли вы веб-приложения на C++? Слышали ли аргументированные обоснования, почему это плохо? Поделитесь своим опытом.
Tags:
Hubs:
+39
Comments 85
Comments Comments 85

Articles