Een web-app
Arnout van Kempen over rommelen in een digitale wereld.
Tot nu toe hebben we Rust uitsluitend gebruikt voor programma's die lokaal werken, via de CLI. Geen grafische grappen en niks online. Toch is dat in Rust betrekkelijk eenvoudig, dus daar gaan we maar eens in duiken. Vandaag beginnen we met het maken van een eerste webpagina. Nog geen spectaculair resultaat. Het is vooral van betekenis om te zien hoe eenvoudig het werkt.
Het is van belang te bedenken dat de Apache-server die we eerder installeerden, voor Rust geen functie heeft. Wat we gaan doen is een complete webserver in Rust bouwen, die zelfstandig kan werken. Dat betekent wel dat we nog wat extra werk te doen hebben, om de server op de achtergrond te laten draaien zodra we de computer aan zetten; net zoals Apache. Maar dat komt later, nu eerst de webserver zelf.
Stap 1: nieuw project en de dependencies toevoegen
Eerst maken we met cargo new een nieuw project. Op GitHub staat mijn versie, met de naam 064-web, aangezien dit aflevering 64 is. Maak je eigen project en ga naar de project-directory. Hier staat het bestand Cargo.toml. Open dit en voeg de volgende dependencies toe:
actix-web = “4.0”
chrono = “0.4”
sys-info = “0.9”
De eerste geeft toegang tot een crate waar alle hulpmiddelen in zitten die we nodig hebben voor een webserver. De tweede bevat enkele tijd- en datum-functies en de derde geeft gegevens over de computer.
Stap 2: het Rust-programma zelf
Om te beginnen moeten we aangeven welke crates we gaan gebruiken, tenzij we iedere keer de crate gaan noemen in de functies zelf. Dat betekent concreet:
use actix_web::{web, App, HttpServer, Responder);
use chrono::Local;
Vervolgens hebben we een functie nodig die beschrijft wat de webpagina moet gaan weergeven. Hierbij gaan we een paar nieuwigheden zien.
async fn index() -> impl Responder {
De index-functie die we gebruiken is een async, een a-synchrone functie. Moderne computers met moderne besturingssystemen, zoals Windows, Linux en MacOS, kunnen verschillende taken tegelijk uitvoeren. Rust haakt daar op aan, maar doet dat op twee manieren. Door gebruik te maken van de multi-threading die het OS biedt en door a-synchrone functies. De eerste variant is relatief simpel, maar kost veel systeembronnen. De tweede is iets complexer, maar gaat extreem zuinig met systeembronnen om. Bij een webserver die in feite maar één gebruiker (wijzelf) zal hebben, is dat misschien niet zo spannend. Maar wat als diezelfde server duizenden aanvragen krijgt te verwerken? Juist, dan is a-synchroon werken de oplossing. Belangrijk bij een async is dat de functie niets doet, tenzij er vraag naar is. Bij een webpagina zou het niet zo handig zijn als deze constant zelf bekijkt of er vraag naar data is; als async doet de functie absoluut niets, tot de vraag komt.
Verder is hier opvallend dat de functie een resultaat "type" heeft van impl Responder, waar we wellicht een bekender type hadden verwacht. Door de impl weet Rust niet welk type teruggegeven zal worden, maar wel dat het een type moet zijn dat de trait Responder heeft. Rust weet dus nog niet wat de functie exact gaat opleveren, maar wel dat dat resultaat begrepen gaat worden zolang Responder er maar is. En dat is precies wat de actix_web crate nodig heeft.
In de volgende regels halen we informatie op van de lokale computer (back end), dat is dus de computer waarop het programma draait en niet de computer waarop de browser draait (front end).
let now = Local::now();
let os_type = sys_info::os_type().unwrap_or_else(|_| “Onbekend”.to_string());
…
let mem_info = sys_info::mem_info().unwrap_or(sysinfo::MemInfo { total: 0, …});
Let op, waar … staat, hoort meer code, te vinden op GitHub!
De eerste let is makkelijk. De variabele krijgt het huidige tijdstip mee. De volgende twee zijn op zich ook niet zo moeilijk, er wordt wat informatie over het systeem verzameld. Wat hier wel opvalt is de foutafhandeling en het verschil tussen beide varianten.
De functies uit sys_info geven een Result terug. Omdat we daar zelden een error-code verwachten, kunnen we die betrekkelijk simpel met een unwrap uitlezen. Maar het kan soms wel fout gaan, dus gebruiken we unwrap_or_else of unwrap_or. Beide functies doen hetzelfde, maar werken net iets anders. De or_else variant voert een unwrap uit en als die een error oplevert, wordt de closure uitgevoerd die als argument is meegegeven. De or variant voert een unwrap uit en evalueert het argument dat is meegegeven. De waarde van dat argument wordt teruggegeven bij een error, of genegeerd.
Waarom nu beide manieren gebruiken? De variant met or_else is iets complexer door het gebruik van de closure, maar heeft als voordeel dat deze uitsluitend wordt geëvalueerd als dat nodig is, bij een error dus. Dat maakt deze benadering vooral zinvol als de evaluatie van de closure relatief veel moeite kost. Door het gebruik van de .to_string() bij de os_type() -functie is dat inderdaad het geval.
De variant met or is simpeler, maar wordt altijd geëvalueerd; ook als die niet nodig is. Deze heeft dus de voorkeur als de evaluatie zelf simpel is. Het toekennen van een nul aan de opgevraagde velden is zo simpel en dus gebruiken we deze variant voor de mem_info()-functie.
Goed, we hebben nu alleen nog een opdracht nodig om alle informatie terug te leveren aan de webserver. Dat doen we met
format!(“Het is nu…) // de rest vind je op GitHub
De format! macro is een soort print! maar dan niet voor stdout, maar voor de aanroepende web-server.
Het enige dat we nu nog nodig hebben is het draaien van de webserver zelf. Dat gaat als volgt.
#[actix_web::main]
Dit is een macro uit de actix_web crate, die een zogenaamde runtime start. Omdat alle taken in ons programma als async zijn gedefinieerd, is die runtime nodig. Het is de code die het beheer van de a-synchrone taken regelt. Je zou het beeld van een kapstok met jassen kunnen gebruiken. Alle functionaliteit zit in de a-synchrone functies, de jassen. Maar je hebt iets nodig om die jassen aan op te hangen, een kapstok. Dat is de runtime.
De mainfunctie zelf doet niets anders dan de webserver opbouwen, naar een poort laten luisteren en aan het werk zetten.
async fn main() -> std::io::Result<()> {
geeft aan dat ook main een async is en dat de uitkomst een Result zal doorgeven aan het operating system.
HttpServer::new(|| App::new().route(“/“, web::get().to(index)))
bouwt een Http (web dus) server op, die gaat reageren als iemand de root (“/“) opvraagt, door de index-functie terug te geven.
.bind(“0.0.0.0:8080”)?
vertelt de server te luisteren naar alle IP-adressen (vandaar de nullen) op poort 8080. De normale poort voor een webpagina is 80, dus als we hiermee echt naar buiten willen, moeten we de poort nog aanpassen. Het vraagteken op het eind hangt samen met het type van de return-waarde: een Result. Immers, als de bind een foutmelding terug geeft, dan breken we met het vraagteken het programma direct af en geven de foutcode door aan het besturingssysteem.
.run()
start de web-server op.
.await
vertelt het programma te wachten tot de aanroepende functie, run() dus, klaar is. Anders gezegd, de server zal blijven draaien tot er een fout optreedt in de bind(). Bijvoorbeeld omdat je deze met ctrl-C afbreekt.
Let op als je hiermee aan het experimenteren gaat: de serverkant, je Rust-programma dus, draait op een computer, het resultaat ga je zien in een browser op dezelfde of een andere computer. Je zal wel toegang moeten hebben tot de servercomputer en het IP adres van die computer moeten kennen. Stel dat je van hostname -I gemeld hebt gekregen dat je adres 192.168.1.179 is, dan kan je het hiervoor beschreven programma dus uitlezen door in je browser te typen: 192.168.1.179:8080
Wie mee wil doen met #klooienmetcomputers kan dat doen via GitHub. Maak een account op github.com en zoek naar Abmvk/kmc. Het account Abmvk volgen kan ook. Lezers zijn vrij te gebruiken wat ze willen en om zelf zaken toe te voegen of aan te passen, vragen te stellen of commentaar te leveren.
Gerelateerd
Over bits & bytes
Arnout van Kempen over rommelen in een digitale wereld.
En arrays dan?
Arnout van Kempen over rommelen in een digitale wereld.
Typecasting in COBOL
Arnout van Kempen over rommelen in een digitale wereld.
Gewone variabelen
Arnout van Kempen over rommelen in een digitale wereld.
Bestanden in soorten en maten
Arnout van Kempen over rommelen in een digitale wereld.