14 releases (4 stable)
Uses new Rust 2024
new 1.0.3 | Apr 28, 2025 |
---|---|
1.0.0 | Apr 27, 2025 |
0.1.2 | Apr 27, 2025 |
0.0.7 | Apr 16, 2025 |
0.0.4 | Mar 31, 2025 |
#257 in HTTP server
806 downloads per month
190KB
2K
SLoC
Serveur Web
François Gibier
Prérequis / Crates
-
rustc 1.85
-
cargo
-
tokio : Un runtime asynchrone, utilisé pour gérer les opérations I/O non-bloquantes, permettant l'exécution de tâches asynchrones de manière concurrente.
-
smallvec : Une structure de données optimisée pour les tableaux dynamiques de petite taille, utilisée ici pour stocker efficacement un petit nombre d'éléments sur la pile, réduisant ainsi les allocations dynamiques.
-
strum / strum_macros : Crate qui simplifie le travail avec les énumérations, en fournissant des macros utiles pour sérialiser les enums en chaînes de caractères et inversement.
-
chrono : Une bibliothèque pour la gestion des dates et heures dans Rust. Elle est utilisée pour manipuler les dates dans les réponses HTTP (par exemple, pour l'en-tête
Date
). -
serde / serde_json : Bibliothèques pour la sérialisation et la désérialisation, utilisées ici pour transformer / parser des structures de données en JSON.
-
toml : Une bibliothèque pour la gestion des fichiers TOML, utilisée ici pour lire et charger les fichiers de configuration du serveur.
Documentation
La documentation est disponible publiquement sur GitLab Pages ici : doc gitlab-pages, ou directement sur crates.io : doc crate.io
Fonctionnalités
- Support de HTTP/1.0 et HTTP/1.1
- Support des méthodes HTTP GET et HEAD (+ POST, DELETE, PATCH, PUT)
- Support du header Connection (keep-alive / close)
- Le serveur émet les headers Server, Date, Content-Type, Content-Length à chaque réponse.
- Code HTTP implémentés:
- 200 Ok
- 201 Created
- 202 Accepted
- 206 Partial Content
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 405 Method not allowed
- 500 Internal server error
- 501 Not Implemented
- 505 HTTP Version not supported
- Le serveur peut avoir trois types d'endpoints:
- Il peut servir des répertoires
- Il peut servir un fichier simple
- Il peut servir des endpoints custom (code rust)
- Les requêtes / réponses ainsi que les informations comme l'adresse du client, et la date de la requête sont loggées dans un fichier si son chemin est spécifié dans le fichier de configuration config.toml.
- Le serveur est multithreadé à l'aide de tokio (asynchrone).
- Les statistiques du serveur sont disponibles au format JSON sur le chemin de la constante metrics_endpoint dans le fichier config.toml s'il est spécifié, la crate serde est utilisée pour sérialiser en JSON.
- La configuration du serveur et du pool tokio se fait à partir d'un fichier de configuration config.toml, tous les paramètres de configurations sont optionnels.
- Support du header Range (et donc du code 206 Partial Content).
- Support du transfer chunked encoding.
- Par défaut, une réponse 404 Not Found vide sera renvoyée par le serveur, mais l'endpoint not found peut être configuré soit avec le fichier de configuration, soit directement avec un objet de configuration.
Optimisations
Buffers / Smallvec
Pour éviter certaines allocations dynamiques qui peuvent prendre du temps pour les petites requêtes, un "Buffer" a été implémenté, les premières entêtes ainsi que le corps d'une requête / réponse HTTP seront stockées dans la pile, pour gérer les headers, la crate Smallvec a été utilisée, ce n'est ni plus ni moins qu'un tableau dans la pile qui dès qu'il est remplie, va commencer à remplir un nouveau tableau dynamique qui lui est dans le tas.
Pour ce qui est du corps du corps d'une requête, on peut déterminer sa taille à l'avance quand le header Content-Length est spécifié, on peut donc allouer dans la pile les petits corps de requêtes / réponses, et dans le tas les plus grands, pour ce faire, une structure Buffer à été implémentée, elle propose plusieurs méthodes pour abstraire la lecture dans un flux.
Toutes les constantes définissant la taille maximale d'un body et le nombre de headers dans la pile sont spécifiés dans le fichier config.rs
Asynchronisme
Pour exploiter au mieux les coeurs du processeur et éviter de perdre du temps d'éxécution en attendant / envoyant des données sur les opérations bloquantes, la crate tokio a été utilisée, elle permet de rendre asynchrone toutes les opérations bloquantes. C'est très intéressant ici car on a beaucoup d'attentes lors de la lecture et l'écriture de flux réseau, cela permet donc de gérer plus de connexions en concurrence.
Une méthode naïve aurait été d'utiliser des processus légers (thread), cependant à chaque lecture / écriture le thread aurait été bloqué ce qui ne permet pas de gérer autant de connexions. De plus, l'utilisation de threads aurait été plus lourde en mémoire que le runtime tokio.
Le serveur propose deux méthodes pour être lancé, pub fn start(self)
et pub async fn async_start(self)
, le premier permet de configurer le nombre de threads que le runtime va utiliser (par défaut le nombre de coeurs du processeur), tandis que le second est directement à utiliser dans un runtime.
Endpoints Tree
Pour gérer les différents endpoints du serveur, une structure de données en arbre a été implémentée, à chaque ajout d'un endpoint, l'arbre va être descendu jusqu'à la feuille correspondante en passant par les différentes partie de l'URI (séparées par un /) et y ajouter un endpoint servit par le serveur.
Exemple:
/dir(./static) -> Sert le dossier static
/hello(hello handler) -> Renvoie une réponse construite à partir du handler hello
/file(file.txt) -> sert le fichier file.txt
-> /second(second-file.txt) -> sert le fichier second-file.txt
Sérialisation des enums
Comme le protocole HTTP est assez strict sur les headers, les content types, il peut être utile de déclarer des enums qui vont couvrir les fonctionnalités implémentées et détecter celles qui ne le sont pas pour réagir en conséquence. Cela permet également de ne pas stocker certaines valeurs sous la forme de chaines de caractères et d'éviter des allocations dynamiques inutiles, qui peuvent êtres couteuses en mémoire. Sérialiser les enums permet d'attribuer des valeurs entières qui pourront être copiées sans allocation dynamique. Le cout lié à la sérialisation est rentabilisé par la mémoire gagné et par exemple, dans le cas du processing des headers, il est beaucoup plus rapide de rechercher des enums sérialisés.
L'intérêt est aussi de faciliter le développement et l'extensibilité, par exemple pour trouver le content type avec l'extension de fichier:
#[derive(Debug, Display, EnumString, PartialEq, Clone, Copy)]
pub enum ContentType {
#[strum(serialize = "application/json", serialize = "json")]
ApplicationJson,
}
Ici, cela permet de sérialiser / désérialiser facilement pour trouver le content-type, sans avoir de logique inutile. Enfin, le fait de sérialiser les enums permet d'éviter des erreurs de parsing et avoir un controle total sur ce qui est implémenté, ou non.
La crate strum permet de sérialiser efficacement les enums.
TODO
- Ajout de niveau de logs dans le Logger (attribut ajouté mais pas implémenté).
- Ajout de middlewares sur les noeuds de l'arbre des endpoints.
- Ajout d'autres tests d'intégrations et des tests pour les exemples.
- Empêcher un noeud d'être créé dans l'arbre des endpoints quand un répertoire est déjà servit plus haut.
Dependencies
~4–11MB
~113K SLoC