Créer un WebService en C++ avec Qt

Un petit article pour évoquer la possibilité de faire un webservice, en C++/Qt.

Mon besoin est simple, je veux fournir une interface web pour réaliser des lancer de dés avec le système de lancement de dés DiceParser, issu de Rolisteam.

DiceParser est écrit en C++ avec Qt5. A partir de ce constat, je pouvais soit faire un serveur web en C++, soit faire un site web en python/php/whatever pour lancer une commande système et utiliser dice, le client en ligne de commande de DiceParser.

La deuxième solution est  la plus facile mais clairement un peu «sale» et niveau sécurité ce n’est pas idéal. Le «challenge» se trouve dans la première méthode.
Je n’avais pas envie de réinventer la roue donc j’ai cherché une solution technique pour satisfaire mon besoin. Un composant C++/Qt qui permet de créer un serveur http avec possibilité d’être notifier à chaque requête. J’ai trouvé un composant qui réalise cela.

Le dépôt git du composant : https://github.com/azadkuh/qhttp.git

Implémentation du serveur


m_server = new qhttp::server::QHttpServer(this);

m_server->listen(  // listening on 0.0.0.0:8080
    QHostAddress::Any,
    port,
    [=](qhttp::server::QHttpRequest* req, qhttp::server::QHttpResponse* res)
    {
        req->collectData(1024);

        // Ici mettre le code d’analyse de la requête
    });

if (!m_server->isListening())
{
    qDebug() << "Failed to listen";
}
else
{
    qDebug() << "Server is on!";
}

Nous créons une instance du serveur, nous démarrons l’écoute sur le port (80 par défaut pour le protocole http).
L’écoute est réalisé par une fonction lambda, cela n’a rien d’obligatoire, mais c’est plus minimaliste.

Notre fonction est notifiée à la réception d’une requête http.

Récupérer les informations d’une requête


// Récupère les données
req->collectData(1024);

// Récupère l'URL et les paramètres GET
const QUrl url = req->url();
const QUrlQuery query(url);

// Récupère les arguments
QHash<QString, QString> hashArgs;

for (const auto& item : query.queryItems())
{
    hashArgs.insert(item.first, item.second);
}

// Recherche la présence de la commande "cmd"
if (hashArgs.contains("cmd"))
{
    // Répondre à la requête reconnue.
}

Ici, nous analysons la requête pour identifier les actions à réaliser. Nous cherchons le paramètre cmd, pour trouver la commander à identifier. Le travail préalable est des créer une tableau associatif (dans une QHash) pour conserver les paramètres et leur valeur.

Une fois la commande de dés identifiés, il faut l’exécuter.

Exécuter la commande et répondre

// Parse the command
const QString command = hashArgs.value("cmd");
const QString result = startDiceParsing(command);

// HTTP response
res->setStatusCode(qhttp::ESTATUS_OK);

res->addHeader("Content-Type", "text/html; charset=utf-8");
res->addHeader("Access-Control-Allow-Origin", "*");
res->addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
res->addHeader("Access-Control-Allow-Headers", "x-requested-with");

// Generate HTML response
const QString html = QStringLiteral(
    "<!doctype html>\n"
    "<html>\n"
    "<head>\n"
    "    <meta charset=\"utf-8\">\n"
    "    <title>Rolisteam Dice System Webservice</title>\n"
    "    <style>"
    "        .dice { color: #FF0000; font-weight: bold; }"
    "    </style>\n"
    "</head>\n"
    "<body>\n"
    "    %1\n"
    "</body>\n"
    "</html>\n"
).arg(result);

// Send response
res->end(html.toUtf8());

La première action est de convertir la commande pour la transformer en données lisibles. Elle est actuellement encodé en mode pourcent. Qt fournit une méthode statique pour faire la conversion. Le resultat de cette conversion est ensuite envoyé à une fonction qui exécute la commande et donne le résultat.

Il faut ensuite générer la réponse. Le premier truc à définir est le code réponse du protocole http. Le module qhttp propose des raccourcis pour cela.

Ensuite, la réponse doit contenir 3 paramètres dans les headers afin d’être accessible par des frameworks Ajax/javascript/Web 2.0 (vous ressentez mon dédain envers ces techno ?).
Après la définition des headers, il faut créer le code html (ou autre) de la réponse.

Créer une page web cliente du service

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">

    <meta name="author" content="Renaud GUEZENNEC">

    <title>Rolisteam Dice System Webservice</title>

    <style>
        .dice {
            color: #ff0000;
            font-weight: bold;
        }
    </style>
</head>

<body>

<form id="dice-form">
    <input
        id="cmd"
        name="cmd"
        type="text"
    >

    <button
        id="roll"
        type="submit"
    >
        Roll
    </button>
</form>

<a href="/">Clear</a>

<div class="diceresult"></div>

<script>
    const form = document.getElementById("dice-form");
    const cmd = document.getElementById("cmd");
    const result = document.querySelector(".diceresult");

    form.addEventListener("submit", async (event) => {
        event.preventDefault();

        const params = new URLSearchParams({
            cmd: cmd.value
        });

        try {
            const response = await fetch(
                `http://127.0.0.1:8085/?${params}`
            );

            if (!response.ok) {
                throw new Error(`HTTP error: ${response.status}`);
            }

            const data = await response.text();

            result.insertAdjacentHTML(
                "afterbegin",
                `<p>${data}</p>`
            );
        }
        catch (error) {
            console.error(error);

            result.insertAdjacentHTML(
                "afterbegin",
                `<p class="error">Unable to contact the dice server.</p>`
            );
        }
    });
</script>

</body>
</html>

Le travail ici est assez simple. Il faut créer un formulaire pour permettre à l’utilisateur de saisir la commande de dés. J’ai utilisé JQuery comme framework javascript pour récupérer la commande et envoyer la requête de mon webservice. Quand la requête s’est bien passée, le résultat s’affiche dans la page sans recharger la page.

Le lien vers le code source: https://github.com/Rolisteam/DiceParser/tree/master/webserver
Le webservice fait partie de projet DiceParser: https://github.com/Rolisteam/DiceParser