Title: M1.3.3: Eigener Webserver mit REST API
Course: LF07 - Cyber-physische Systeme ergänzen - FI/ITSE - Raspberry Pi > Section: LS7.1: Messdatenerfassungssysteme funktional zusammenführen

Überblick

Diese Aussage kennen wir schon aus dem vorangegangenen Abschnitt: HTTP nutzt die Topologie einer Client-Server Struktur. Ein Server stellt Ressourcen zur Verfügung und der Client greift über das Protokoll HTTP auf diese Ressourcen zu. REST nutzt die Verben (Befehle), die HTTP zur Verfügung stellt. Der Client hat damit die Möglichkeit, die Datenressourcen des Servers abzurufen und zu verarbeiten, neue Daten auf dem Server hinzuzufügen oder bestehende Daten zu ändern.

Unser Ziel ist es, die gemessenen Temperaturwerte und eventuell weitere Informationen, wie beispielsweise den Sensor- oder Raumnamen, Datum- und Uhrzeit der Messung, an den Webserver zu übermitteln. Die Parameterübergabe an den Server kann

- einerseits über an die URL angehängte Informationen erfolgen oder
- über einen im Body der HTML Anfrage übermittelten Datensatz im XML- oder JSON-Format oder
- auch beides kombiniert.

Das JSON Format hat sich in den letzten Jahren als Standard etabliert. Es hat auch Vorteil, dass die Schreibweise der Daten dem Format eines Python Dictionary Datentyps gleich ist.

Verfahren zur Datenübermittlung

1. Übermittlung mit an die URL angehängten Informationen

Ein URL-Parameter ist ein Schlüssel-Wert-Paar, welches an einen Uniform Resource Locator (URL) angehängt ist. Als Trennzeichen zwischen der eigentlichen Adresse und möglichen Schlüssel-Wert-Paaren wird das Fragezeichen verwendet. Zur Übergabe mehrerer Parameter dient als Trennzeichen zwischen den Schlüssel-Wert-Paaren das Ampersand & oder ein Semikolon ; siehe dazu auch https://wiki.selfhtml.org/wiki/URL-Parameter
URL mit einem Parameter
```https://google.de/?q=Multi Media BbS```

Der Buchstabe q hat bei der angesteuerten Suchmaschine offenbar die Bedeutung eines queries, also einer Abfrage und der Parameter ist der Suchstring

URL mit mehreren Parametern
```https://example.org/?a=1&b=2&c=3```

Der Webserver benötigt nun ein Programm, das diese Parameter aus der URL extrahieren kann, um sie zu speichern oder zu bearbeiten. Hier noch ein Beispiel mit folgendem Webquest (mit welchem Hilfsmittel man solch eine Abfrage versenden kann, wird weiter unten erklärt):

```POST http://10.2.5.140:8080/raumtemps?temp=32.9&ort=Raum 3.18 HTTP/1.1```

Erst einmal zur Bedeutung dieser URL: Es sollen Daten an den Server mit der IP 10.2.5.140 versendet werden. Kommt die Anfrage auf TCP Port 8080 beim Webserver an, leitet das Betriebssystem den Request an den Webserver weiter und dort an den Algorithmus für den Endpunkt /raumtemps. Das entspricht dem relativen Pfad in der URL, ausgehend vom Stammverzeichnis. Die Daten sollen als neuer Datensatz an die bestehenden Daten angehängt werden (POST). Der Name des ersten Parameters ist temp und dessen Wert 32.9. Der zweite Parameter heißt ort und besitzt den Wert Raum 3.18.

Wie sieht nun eine mögliche Anwendung aus, mit der solche URLs ausgewertet werden können? Das wird gar nicht so schwer, denn viele Funktionen haben bereits andere Entwicklerinnen und Entwickler für uns geschrieben. Lernen müssen wir lediglich, wie deren Schnittstellen gestaltet sind, wie sie aufzurufen sind und an welcher Stelle wir das tun sollten. Die wichtigste Bibliothek für unsere Aufgabe ist http.server. Aus ihr benötigen wir zwei Klassen für die Bearbeitung von Webanfragen.

