Récemment Bruno expliquait comment utiliser Go pour écrire des services web. Go est un langage puissant, conçu autant pour les performances, que pour faciliter la vie des développeurs. Je vous conseille de relire ses deux premiers articles sur le sujet, le premier vous donnera les bases, et le second vous donnera envie d'en savoir plus.
Néanmoins, en y repensant, je me suis demandé à quoi pourrait ressembler un service web équivalent écrit en C. Faire du web en C est une idée saugrenue et dangereuse dans (presque) tous les cas. Enfin, si le langage lui même est moins amical que Go, il a derrière lui les années de développeurs barbus, et il lui reste encore de beaux jours… :)
Il y a mille manières de faire du web en C. Pour aller au plus vite, j'ai pris le parti d'utiliser Tofu d'Alessandro Ghedini. Le projet est récent, purement expérimental, mais démontre le type d'API que peut proposer une bibliothèque C pour des applications web.
Tofu est un projet qui me semble intéressant dans la mesure où il supporte plusieurs backends, pour n'en citer que deux :
- zmq, qui lui permet de tourner derrière un certain mongrel2,
- ou, plus simplement, la libevent, qu'on va sagement utiliser pour notre petit PoC.
Dans d'autres contextes que celui-ci, ce ne serait donc pas une brique monolithique déterminant toute une architecture, mais bien un élément qui pourrait s'intégrer avec d'autres. C'est un détail qui mérite d'être souligné.
Nous allons construire un service HTTP simplissime. Ce dernier va
répondre sur une URL du type http://server/hello/af83
, avec le JSON
{"hello": "af83"}
.
Commençons par créer un fichier hello.c
. Pour utiliser les fonctions
de Tofu, on doit inclure le header tofu.h
, et pour parler JSON sans
bégayer, jansson.h
.
#include <tofu.h>
#include <jansson.h>
int main(int argc, char ** argv) {
return 0;
}
Pour récupérer la dernière version de Tofu, un rapide git clone
https://github.com/AlexBio/Tofu.git
devra faire l'affaire. Après quoi
il faudra suivre le README de la bibliothèque pour la
compiler avec vos petites mimines, et finalement soit l'installer sur
votre système, soit jouer avec les flags -I
et -L
de gcc pour linker
votre programme…
C'est là qu'un outil comme go get
nous faciliterait bien la vie !
Pour initialiser Tofu, dans notre fonction main
, on fera :
// Initialise le serveur HTTP sur le port TCP 8080.
char *opts[] = { "0.0.0.0", "8080" };
tofu_ctx_t *ctx = tofu_ctx_init(TOFU_EVHTTP, opts);
// Lance le serveur HTTP, et bloque le programme.
tofu_loop(ctx);
Si vous compilez votre programme en l'état, vous obtiendrez un serveur HTTP des plus inutiles qui ne répondra que par des erreurs 404 quelle que soit l'URL accédée. C'est pas encore ça.
Tofu permet d'assigner des callbacks à des URL statiques, ou
dynamiques. Dans cet exemple, on aimerait, lorsqu'on accède à
/hello/af83
, obtenir le JSON {"hello": "af83"}
. On peut d'ores et
déjà relier l'URL attendue à une tierce fonction en utilisant
tofu_handle_with
:
// à placer *avant* l'appel bloquant à tofu_loop, hein ;)
tofu_handle_with(ctx, GET, "/hello/:name", hello, 0x0);
Pour répondre sur cette URL, il nous faut définir le callback hello
mentionné dans le code. Il doit renvoyer un pointeur du type
tofu_rep_t
, et accepter deux arguments :
// voilà un callback "hello" minimal...
tofu_rep_t *hello(tofu_req_t *req, void *argp) {
tofu_rep_t *rep = tofu_rep_init();
return rep;
}
Notre serveur HTTP ne renvoie plus d'erreur 404 sur /hello/af83
, mais
répond avec rien, ce qui est bien mais pas top. Pour bien faire, on va
récupérer le paramètre name
qu'on a créé avec tofu_handle_with
: Tofu
a le bon goût de fournir tofu_param
pour ça.
char *param = tofu_param(req, "name");
On pourrait alors construire une réponse JSON « à la main » avec les informations qu'on a récupérées. Mais pourquoi se priver d'utiliser la lib Jansson qui sait échapper les caractères spéciaux, et construire du JSON propre ?
// `obj` est un objet JSON vide, soit : {}
json_t *obj = json_object();
// obj a désormais une clef "hello", qui reprend la valeur de param.
// Par exemple: {"hello": "af83"}
json_object_set(obj, "hello", json_string(param));
// `json_dumps` encode `obj` pour en faire une chaîne de caractères.
char *reply = json_dumps(obj, JSON_ENSURE_ASCII);
Il nous reste à mettre tout ça en forme pour répondre, à l'aide de
tofu_head
, et de tofu_write
. L'exemple complet est disponible sur un
Gist. Si vous êtes assez curieux pour y jeter un œil,
vous vous rendrez compte qu'en à peine quelques lignes de C, on peut
faire pas mal de choses…
Ce court billet pourrait illustrer une question que se posent souvent les développeurs (du moins on l'espère) : est-ce que j'utilise le bon outil pour ce boulot ?
Le C par exemple est un langage formidable pour écrire des programmes qu'on dit de « bas-niveau » (un serveur DNS, un kernel, une machine virtuelle, …), Go en est un autre. Pour du web en revanche, vous serez beaucoup mieux servis par des langages dynamiques comme Ruby, ou Javascript. S'il est donc facile (et surtout divertissant :p) d'écrire des petites applications web comme celle-ci en C, ça n'est que rarement souhaitable.