Quelques cadriciels Web C++ (1/2)

Posté par  (site web personnel) . Édité par Davy Defaud, ZeroHeure, Julien Jorge, palm123, Benoît Sibaud, bubar🦥 et claudex. Modéré par ZeroHeure. Licence CC By‑SA.
Étiquettes :
34
9
déc.
2018
C et C++

Actuellement, il existe de nombreux langages et cadriciels (frameworks) intéressants pour le développement Web côté serveur. Dans ce domaine, le C++ n’est pas le langage le plus à la mode, mais il possède cependant des atouts intéressants. En effet, le C++ dispose de nombreuses bibliothèques (dont des cadriciels Web), il est réputé pour ses performances, enfin ses dernières normes le rendent plus agréable à utiliser.

L’objectif de cet article est de donner un aperçu des outils C++ disponibles pour le développement Web back‐end, à partir d’un exemple d’application. Les codes sources présentés ici sont disponibles sur ce dépôt Git. Les différents cadriciels utilisés sont résumés en annexe (partie 2). Enfin, une liste de bibliothèques C++ est disponible sur Awesome C++.

Partie 1 : exemple d’application, génération de HTML et accès à une base de données.

Sommaire

Exemple d’application

Application finale

On veut implémenter une application qui permet d’afficher des images d’animaux stockées sur le serveur. Un formulaire permet d’indiquer le début du nom des animaux à afficher. On peut afficher l’image complète en cliquant sur la vignette et l’on peut afficher une page d’information via un lien en bas de page. Les données des animaux (noms et fichiers) sont stockées dans une base SQLite sur le serveur.

application finale

Ici, la génération des pages HTML est effectuée sur le serveur, même si la tendance actuelle est plutôt de proposer une API côté serveur et de générer le code HTML côté client.

Architecture MVC

De façon très classique, on peut organiser le code de cette application selon une architecture de type MVC, c’est‐à‐dire en distinguant les données (modèle), leur affichage (vue) et leur gestion (contrôleur).

Pour notre application, les images sont disponibles sur le serveur et on utilise une base de données SQLite contenant une table avec les noms et fichiers des animaux. Fichier animals.sql :

CREATE TABLE animals (
  id INTEGER PRIMARY KEY, 
  name TEXT,
  image TEXT 
);

INSERT INTO animals (name, image) VALUES('dolphin', 'dolphin-marine-mammals-water-sea-64219.jpg');
INSERT INTO animals (name, image) VALUES('dog', 'night-garden-yellow-animal.jpg');
INSERT INTO animals (name, image) VALUES('owl', 'owl.jpg');

La partie modèle se résume alors à un type Animal et à une fonction getAnimals qui interroge la base de données et retourne les enregistrements de type Animal dont le nom commence par le préfixe donné. Fichier Animal.hpp :

#include <string>
#include <vector>

// Animal datatype
struct Animal {
  std::string name;
  std::string image;
};

// query database (select animals whose name begins with myquery)
std::vector<Animal> getAnimals(const std::string & myquery);

La partie vue contient deux fonctions retournant des pages au format HTML : renderAbout retourne la page d’information et renderHome retourne la page principale avec les animaux demandés par l’utilisateur. Fichier View.hpp :

#include "Animal.hpp"

// render the about page to HTML
std::string renderAbout();

// render the home page to HTML
std::string renderHome(
    const std::string & myquery, 
    const std::vector<Animal> & animals);

Enfin la partie contrôleur récupère les événements du client puis met à jour le modèle et la vue. Pour notre application, il n’y a pas de traitement compliqué à réaliser, juste à récupérer les requêtes HTTP et à appeler les fonctions précédentes.

Exemple en JavaScript

Avant de voir comment développer cette application en C++, voici une implémentation possible en JavaScript, basée sur le classique cadriciel Node.js.

Pour accéder à la base de données, on peut utiliser le paquet better-sqlite3. Il suffit d’ouvrir la base de données, d’exécuter une requête SQL et de récupérer les données au format JSON. Fichier animals-nodejs/src/animals.js :

"use strict";

const db = require("better-sqlite3")("animals.db");