Klasse ist in diesem Fall ein Begriff aus der objektorientierten Programmierung. Eine Klasse liefert den Bauplan für die spätere Erzeugung von Objekten (Variablen des Typs dieser Klasse). Man kann sich das vorstellen wie den Plan und die Ausführungsregeln für einen Hausbau. Der Plan zeigt, wie das Haus später aussehen soll und die Ausführungsregeln zeigen dem Bauführer, wie im Detail etwas handwerklich umgesetzt werden soll. In einen Plan kann man aber noch nicht mit seinen Möbeln einziehen. Das eigentliche Objekt, also das Haus, entsteht unter Vorlage der Pläne. Und mit diesen Plänen kann man getrost auch weitere Häuser (Objekte) bauen, die genau aussehen wie das erste. Also "Klasse=Plan, Objekt/e=Haus/Häuser"

Die erste der beiden benötigten Klassen HTTPServer liefert alle Services, also Attribute (Variablen) und Methoden (Funktionen), die man zur Realisierung eines HTTP Servers benötigt, wie Verbindungsaufbau, Senden und Empfangen, Zwischenspeicherung der Daten in Streams usw. Intern ist der Code dieser Klasse umfangreich. Den müssen wir in der Regel aber nicht kennen. Uns reicht zu wissen, wie man damit umgeht. Hier die Beschreibung von https://docs.python.org/3/library/http.server.html dazu:

Wie man bei HTTPServer an den beiden Parametern erkennen kann, benötigt man eine sogenannte RequestHandlerClass. Diese zweite Klasse müssen wir selber erzeugen. Sie erbt allerdings viele Eigenschaften von BaseHTTPRequestHandler. Hier sind Routinen zur Verarbeitung der URL-Parameter, dem Erzeugen eines Datenstromes, und der HTTP-Methoden GET, POST usw. enthalten. Und bei diesen Methoden zur Behandlung von GET, POST usw. Anfragen liegt dann der Hauptteil unserer eigenen Programmiertätigkeit. Hier nochmal die Beschreibung von docs.python.org:

```# Python 3 server example

from http.server import HTTPServer, BaseHTTPRequestHandler

class TempServer(BaseHTTPRequestHandler):

def do_GET():

#This is the method that will be executed after http GET Method was used by client

def do_POST():

#put here the code for POST Method

def do_PUT():

#here PUt

def do_DELETE():

...

```
Wir erzeugen hiermit die Klasse TempServer, also unseren Bauplan für die Erstellung eines Webservers, der Requests für unsere Temperaturprotokollierung behandeln kann. Unsere Klasse erbt alle Eigenschaften der Elternklasse BaseHTTPRequestHandler. TempServer ist dann die Klasse, die als zweiter Parameter bei HTTPServer übergeben wird. Es wird gleich gezeigt, wie das in der Praxis aussieht.

Vererbung ist wieder ein Begriff aus der objektorientierten Programmierung. Alle Methoden und Variablen, die in der Elternklasse bereits definiert wurden, kann man in der Kindklasse verwenden. Eine Ausnahme: Wenn die Methoden der Kindklasse den gleichen Namen wie in der Elternklasse besitzen, werden die der Kindklasse bei Programmausführung verwendet. Man spricht dabei von Überlagerung.

Diese Überlagerung nutzen wir auch gleich in unserem Beispiel. In BaseHTTPRequestHandler sind die Methoden do_GET(), do_POST(), do_PUT(), do_PATCH(), do_DELETE() und weitere deklariert. Sind diese in der Elternklasse noch nicht mit Code versehen, sondern nur als Rumpf gegeben, spricht man von abstrakten Methoden. Bei den gerade genannten ist genau das der Fall. Wollen wir sie nutzen, müssen wir sie mit eigenem Code füllen.

Hier folgt nun ein Codebeispiel mit (detaillierten) Erklärungen für das Hinzufügen eines Datensatzes (POST). Wir werden weiter unten sehen, dass der Code wesentlich kürzer ausfallen kann, wenn man mehrere Operationen zusammenfügt.

Und hier das "zusammengeschrumpfte Beispiel" mit gleicher Funktion:

Response Codes: https://developer.mozilla.org/de/docs/Web/HTTP/Status

