Inhaltsverzeichnis

Wortkartei als REST-Server

Wir haben bereits eine Wortkartei-App mit OO gebaut:

Ein Konsolen-Programm liest Daten im CSV-Format ein, erfragt Übersetzungen, und speichert die Resultate wieder ab.

Das ist schön und gut, aber wir möchten ja eine App bauen, die nicht nur für uns selbst zugänglich ist, sondern für jedermann über das Internet verwendet werden kann:

HTTP

Das HyperText Transfer Protocol wird benützt, um Webseiten (und CSS und JS und Bilder…) zwischen einem Client (z.B. dem Browser) und einem Server auszutauschen. Wir schauen uns an, was passiert, wenn wir auf eine Webseite zugreifen. In den DevTools zeigen wir den Network Tab, wo die Folge der HTTP Requests angezeigt werden.

Ressourcen & URLs

HTTP dient dazu, Ressourcen über das Internet zu übertragen. Ressourcen sind zum Beispiel Dateien, die vom Server zum Client übertragen werden:

Damit erstellen wir das Grundgerüst für unsere Web-App.

Mehr zu diesen Sprachen im Grundlagenfach Informatik 2.

Statt ganzen Dateien können wir aber als Ressourcen auch Daten übertragen, die „on-the-fly“ auf dem Server erzeugt werden. Beispielsweise können wir C#-Code schreiben, um dynamisch ein zufälliges Wort zur Übersetzung zu liefern.

Jede Ressource wird durch einen Uniform Resource Locator (URL) identifiziert. Die URL ist genau das, was in der Adresszeile des Browsers angezeigt wird. Beispiel für URLs in unserer App:

Die URL besteht aus:

Request & Response

Jeder Request verlangt eine bestimmte Ressource mit einer bestimmten Method. Die Method beschreibt die Aktion, typischerweise GET, um eine Ressource zu holen, POST um eine Ressource auf dem Server zu verändern.

Der Request enthält eine Anzahl zusätzlicher Informationen als Headers, insbesondere:

Der Server antwortet mit einem Response Code und zusätzlichen Informationen, zum Beispiel der angeforderten Ressource. Die wichtigsten Codes sind:

Cookies

Zusätzlich kann der Server noch Cookies mitliefern, die der Browser speichert und beim nächsten Request an den gleichen Server im Request mitgeliefert werden.

Session-Cookies leben nur, solange das Browser-Fenster offen ist und sind in der Regel unproblematisch. Long-lived cookies können über Jahre installiert bleiben und bei jedem Seitenbesuch erneuert werden. Sie werden zum Teil benützt, um Benutzer über Webseiten hinweg zu verfolgen, und haben zur ganzen Diskussion um Cookies geführt. Um den Benutzer zu identifizieren (zum Beispiel nach erfolgreichem Login) sind Session-Cookies aber unverzichtbar.

Ein Web-Server mit C#

Wir verwenden das Grapevine-Framework, um einen einfachen Webserver zu bauen.

Wir fügen das Framework zu unserem Projekt hinzu:

dotnet add package Grapevine --version 5.0.0-rc.10

Dann starten wir einen Server, der Dateien im Ordner website als Dateien überträgt:

VociServer.cs
using Grapevine;
 
namespace server;
 
class VociServer {
    static void Main(string[] args) {
        // Start the standalone REST server.
        using (var server = RestServerBuilder.UseDefaults().Build())
        {
            // The server will scan the entire assembly for RestResource annotations
            // and add them to the served prefixes.
 
            // In addition, we want to serve static content (HTML, CSS, JS that is not modified
            // but transferred to the client as is from the "website" folder.
            string staticPath = Path.Combine(Directory.GetCurrentDirectory(), "website");
            server.ContentFolders.Add(staticPath);
            server.UseContentFolders();
 
            // Start the server.
            server.Start();
 
            Console.WriteLine("Press enter to stop the server");
            Console.ReadLine();
        }
    }
}

Wenn wir ihn starten mit dotnet run, können wir alle Dateien im website Ordner übertragen. Wenn du jetzt mehrere Main Funktionen hast, kannst du die auszuführende angeben mit

dotnet build -p:StartupObject=server.VociServer
dotnet run 

Eine einfache Website könnte beispielsweise mit folgendem HTML dargestellt werden:

index.html
<html>
    <head>
        <title>Voci Trainer</title>
    </head>
    <body>
        <h1>Voci Trainer</h1>
        <p>Trainiere <strong>täglich</strong>!</p>
    </body>
</html>

Aufgabe: Statische Webseite

Dynamische Ressourcen

Für eine Webapp wollen wir nicht nur ganze Dateien übertragen, sondern dynamisch auf eine Anfrage (also eine bestimmte URL) reagieren.

