| Crates.io | francoisgib_webserver |
| lib.rs | francoisgib_webserver |
| version | 1.0.3 |
| created_at | 2025-03-27 01:22:51.253059+00 |
| updated_at | 2025-04-28 13:56:33.132099+00 |
| description | HTTP Webserver |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1607411 |
| size | 284,475 |
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.
La documentation est disponible publiquement sur GitLab Pages ici : doc gitlab-pages, ou directement sur crates.io : doc crate.io
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
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.
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
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.