// query database (select animals whose name begins with myquery)
exports.getAnimals = myquery =>
    db.prepare("SELECT name,image FROM animals WHERE name LIKE ?||'%'").all(myquery);

Pour la vue, le paquet pug permet de générer du code HTML à partir d’une chaîne de caractères, en utilisant un formatage particulier. Ceci apporte plusieurs avantages : le formatage utilisé est plus concis à écrire que du code HTML, il n’y a pas de risque d’oublier de fermer une balise HTML, on peut traiter facilement des données d’entrée, par exemple la liste des animaux à afficher… Fichier animals-nodejs/src/view.js :

"use strict";

const pug = require('pug');

// render the about page to HTML
const aboutFunc = pug.compile(`
doctype html
html
  head
    link(rel="stylesheet", type="text/css", href="static/style.css")
  body
    h1 About (Node.js)
    p Generated by 
        a(href="https://nodejs.org/en/") Node.js
        | , 
        a(href="https://expressjs.com/") Express
        | , 
        a(href="https://github.com/JoshuaWise/better-sqlite3") Better-sqlite3
        |  and  
        a(href="https://pugjs.org/api/getting-started.html") Pug
    a(href="/") Home
`);
exports.renderAbout = aboutFunc;

// render the home page to HTML
const homeFunc = pug.compile(`
doctype html
html
  head
    link(rel="stylesheet", type="text/css", href="static/style.css")
  body
    h1 Animals (Node.js)
    form(action="/", method="get")
        input(name="myquery", value=myquery)
    each animal in animals
        a(href="static/"+animal.image)
            div(class="divCss")
                p= animal.name
                img(src="static/"+animal.image, class="imgCss")
    p(style="clear:both")
        a(href="/about") About
`);
exports.renderHome = (myquery, animals) => homeFunc(myquery, animals);

Cependant Pug ne vérifie pas les types de balises. Par exemple, si l’on demande une balise <toto>, Pug génèrera bien le code <toto> … </toto>, alors que cette balise n’existe pas dans la norme HTML.

Enfin, on utilise le très classique express, pour lancer un serveur avec routage des requêtes HTTP. Fichier animals-nodejs/src/app.js :

"use strict";

const port = 3000;

const view = require("./view.js")
const animal = require("./animal.js")

const express = require("express");
const app = express();

// serve the about page
app.get("/about", function (request, response) {
    const html = view.renderAbout();
    response.send(html);
});

// serve the home page (and filter the animals using the myquery parameter)
app.get("/", function (request, response) {
    const myquery = request.query.myquery ? request.query.myquery : ""
    const animals = animal.getAnimals(myquery);
    const html = view.renderHome({myquery, animals});
    response.send(html);
});

// serve static files (located in the "static" directory)
app.use("/static", express.static("./static"));

// run a server listening on port 3000
app.listen(port, function () {
    console.log(`Listening on port ${port}...`);
});

À noter que Node.js est un cadriciel asynchrone, c’est‐à‐dire que des fonctions peuvent être appelées de façon non bloquante. Ceci permet d’optimiser les performances de l’application générale, au prix d’un peu de complexité de programmation (promise/callback, async/await…). Pour notre application, cela n’a pas vraiment d’influence, car la principale fonction potentiellement concernée (la fonction d’accès à la base de données, via better-sqlite3) est bloquante.

Exemple en Haskell

L’application est également simple à implémenter en Haskell, avec le cadriciel scotty.

Pour le modèle, on définit un type Animal et une fonction d’accès à la base de données par requête SQL via sqlite-simple. Fichier animals-scotty/src/Animal.hs :