Wir müssen also Server-Code schreiben, der weiss, wie er auf bestimmte URLs reagieren soll. Dazu verwenden wir Annotations, die vom Server automatisch erkannt werden. Das zugrundeliegende Muster heisst Representational State Transfer (REST), deshalb heissen die Annotations RestResource und RestRoute.

Der folgende Code wird ausgeführt, wenn die Adresse http://localhost:1234/hello/world aufgerufen wird:

Hello.cs
using System.Net;
using Grapevine;
 
[RestResource]
public class Hello {
    // RestRoute tells the server to reply to requests of the given pattern.
    // "Get": the HTTP-method (normally either "Get" or "Post")
    // "/hello/world": the path to match.
    [RestRoute("Get", "/hello/world")]
    public async Task World(IHttpContext context) {
        await context.Response.SendResponseAsync("Hello Client");
    }
}

Es ist auch möglich, statt eines exakten Pfads ein Muster anzugeben und die passenden Segmente auszulesen. Achtung: in URLs werden Sonderzeichen und Leerschläge speziell codiert, wir müssen die Codierung wieder rückgängig machen:

    // Matches any path of the pattern "/greetings/<anything>"
    [RestRoute("Get", "/greetings/{name}")]
    public async Task Greetings(IHttpContext context) {
        // Extract the name from the URL path.
        // UrlDecode ensures that spaces and special characters are in the right format.
        string name = WebUtility.UrlDecode(context.Request.PathParameters["name"]);
        await context.Response.SendResponseAsync($"Greetings, {name}!");
    }

Aufgabe: Dynamischer Webserver

Web-Applikation

Nun kennen wir die Grundbausteine einer Webapp. Wir möchten nun unseren Voci-Trainer als Web-Schnittstelle zur Verfügung stellen. Verwende zum Start den untenstehenden Code und verbinde ihn mit deinem Voci-Code, um ein zufälliges Wort zurückzugeben.

VociRoutes.cs
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Grapevine;
 
/**
 * ServiceLifetime.Singleton tells the server to keep the same
 * resource around for the entire time the application runs, instead
 * of creating a fresh resource for every request.
 */
[RestResource]
[ResourceLifetime(ServiceLifetime.Singleton)]
public class VociRoutes {
    // TODO: load the unit from CSV
    private Unit unit;
 
    [RestRoute("Get", "/random")]
    public async Task Random(IHttpContext context) {
        // TODO: Choose a random word from the unit and return in the request.
    }
}

Aufgabe: Web-Schnittstelle für Voci-App

Aufgabe: Statische Webseite

Aufgabe: Web-App

JavaScript

JavaScript einbinden:

index.html
<html>
    <head>
        <title>Voci Trainer</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="voci.css" />
    </head>
    <body>
        <h1>Voci Trainer</h1>
        <div class="words">
            <span id="original"></span>
            <input type="text" id="translation" name="translation" onkeyup="keyup(this, event);">
            <div class="buttons">
                <button type="button" id="submit" onclick="submit();">Eingabe</button>
            </div>
        </div>
        <div class="status">
            <p id="reply"></p>
            <p id="stats"></p>
        </div>
    </body>
    <script src="voci.js" async></script>
</html>

JavaScript Code:

voci.js
/**
 * Function called when the user clicks the submit button or presses Enter.
 */
async function submit() {
    // Get the original and translated words, send them to the server.
    original = document.getElementById("original").innerHTML;
    translation = document.getElementById("translation").value;
    uri = `/submit/${original}/${translation}`;
    let response = await fetch(uri);
 
    let response_text =  await response.text();
    if (!response.ok) {
        // something unexpected happened...
        console.log(`Error: ${response_text} in response to '${uri}'`);
        return;
    }
    // Update the status text.
    document.getElementById("reply").innerText = response_text;
 
    // And fetch the next word.
    next();
}
 
/**
 * Update the stats.
 */
async function updateStats() {
    uri = '/stats';
    let response = await fetch(uri);
    let response_text =  await response.text();
    document.getElementById("stats").innerText = response_text;
}
 
/**
 * Fetches a new word to translate. Called automatically after submit(), and initially for the first
 * word.
 */
async function next() {
    // Ensure the stats are correctly displayed.
    updateStats();
 
    uri = `/random`;
    let response = await fetch(uri);
    let response_text =  await response.text();
    if (!response.ok) {
        console.log(`Error: ${response_text} in response to '${uri}'`);
        return;
    }
    document.getElementById("original").innerText = response_text;
    document.getElementById("translation").value = "";
}
 
/** Called when a key is released within input. */
async function keyup(input, event) {
    var code = event.code;
    if (code == "Enter") {
        submit();
    }
}
 
// Initial call to fetch the first word.
next();