MIME Types: https://de.wikipedia.org/wiki/Internet_Media_Type

Die anderen Methoden werden im Prinzip ähnlich abgehandelt. Werden bei do_GET() keine Parameter übergeben, wird in der Regel der gesamte Datenbestand, der zu dem Endpunkt gehört im Body zurückgegeben. Ein in der URL mitgeführter Parameter gibt häufig eine ID an, nach der dann in der Datenbank gesucht werden soll, z.B. so:
```GET http://10.2.5.140:8080/raumtemps?id=3 HTTP/1.1```
Der zugehörige Datensatz wird dann zurückgegeben.

Bei einem POST soll der Webserver im Body der Anwort die von ihm gespeicherten Daten an den Client zurückgeben. Das geschieht nicht automatisch, sondern muss im Code berücksichtigt werden, siehe Zeile 109 im letzten Codebeispiel. Die Abfrage ```POST http://10.2.5.140:8080/raumtemps?temp=32.9&ort=Raum 3.18 HTTP/1.1``` erhält nun folgende Rückantwort beim Client:

Auf der Webserverkommandozeile werden (mit dem ersten Codebeispiel) folgende Meldungen ausgegeben:

Das hier erläuterte Verfahren mit Übergabe der Parameter innerhalb der URL hat den Nachteil, dass alle vom Client an den Server übertragenen Daten unverschlüsselt sind und auch beim Abschicken über einen Webbrowser natürlich sichtbar in der Adressleiste auftauchen.

2. Übermittlung der Parameter im Body (content-type: application/json)

Heute und vor allem bei den IoT REST APIs gebräuchlich ist es, die Parameter in einem JSON Format im Body der Anfrage zu übergeben. Die URL bezeichnet dann lediglich einen Endpunkt.

Abhängig vom Endpunkt (dem relativen Pfad in der URL ohne den Domänenanteil) verzweigen die do_XXX() Methoden dann zu den entsprechenden Bearbeitungsroutinen für die einzelnen Endpunkte. Zum Beispiel könnte ein Endpunkt raumtemps heißen. Hier würden die Werte in der Liste für die Raumtemperaturen gepeichert, vielleicht könnte der Response auch eine Warnmeldung bei zu hoher Temperatur enthalten. Am Endpunkt buli könnten Spielergebnisse von Bundesligaspielen eingereicht werden und gleichzeitig der Tabellenstand ermittelt und zurückgegeben werden. Hinter raumtemps und buli würden dann völlig unterschiedliche Algorithmen arbeiten, beide gehören aber zum selben Webserver. Beide Anfragen würden zunächst in der do_POST() Routine ankommen und müssten dort anhand des Pfades in der URL auf unterschiedliche Routinen aufgeteilt werden.

Schauen wir zunächst auf das schon weiter oben genutzte Beispiel viel zu warmer Klassenräume und einem POST im JSON Format.

Zunächst fällt auf, dass der MIME-Type nun nicht mehr text/plain ist sondern application/json. Das Format des Anfragenbodys,  sein MIME-Type (Multipurpose Internet Mail Extensions) deutet nun auf das Datenformat JSON hin. JSON Daten werden durch Schlüssel-Wert-Paare beschrieben, ähnlich wie ein Python Dictionary. Da hier die Parameter im Body der Anfrage transportiert werden, muss natürlich der Auswertungsalgorithmus der do_POST() Routine des Webservers auch an völlig anderer Stelle nach den Werten suchen. Somit ist eine andere, wenngleich in einigen Punkten ähnliche, Herangehensweise gefordert als bei der Übergabe der Parameter in der URL.

Hier folgt nun ein Codebeispiel mit (detaillierten) Erklärungen für das Hinzufügen eines Datensatzes (POST). Wir werden auc hier gleich sehen, dass der Code wesentlich kürzer ausfallen kann, wenn man mehrere Operationen zusammenfügt.

Der entscheidende Unterschied zur Parameterübergabe innerhalb der URL liegt hier in Zeile 114. rfile.read liest die Daten aus dem Datenstrom des Bodys.

Und hier das "zusammengeschrumpfte Beispiel" mit gleicher Funktion:

Mit dem ersten Codesnippet erscheinen folgende Systemmeldungen auf der Webserverkommandozeile

Und hier die beim Client eintreffende Rückantwort. Die Antwort auf einen POST soll immer den neu angelegten Datensatz beinhalten. Hier ist die Methode append_data(temp, ort) erweitert worden um die Speicherung einer ID und der aktuellen Zeit bei der Abspeicherung. Dieser komplette Datensatz ist dann auch der Rückgabewert von append_data().

Code von append_data()

Und hier das Hauptrogramm im Überblick.

Der fertige Code mit den zwei API-Methoden für GET und POST zum Experimentieren und gerne erweitern:

```# Python 3 Webserver mit REST API

from http.server import HTTPServer, BaseHTTPRequestHandler
from time import ctime, time
from urllib.parse import urlparse, parse_qs
import json
from pathlib import Path

hostName = "10.2.5.140"
serverPort = 8080
endpoint_temps = "/raumtemps"
temps_fp =Path("/home/pi/temps.json")
tempset = {
            "id": "",
            "zeit": "",
            "ort": "",
            "temp": ""
        }
templist = []
error_msg = {
        "problem": "",
        "nachricht": "",
        "zeit": ""
    }
response=""

def get_data_from_file():
      global templist
      if temps_fp.is_file():
        print ("Opening file...")
        with open(temps_fp, 'r') as read_file:
          templist = json.load(read_file)
def save_data_to_file():
    # body of destructor, Datensätze sichern
      global templist
      temps_fp.touch(exist_ok=True)
      print ("Writing templist to file...")
      with open(temps_fp, 'w') as write_file:
        json.dump(templist, write_file)

class MyServer(BaseHTTPRequestHandler):
   # def __init__(self):

    def do_GET(self):
        global templist
        global temp_set
        global error_msg

        print("Habe einen Request:")
        endpoint=(urlparse(self.path)).path # Endpunkt bestimmen
        if endpoint[len(endpoint)-1]== '/':
           endpoint=endpoint[:-1] # falls jemand am Ende einen / eingegeben hat, muss der weg
        if endpoint.startswith(endpoint_temps):
            # Endpunkt für Temperaturbearbeitung
            # nun Endpunkt beim / trennen, um herauszufinden, ob alle Datensätze oder ein spezieller
            # geliefert werden sollen (/raumtemps oder /raumtemps/7)
            if (len(endpoint.split("/")) == 2):
                # Nur Basispfad angegeben --> Alle Daten senden
                response = json.dumps (templist)
                self.send_response(200)
            elif (len(endpoint.split("/")) > 2) and not(str.isdigit(endpoint.split("/")[2])):
              # id ist nicht integer
                error_msg["problem"]="Datensatz nicht gefunden!"
                error_msg["nachricht"]="Die angegebene id " + (endpoint.split("/"))[2] + " ist keine Ganzzahl!"
                error_msg["zeit"]=ctime(time())
                response = json.dumps(error_msg)
                self.send_response(400) # 400 Bad Request

            else:
                # Bestimmten Datensatz suchen und senden
                id_set=int((endpoint.split("/"))[2])
                print("id_set: ", id_set)
                temp_set=list(filter(lambda data: data['id'] == id_set, templist))
                print("temp_set: ", temp_set)
                # Suchen in einer List of Dictionaries siehe
                # https://stackoverflow.com/questions/8653516/python-list-of-dictionaries-search#comment18634157_8653568
                if len(temp_set) > 0:
                    # angefragten Datensatz gefunden, Ergebnis ist eine Liste
                    response=json.dumps(temp_set[0])
                    self.send_response(200)

                else:
                    # nicht gefunden
                    error_msg["problem"]="Datensatz nicht gefunden!"
                    error_msg["nachricht"]="Der Datensatz mit der id " + str(id_set) + " existiert nicht im System!"
                    error_msg["zeit"]=ctime(time())
                    response = json.dumps(error_msg)
                    self.send_response(400) # 400 Bad Request
        else:
            # Keinen gültigen Endpunkt gefunden
            error_msg["problem"]="Fehlerhafte URL"
            error_msg["nachricht"]="Der Endpunkt " + endpoint + " existiert nicht im System!"
            error_msg["zeit"]=ctime(time())
            response = json.dumps(error_msg) # Datensatz von Dictionary in String wandeln (Serialize)
            self.send_response(404) # Nachricht über den Misserfolg, der eingegangenen Anfrage, 404 Not Found

        self.send_header("Content-type", "application/json")  # Headerinformation über den Datentyp, der im Body zurückgesendet wird
        self.end_headers() # Header zu Ende
        self.wfile.write(bytes(response, 'utf-8')) # Die Rückmeldung an den Client über den body der Webseite

    def do_POST(self):
        # Übergabe der Daten über den Body mit "Content-type", "application/json"
        post_body_dict = json.loads(self.rfile.read(int(self.headers.get('content-length', 0))))
        # Bodylänge, Auslesen und Umwandeln des JSON-Strings aus dem Body in Dictionary (Deserialize) in einem Zug
        t= str(post_body_dict["temp"])  # Der Wert, der im Body mit Parameter "temp" übergeben wurde,
        o= str(post_body_dict["ort"]) # Der Wert, der im Body mit Parameter "ort" übergeben wurde
        appended_data=self.append_data (o,t) # diese Methode speichert die Daten irgendwo hin (muss noch geschrieben werden)
        self.send_response(200) # Nachricht über den Erfolg, der eingegangenen Anfrage, 200=OK, die Anfrage ist erfolgreich,
        self.send_header("Content-type", "application/json") # Datentyp im Body der zurückgelieferten Webseite,
        self.end_headers() # mit diesem Befehl wird der Header an den Client zurückgesendet
        response = json.dumps(appended_data) # Datensatz von Dictionary in String wandeln (Serialize)
        self.wfile.write(bytes(response, 'utf-8')) # Die Rückmeldung an den Client über den body der Webseite
    def append_data (self,ort, temp):
        global templist
        global tempset
        if len(templist) == 0:
            tempset["id"] = 1 # vorher wurden noch keine Daten in der Liste gespeichert
        else:
            tempset["id"] = max(tdict['id'] for tdict in templist) + 1 # Größte id finden und inkrementieren
        tempset["zeit"] = ctime(time()) # ctime() https://docs.python.org/3/library/datetime.html?highlight=ctime#datetime.date.ctime
        tempset["ort"] = ort
        tempset["temp"] = temp
        templist.append(dict(tempset)) # anfügen einer Kopie (dict()) des Datensatzes an die Liste
        return dict(tempset) # eine Kopie des Datensatzes mit Zusatzinfos Zeit und ID zurückgeben

if __name__ == "__main__":
    webServer = HTTPServer((hostName, serverPort), MyServer)
    print("Server started http://%s:%s" % (hostName, serverPort))
    get_data_from_file() # Datenbank auslesen

    try:
        webServer.serve_forever()
    except KeyboardInterrupt:
        pass
    save_data_to_file() # Datenbank wieder speichern
    webServer.server_close()
    print("Server stopped.")
```
Er liefert folgende Ergebnisse:

Tools zum Senden von Anfragen

Das wohl bekannteste Tool ist curl. Es existiert für viele Betriebssysteme. Infos darüber z. B. unter https://thomas-falkner.de/2011/01/25/curl-mit-6-praktischen-beispielen-erklaert/  oder https://www.ionos.de/digitalguide/server/tools/einstieg-in-curl-in-windows/.

Curl Aufruf für einen POST

```curl -X POST --header 'Content-Type: application/json' -d '{ "ort": "Raum 2.18", "temp": "25.9"}' 'http://10.2.5.140:8080/raumtemps'```

Auch die Werkzeuge Insomnia oder Postman werden häufig eingesetzt.

Ein übersichtliches Tool ist eine Extension für Visual Studio Code: "REST Client".

Einfach in den Extensions bei VS Code suchen und installieren. Der Webquest steht dann immer zwischen ### und ### und kann mit Rechtsklick auf die Anfrage verschickt werden. Die Antwort des Servers erscheint dann in einem zweiten Fenster.

Verweise

Response Codes: https://developer.mozilla.org/de/docs/Web/HTTP/Status

MIME Types: https://de.wikipedia.org/wiki/Internet_Media_Type