{-# LANGUAGE OverloadedStrings #-}

module Animal where

import qualified Data.Text.Lazy as L
import qualified Database.SQLite.Simple as SQL 
import           Database.SQLite.Simple.FromRow (FromRow, fromRow, field)

-- Animal datatype
data Animal = Animal 
    { animalName :: L.Text
    , animalImage :: L.Text
    } deriving Show

-- deserialize an Animal from the database
instance FromRow Animal where
    fromRow = Animal <$> field <*> field 

-- query database (select animals whose name begins with myquery)
getAnimals :: L.Text -> IO [Animal]
getAnimals myquery = do
    let req = "SELECT name,image FROM animals WHERE name LIKE ?||'%'" 
    SQL.withConnection "animals.db" 
        (\conn -> SQL.query conn req (SQL.Only myquery))

Pour la génération du code HTML et du code CSS, Haskell dispose de DSL (Domain Specific Languages), ici lucid et clay. Ceci permet non seulement d’assurer le formatage correct des balises mais également que ces balises sont bien correctes. Ainsi, si l’on essaie de générer une balise <toto> (qui n’existe pas dans la norme HTML), le compilateur indiquera une erreur. Fichier animals-scotty/src/View.hs :

{-# LANGUAGE OverloadedStrings #-}

module View where

import           Animal
import qualified Clay as C
import           Control.Monad(forM_)
import           Lucid
import qualified Data.Text.Lazy as L

-- render the about page to HTML
aboutPage :: L.Text
aboutPage = renderText $ html_ $ do
    head_ $ style_ $ L.toStrict $ C.render $ myCss
    body_ $ do
        h1_ "About (Scotty)"
        p_ $ do 
            "Generated by "
            a_ [href_ "http://hackage.haskell.org/package/scotty"] "Scotty"
        p_ $ a_ [href_ "/"] "Home"

-- render the home page to HTML
homePage :: L.Text -> [Animal] -> L.Text
homePage myquery animals = renderText $ html_ $ do
    head_ $ style_ $ L.toStrict $ C.render $ myCss
    body_ $ do
        h1_ "Animals (Scotty)"
        -- add the HTML form
        form_ [action_ "/", method_ "get"] $ do
            input_ [name_ "myquery", value_ $ L.toStrict myquery]
        -- add every animal in a HTML div
        forM_ animals $ \ animal -> do
            let img = L.toStrict $ L.concat ["./img/", animalImage animal]
            a_ [href_ img] $ div_ [class_ "divCss"] $ do
                p_ $ toHtml $ animalName animal
                img_ [src_ img, class_ "imgCss"]
        p_ [style_ "clear: both"] $ a_ [href_ "/about"] "About"

-- our CSS styles
myCss :: C.Css
myCss = do
    C.a C.# C.byClass "aCss" C.? do
        C.textDecoration  C.none
        C.color           C.inherit
    C.body C.? do
        C.backgroundColor  C.azure
    C.div C.# C.byClass "divCss" C.? do
        C.backgroundColor  C.beige
        C.border           C.solid (C.px 1) C.black
        C.margin           (C.em 1) (C.em 1) (C.em 1) (C.em 1)
        C.width            (C.px 320)
        C.textAlign        C.center
        C.float            C.floatLeft
    C.img C.# C.byClass "imgCss" C.? do
        C.width            (C.px 320)
        C.height           (C.px 240)

Enfin, le serveur principal se résume à router les requêtes HTTP en utilisant les fonctions du cadriciel scotty. Fichier animals-scotty/src/Main.hs :

{-# LANGUAGE OverloadedStrings #-}

import           Animal
import           View
import           Control.Monad.Trans (liftIO) 
import           Network.Wai.Middleware.RequestLogger (logStdoutDev)
import           Network.Wai.Middleware.Static (addBase, staticPolicy)
import           Web.Scotty (get, html, middleware, param, rescue, scotty) 

-- run a server listening on port 3000
main = scotty 3000 $ do

    -- show logs
    middleware logStdoutDev

    -- serve the about page
    get "/about" $ html aboutPage

    -- serve the home page (and filter the animals using the myquery parameter)
    get "/" $ do
        myquery <- param "myquery" `rescue` (\_ -> return "")
        animals <- liftIO $ getAnimals myquery
        html $ homePage myquery animals

    -- serve static files (located in the "static" directory)
    middleware $ staticPolicy $ addBase "static"

Par conception, Haskell permet également de faire de l’asynchrone assez facilement (langage fonctionnel pur, environnement d’exécution supportant les green threads…).

Au final, l’implémentation Haskell est assez similaire à l’implémentation JavaScript. La principale différence est que Haskell fait du typage statique, ce qui allonge légèrement le code mais permet de détecter les erreurs de typage plus précocement.

Génération de HTML

Les générateurs de documents HTML

C++ ne semble pas avoir d’outils de génération de documents HTML aussi aboutis que Lucid en Haskell. La bibliothèque CTML permet de définir la structure arborescente d’un document puis d’en générer le code HTML correspondant. Cependant, sa syntaxe est assez verbeuse et il n’y a pas de vérification des balises. Exemple d’utilisation (animals-cpprestsdk/src/View.cpp) :

const string css = R"(
    body {
      background-color: azure;
    }
    ...
)";

string renderHome(const string & myquery, const vector<Animal> & animals) {

    CTML::Document doc;

    doc.AddNodeToHead(
            CTML::Node("style", css));

    doc.AddNodeToBody(
            CTML::Node("h1", "Animals (Cpprestsdk)"));

    doc.AddNodeToBody(
            CTML::Node("form")
            .AppendChild(
                CTML::Node("input")
                .UseClosingTag(false)
                .SetAttribute("type", "text")
                .SetAttribute("name", "myquery")
                .SetAttribute("value", myquery)));

    for (const Animal & animal : animals)
        doc.AddNodeToBody(
                CTML::Node("a.aCss")
                .SetAttribute("href", "static/"+animal.image)
                .AppendChild(
                    CTML::Node("div.divCss")
                    .AppendChild(
                        CTML::Node("p", animal.name))
                    .AppendChild(
                        CTML::Node("img.imgCss")
                        .UseClosingTag(false)
                        .SetAttribute("src", "static/"+animal.image))));

    doc.AddNodeToBody(
            CTML::Node("p")
            .SetAttribute("style", "clear:both")
            .AppendChild(
                CTML::Node("a", "About")
                .SetAttribute("href", "/about")));

    return doc.ToString(CTML::Readability::MULTILINE);
}

string renderAbout() {
    ...
}

Les systèmes de patrons

Ces systèmes consistent à écrire des patrons (templates) paramétrables, c’est‐à‐dire du code HTML dans lequel on utilise des paramètres qui seront remplacés par les valeurs indiquées lors du rendu du patron.

Les cadriciels MVC proposent généralement des systèmes de patrons évolués, mais il existe également des outils indépendants, par exemple mustache. Mustache est un formalisme qui possède des implémentations dans de nombreux langages, dont plusieurs en C++. Par exemple, animal-pistache/src/View.cpp utilise l’implémentation kainjow mustache et le code suivant (animals-crow/src/View.cpp) l’implémentation du cadriciel crow :

const string css = ...

string renderHome(const string & myquery, const vector<Animal> & animals) {

  // create the template 
  const string homeTmpl = R"(
    <html>
      <head>
        <style>
          {{mycss}}
        </style>
      </head>
      <body>
        <h1>Animals (Crow)</h1>
        <form>
          <p> <input type="text" name="myquery" value="{{myquery}}"> </p>
        </form>
        {{#animals}}
        <a href="static/{{image}}">
            <div class="divCss">
              <p> {{name}} </p>
              <img class="imgCss" src="static/{{image}}" />
            </div>
          </a>
        {{/animals}}
        <p style="clear: both"><a href="/about">About</a></p>
      </body>
    </html>
  )";

  // create a context containing the data to use in the template
  crow::mustache::context ctx;
  ctx["mycss"] = css;
  ctx["myquery"] = myquery;
  for (unsigned i=0; i<animals.size(); i++) {
    ctx["animals"][i]["name"] = animals[i].name;
    ctx["animals"][i]["image"] = animals[i].image;
  }

  // render the template using the context
  return crow::mustache::template_t(homeTmpl).render(ctx);
}

string renderAbout() {
    ...
}

Génération « à la main »

Il est également relativement simple de générer du code HTML manuellement, en utilisant les flux de chaînes C++. Cependant, cette méthode ne facilite pas la réutilisation de code ni la vérification du code HTML produit. Exemple de génération manuelle (animals-silicon/src/main.cpp) :

string renderHome(const string & myquery, const vector<Animal> & animals) {

  // create a string stream
  ostringstream oss;

  // generate some HTML code, in the stream
  oss << R"(
    <html>
      <head>
        <link rel="stylesheet" type="text/css" href="mystatic/style.css">
      </head>
      <body>
        <h1>Animals (Silicon)</h1>
        <form>
          <p> <input type="text" name="myquery" value=")" << myquery << R"("> </p>
        </form>
    )";

  for (const Animal & a : animals) {
    oss << R"(
        <a href="mystatic/)" << a.image << R"(">
            <div class="divCss">
              <p>)" << a.name << R"(</p>
              <img class="imgCss" src="mystatic/)" << a.image << R"(" />
            </div>
          </a>)";
  }

  oss << R"(
        <p style="clear: both"><a href="/about">About</a></p>
      </body>
    </html>
  )";

  // return the resulting string
  return oss.str();
}

