URL: https://linuxfr.org/users/superzen/journaux/des-nouvelles-d-ulfius-framework-web-en-c Title: Des nouvelles d'Ulfius, framework web en C Authors: Babelouest Date: 2018年10月09日T22:43:38+02:00 License: CC By-SA Tags: langage_c, framework, web et c Score: 43 J'avais parlé [ici](https://linuxfr.org/users/superzen/journaux/ulfius-framework-pour-faire-des-api-web-en-c) d'Ulfius, un framework web écrit en C pour se faciliter la vie quand on veut développer des API web. J'en profite donc de sortir la dernière version 2.4 pour en parler à nouveau et vous raconter ce que ca peut faire. Adresse du projet: https://github.com/babelouest/ulfius Le besoin est d'avoir un framework web pour écrire des applications web en C, en combinant la rapidité d'exécution ainsi que la consommation de mémoire plus réduite, avec un niveau d'abstraction permettant de réduire le temps de développement sans avoir à se préoccuper du bas-niveau (interprétation et validation de la requête, construction de la réponse, etc.). Avec ca j'ai créé mon [SSO](https://github.com/babelouest/glewlwyd), mon [système domotique](https://github.com/babelouest/angharad) ou encore mon serveur de [streaming audio](https://github.com/babelouest/taliesin). Je fais ici une description simplifiée des principales fonctionnalités, mais la [documentation officielle](https://github.com/babelouest/ulfius/blob/master/API.md) est bien mieux fournie! ## Implémenter des API HTTP La principale fonctionnalité est l'implémentation d'API HTTP. En dessous d'ulfius, il y a l'honorable [libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) qui gère les parties bas-niveau, notamment réseau, file d'attente, conformité aux standards. Ulfius s'est largement inspiré d'autres frameworks comme [nodejs express](https://expressjs.com/fr/) pour son utilisation. On déclare d'abord une instance ulfius qui va écouter sur un port TCP donné, puis à cette instance on associe des endpoints http qui correspondent à des adresses HTTP (méthode + url). Un endpoint peut avoir des variables dans son url pour faire du REST plus facilement. Par exemple, si on associe l'url `GET /api/user/@username` à un endpoint, le contenu après `/user/` sera affecté à la variable `username` disponible dans la fonction callback. À chaque endpoint, on déclare une fonction callback qui sera exécutée lorsqu'un client fera un appel à l'url. Un endpoint peut avoir un pattern `"générique"` du type `/api/user/*` ce qui signifie que toute url qui commence par `/api/user/` sera interceptée. Chaque fois qu'une url sera appelée par un client, ce sera traité dans un thread séparé. Dans la fonction callback, on a accès à deux structures de données: - `struct _u_request` qui contient l'ensemble des paramètres de la requête HTTP (headers, paramètres d'url ou de body, corps du body, etc.) - `struct _u_response` qui permet à la fonction callback de spécifier la réponse à renvoyer au client avec tous les paramètres HTTP disponibles (statut, headers, body, cookies, etc.) L'exemple du `Hello World!` est le suivant: ```C /** * test.c * Small Hello World! example * to compile with gcc, run the following command * gcc -o test test.c -lulfius */ #include #include #define PORT 8080 /** * Callback function for the web application on /helloworld url call */ int callback_hello_world (const struct _u_request * request, struct _u_response * response, void * user_data) { ulfius_set_string_body_response(response, 200, "Hello World!"); return U_CALLBACK_CONTINUE; } /** * main function */ int main(void) { struct _u_instance instance; // Initialize instance with the port number if (ulfius_init_instance(&instance, PORT, NULL, NULL) != U_OK) { fprintf(stderr, "Error ulfius_init_instance, abort\n"); return(1); } // Endpoint list declaration ulfius_add_endpoint_by_val(&instance, "GET", "/helloworld", NULL, 0, &callback_hello_world, NULL); // Start the framework if (ulfius_start_framework(&instance) == U_OK) { printf("Start framework on port %d\n", instance.port); // Wait for the user to press on the console to quit the application getchar(); } else { fprintf(stderr, "Error starting framework\n"); } printf("End framework\n"); ulfius_stop_framework(&instance); ulfius_clean_instance(&instance); return 0; } ``` Ainsi, le programme peut traiter la requête HTTP en ayant tout sur place, et remplir la réponse HTTP dans une seule structure avec des paramètres lisibles, genre "Je veux que ma réponse ait le statut 400 avec dans le body 'Erreur dans le paramètre toto' et dans le header, la paire clé:valeur". ```C ulfius_set_string_body_response(response, 400, "Erreur dans le paramètre toto"); u_map_put(response->map_header, "clé", "valeur"); ``` Une interface optionnelle avec la librairie [jansson](http://www.digip.org/jansson/) permet d'échanger plus facilement du JSON dans le corps de la requête ou de la réponse. Une nouveauté depuis Ulfius 2.0 est la possibilité de déclarer autant de endpoints que l'on souhaite pour une même url ou des urls qui se croisent. Un paramètre permettant de spécifier la priorité d'un endpoint permet de spécifier quelle callback est appelée avant quelle autre. Cela permet par exemple de déclarer une callback qui authentifie, une autre callback qui traite la demande dans la requête, et une dernière qui gzipe ca à la fin. Pour renvoyer de grosses réponses HTTP bien lourdes, plutôt que de tout mettre dans la variable `struct _u_response.binary_body` qui sera nécessairement stockée en mémoire, on peut aussi utiliser la fonctionnalité de réponse par lot, qui permet de remplir le body de la réponse morceau par morceau. C'est pratique quand tu veux par exemple envoyer des fichiers potentiellement gros, voire faire du streaming audio dont tu ne connais pas la taille à l'avance. Aussi, pour traiter un upload de fichier potentiellement gros, on peut aussi le traiter par lot, comme le streaming, mais dans la requête. ## Faire des requêtes sortantes HTTP ou SMTP L'alter-ego aux API HTTP est le client HTTP, qui permet de faire des requêtes sortantes. Ca utilise les mêmes structures que pour les API HTTP, mais à l'inverse évidemment, à savoir qu'on remplit un `struct _u_request`, et le résultat est éventuellement stocké dans un `struct _u_response`. Il a aussi la possibilité de traiter la réponse par morceaux si on attend un résultat trop gros. ```C struct _u_request request; struct _u_response response; ulfius_init_request(&request); ulfius_init_response(&response); request.http_verb = strdup("GET"); request.http_url = strdup("https://linuxfr.org/"); if (ulfius_send_http_request(&request, &response) != U_OK) { perror("problème!"); } else { printf("Mon site préféré: %.*s", response.binary_body_length, response.binary_body); } ulfius_clean_request(&request); ulfius_clean_response(&response); ``` On peut aussi faire des envois de courriels, en spécifiant soi-même tous les paramètres. ```C int ulfius_send_smtp_email(const char * host, const int port, const int use_tls, const int verify_certificate, const char * user, const char * password, const char * from, const char * to, const char * cc, const char * bcc, const char * subject, const char * mail_body); ``` Les requêtes sortantes sont basées sur l'excellentissime [libcurl](https://curl.haxx.se/libcurl/). Elles ne réinventent rien, mais permettent de me faciliter l'usage de ces fonctionnalités pour les cas principaux. ## Gérer des websockets Les [websockets](https://fr.wikipedia.org/wiki/WebSocket) ont été spécifiées pour permettre l'échange d'informations full-duplex entre le serveur web et les clients, donc permettre au serveur de pousser des informations au client sans que le client n'ait rien demandé, ou permettre au client d'envoyer des informations au serveur sur la même connexion. Ulfius gère les websocket coté client ou serveur. Coté serveur on spécifie une url sur laquelle sera hébergé le service websocket, coté client on spécifie l'adresse du service websocket auquel on souhaite se connecter. Dans les deux cas, on gère les échanges de messages à travers 2 types de fonction callback: le `gestionnaire principal` et le `message entrant`. Le `gestionnaire principal` est appelé une fois lorsque la connexion est établie, et gardera la connexion ouverte tant qu'il est en cours d'exécution. Dans cette fonction on pourra lire les messages reçus, ou envoyer des messages. Lorsque la fonction se termine, la connexion websocket est fermée. À l'inverse, si la connection est fermée par l'autre extrémitée, cette fonction ne sera pas fermée par le framework, mais elle devra elle-même se fermer toute seule. ```C // Envoyer un message texte toutes les 2 secondes tant que la connexion est ouverte void websocket_manager_callback(const struct _u_request * request, struct _websocket_manager * websocket_manager, void * websocket_manager_user_data) { while (ulfius_websocket_wait_close(websocket_manager, 2000) == U_WEBSOCKET_STATUS_OPEN) { if (ulfius_websocket_send_message(websocket_manager, U_WEBSOCKET_OPCODE_TEXT, o_strlen("Message without fragmentation from client"), "Message without fragmentation from client") != U_OK) { fprintf(stderr, "Error send message without fragmentation\n"); } } } ``` Le `message entrant` est appelé à chaque nouveau message reçu. La fin de l'exécution de cette fonction ne cloture pas la connexion. ```C void websocket_incoming_message_callback (const struct _u_request * request, struct _websocket_manager * websocket_manager, const struct _websocket_message * last_message, void * websocket_incoming_message_user_data) { fprintf(stdout, "Incoming message, opcode: 0x%02x, mask: %d, len: %zu\n", last_message->opcode, last_message->has_mask, last_message->data_len); if (last_message->opcode == U_WEBSOCKET_OPCODE_TEXT) { fprintf(stdout, "text payload '%.*s'\n", last_message->data_len, last_message->data); } else if (last_message->opcode == U_WEBSOCKET_OPCODE_BINARY) { fprintf(stdout, "binary payload\n"); } } ``` C'est libre sous license LGPL. Je reste pas mal actif sur le développement, mais les nouvelles fonctionnalités arrivent au gré des besoins et du temps que je peux y passer. Ulfius commence à avoir une base d'utilisateurs sympathique, mais si ca peut servir à d'autres, j'en serai ravi!

AltStyle によって変換されたページ (->オリジナル) /