string renderAbout() {
    ...
}

Accès à une base de données SQL

Les connecteurs SQL

Ils permettent de construire explicitement des requêtes SQL, de les envoyer au système de base de données et d’en récupérer le résultat. Les connecteurs SQL sont généralement faciles à utiliser (il suffit de connaître le langage SQL) mais ils ne vérifient pas que les requêtes sont correctes.

De nombreux cadriciels proposent des connecteurs SQL. Par exemple, cppcms (voir animals-cppcms/src/Animal.cpp), tntnet (voir animals-tntnet/src/Animal.cc) et silicon (voir animals-silicon/src/main.cpp). Il existe également des connecteurs indépendants, par exemple sqlite_modern_cpp (voir animals-pistache/src/Animal.cpp) :

#include "Animal.hpp"
#include <sqlite_modern_cpp.h>

using namespace sqlite;
using namespace std;

vector<Animal> getAnimals(const string & myquery) {

  vector<Animal> animals;

  try {
    // open database
    database db("animals.db");

    // query database and process results
    db << "SELECT name,image FROM animals WHERE name LIKE ?||'%'" 
      << myquery
      >> [&](string name, string image) { animals.push_back({name, image}); };
  }
  catch (exception & e) {
    cerr << e.what() << endl;
  }

  return animals;
}

Les ORM

Les ORM (Object‐Relational Mapping) permettent de convertir des données d’une table d’une base vers une classe C++, et réciproquement. Ceci permet d’utiliser la base de façon plus sûre, car les données sont vérifiées par le système de typage et vérifiées à la compilation puisque les requêtes sont réalisées par des fonctions C++. Cependant, un ORM définit sa propre couche d’abstraction équivalente au SQL, mais forcément moins connue.

Il existe différents ORM C++, par exemple wt (voir animals-wt/src/main.cpp), sqlpp11 (voir animals-crow/src/Animal.cpp) ou sqlite_orm (voir animals-cpprestsdk/src/Animal.cpp) :

#include "Animal.hpp"
#include <sqlite_orm/sqlite_orm.h>

using namespace std;
using namespace sqlite_orm;

vector<Animal> getAnimals(const string & myquery) {

    vector<Animal> animals;

    // open database and map the "animals" table to the "Animal" datatype
    auto storage = make_storage(
            "animals.db",
            make_table("animals",
                make_column("name", &Animal::name),
                make_column("image", &Animal::image)));

    // query database
    auto results = storage.get_all<Animal>(where(like(&Animal::name, myquery+"%")));

    // process results
    for(auto & animal : results)
        animals.push_back(animal);

    return animals;
}

Suite en partie 2 (cadriciels Web), dans laquelle il sera question de micro‐cadriciels, tels que Sinatra et Flask pour le Ruby et le Python, et de Crow et Silicon pour le C++.

Aller plus loin

  • # Merci

    Posté par  (site web personnel) . Évalué à 3.

    J'ai adoré cet article, j'ai déjà hates de voir la suite !
    C'est vraiment chouette de voir ce qui existe en techno "web" avec mon language favori :)
    Avec les récents articles sur Wt ça montre bien que le C++ est un language qui peut s'adapter au web, c'est rafraichissant !

    J'ai notamment beaucoup aimé crow qui est très simple à prendre en main et sqlite_modern_cpp ressemble à une friandise de syntaxe !

  • # Alternatives

    Posté par  (site web personnel) . Évalué à 8.

    Personnellement, pour le projet d'un client, j'ai choisi de faire toute l'interface Web côté navigateur avec Vue.js, le serveur en C++ ne se contentant que de fournir une API REST en JSON. Du coup, ça fait beaucoup moins de bazar à gérer du côté C++…

    Côté serveur, j'ai choisi le micro serveur CivetWeb largement suffisant pour mon application embarquée, sur lequel j'ajoute une petite surcouche C++ (fournie, en partie d'ailleurs).

    • [^] # Re: Alternatives

      Posté par  . Évalué à 2. Dernière modification le 11 décembre 2018 à 00:34.

      Salut l'ami,
      il semble que la ou je travaille nous sommes arrivés à la même solution technique.
      Nous développons une application qui au départ est codée en c++/wxwidgets et nous migrons peu à peu vers vuejs avec un dialogue en websocket entre la logique c++ et l'interface vuejs.
      La partie vuejs est chargée à travers le composant WebView de wxWidgets (wxWebView).
      Pour le serveur en théorie il y a nginx mais en fait on ne l'utilise pas et on va charger directement le .html.

      • [^] # Re: Alternatives

        Posté par  (site web personnel) . Évalué à 3.

        Cette architecture est intéressante car elle rend le code serveur C++ agnostique ;il devient serveur TCP/IP générique et on peut imaginer une foule de clients pouvoir se connecter dessus, quelque soit le langage ou la technologie. Not only HTML …

    • [^] # Re: Alternatives

      Posté par  (site web personnel) . Évalué à 4. Dernière modification le 11 décembre 2018 à 01:11.

      Oui effectivement, c'est un peu la mode actuelle de réduire la partie serveur à une API JSON et de gérer le reste côté client via des frameworks comme Vue.js ou React. C'est d'ailleurs pour ça que j'ai précisé dans l'article : "même si la tendance actuelle est plutôt de proposer une API côté serveur et de générer le code HTML côté client".
      Les frameworks C++ proposent souvent des fonctionnalités pour gérer du JSON mais le but ici était plutôt de voir, pour une application donnée, les différents outils disponibles côté backend : frameworks asynchrones, frameworks basés templates (tntnet), frameworks basés widgets (wt), etc. Cf la partie 2…

      • [^] # Re: Alternatives

        Posté par  (site web personnel) . Évalué à 2.

        Yep, c'est toujours bien d'avoir un aperçu de ce qui existe.

        Mon avis tout à fait personnel est que le rendu côté client va gagner la bataille.

        Il y a quelques framework modernes qui me font de l'oeil (cpprestsdk, Pistache.io …), vas-tu les tester ?

        • [^] # Re: Alternatives

          Posté par  (site web personnel) . Évalué à 2. Dernière modification le 11 décembre 2018 à 16:23.

          Oui, la partie 2 contient une section sur Pistache (avec une petite illustration du fonctionnement asynchrone) et il y a une version avec Cpprestsdk sur le dépôt git. Pistache a un système description de service REST qui a l'air très intéressant mais je ne l'ai pas testé.

  • # Tntnet

    Posté par  . Évalué à 5.

    Bonjour,

    Personnellement, j'utilise le cadriciel tntnet. Il fournit entre autres le compilateur ecppc qui permet de créer des pages HTML, XML, etc qui seront compilées et intégrées à l'exécutable. C'est aussi facile à utiliser que PHP ou pour les connaisseurs HTML::Mason.

    Concernant l'accès aux bases de données, l'auteur propose tntdb. Ce n'est pas un ORM mais tout le monde n'apprécie pas forcement les ORM :)

    Après, comme c'est du C++, toutes les bibliothèques C++ tierces peuvent être utilisées.

    Ce cadriciel mériterait d'être plus connu.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.