5 Möglichkeiten, einen Python-Server auf einem Raspberry Pi zu erstellen Teil 2

Hallo Habr.

Heute werden wir weiterhin die Netzwerkfähigkeiten des Raspberry Pi bzw. deren Implementierung in Python untersuchen. Im ersten Teil haben wir die Grundfunktionen des einfachsten Webservers untersucht, der auf dem Raspberry Pi ausgeführt wird. Jetzt werden wir noch weiter gehen und verschiedene Möglichkeiten in Betracht ziehen, um unseren Server interaktiv zu gestalten.



Der Artikel ist für Anfänger gedacht.

Bevor Sie ein paar Notizen beginnen.

Zunächst bezweifelte ich selbst, ob es sich lohnt, eine Fortsetzung zu machen, und erwartete einen größeren Fluss an Kritik und niedrigen Bewertungen, aber wie die Umfrage im ersten Teil ergab, fanden 85% der Leser die dort bereitgestellten Informationen nützlich. Ich verstehe, dass einige Pro-Artikel für Dummies nervig sind, aber alle haben einmal angefangen, also muss man warten.

Zweitens werde ich über Programmierung schreiben, nicht über Administration. Daher werden die Probleme bei der Konfiguration von Raspbian, Konfigurationen, VPN, Sicherheit und anderen Dingen hier nicht berücksichtigt. Obwohl dies auch wichtig ist, kann man das Unermessliche nicht annehmen. Es geht nur um Python und darum, wie man einen Server darauf erstellt.

Wer nicht interessiert ist, kann jetzt im Browser auf den Zurück-Button klicken und seine wertvolle Zeit nicht verschwenden;)

Und wir werden anfangen.

Ich möchte Sie daran erinnern, dass wir im vorherigen Teil einen einfachen Webserver auf einem Raspberry Pi gestartet haben, der eine statische Seite zeigt:

Bild

Jetzt werden wir weiter gehen und unseren Server interaktiv machen und der Webseite eine LED-Steuerung hinzufügen. Natürlich kann es anstelle der LED jedes andere Gerät geben, das von GPIO gesteuert werden kann, aber mit der LED ist es am einfachsten, ein Experiment durchzuführen.

Vorbereitung


Ich werde nicht beschreiben, wie man die LED an den Raspberry Pi anschließt, jeder kann sie in 5 Minuten bei Google finden. Wir werden mehrere Funktionen für die gleichzeitige Verwendung von GPIO schreiben, die wir dann in unseren Server einfügen.

try: import RPi.GPIO as GPIO except ModuleNotFoundError: pass led_pin = 21 def raspberrypi_init(): try: GPIO.setmode(GPIO.BCM) GPIO.setup(led_pin, GPIO.OUT) except: pass def rasperrypi_pinout(pin: int, value: bool): print("LED ON" if value else "LED OFF") try: GPIO.output(pin, value) except: pass def rasperrypi_cleanup(): try: GPIO.cleanup() except: pass 

Wie Sie sehen können, ist jede GPIO-Aufruffunktion in einen Try-Catch-Block "eingeschlossen". Warum wird das gemacht? Auf diese Weise können Sie den Server auf jedem PC, einschließlich Windows, debuggen, was sehr praktisch ist. Jetzt können wir diese Funktionen in den Webserver-Code einfügen.

Unsere Aufgabe ist es, der Webseite Schaltflächen hinzuzufügen, mit denen wir die LED über den Browser steuern können. Es werden 3 Arten der Implementierung betrachtet.

Methode 1: Falsch


Diese Methode kann nicht als schön bezeichnet werden, ist aber kurz und am einfachsten zu verstehen.

Erstellen Sie eine Zeile mit einer HTML-Seite.

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <body> <h2>Hello from the Raspberry Pi!</h2> <p><a href="/led/on"><button class="button button_led">Led ON</button></a></p> <p><a href="/led/off"><button class="button button_led">Led OFF</button></a></p> </body> </html>''' 

Hier sind 3 Punkte zu beachten:

  • Wir verwenden CSS, um den Stil der Schaltflächen festzulegen. Sie könnten dies nicht tun und mit nur 4 Zeilen HTML-Code auskommen, aber dann würde unsere Seite wie "Grüße aus den 90ern" aussehen:

  • Für jede Schaltfläche erstellen wir einen lokalen Link wie / led / on und / led / off
  • Das Mischen von Ressourcen und Code ist ein schlechter Programmierstil, und im Idealfall wird HTML am besten vom Python-Code getrennt. Mein Ziel ist es jedoch, minimal funktionierenden Code anzuzeigen, in dem ein Minimum an Überflüssigkeit vorhanden ist, sodass einige Dinge der Einfachheit halber weggelassen werden. Darüber hinaus ist es praktisch, wenn der Code einfach aus einem Artikel kopiert werden kann, ohne dass zusätzliche Dateien erforderlich sind.

Wir haben den Server bereits im vorherigen Teil untersucht. Es bleibt noch die Verarbeitung der Zeilen '/ led / on' und '/ led / off' hinzuzufügen. Der gesamte Code wurde aktualisiert:

 from http.server import BaseHTTPRequestHandler, HTTPServer class ServerHandler(BaseHTTPRequestHandler): def do_GET(self): print("GET request, Path:", self.path) if self.path == "/" or self.path.endswith("/led/on") or self.path.endswith("/led/off"): if self.path.endswith("/led/on"): rasperrypi_pinout(led_pin, True) if self.path.endswith("/led/off"): rasperrypi_pinout(led_pin, False) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) else: self.send_error(404, "Page Not Found {}".format(self.path)) def server_thread(port): server_address = ('', port) httpd = HTTPServer(server_address, ServerHandler) try: httpd.serve_forever() except KeyboardInterrupt: pass httpd.server_close() if __name__ == '__main__': port = 8000 print("Starting server at port %d" % port) raspberrypi_init() server_thread(port) rasperrypi_cleanup() 

Wir starten es und wenn alles richtig gemacht wurde, können wir die LED über unseren Webserver steuern:



Sie können den Server nicht nur unter Raspberry Pi, sondern auch unter Windows oder OSX testen. In der Konsole werden LED-EIN- und LED-AUS-Meldungen angezeigt, wenn Sie auf die entsprechende Schaltfläche klicken:



Jetzt finden wir heraus, warum diese Methode schlecht ist und warum sie "falsch" ist. Dieses Beispiel funktioniert sehr gut und wird häufig in verschiedenen Tutorials kopiert. Es gibt jedoch zwei Probleme: Erstens ist es falsch, die gesamte Seite neu zu laden, wenn nur die LED aufleuchten soll. Das ist aber immer noch das halbe Problem. Das zweite und schwerwiegendere Problem ist, dass beim Drücken der Taste zum Einschalten der LED die Seitenadresse http://192.168.1.106:8000/led/on lautet . Browser erinnern sich normalerweise an die zuletzt geöffnete Seite, und wenn Sie das nächste Mal den Browser öffnen, funktioniert der Befehl zum Einschalten der LED wieder, auch wenn wir dies nicht wollten. Daher werden wir zum nächsten, korrekteren Weg übergehen.

Methode 2: Richtig


Um alles richtig zu machen, setzen wir die Ein- und Ausschaltfunktionen der LED in separaten Anforderungen aus und rufen sie mit Javascript asynchron auf. Der HTML-Code für die Seite sieht nun folgendermaßen aus:

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <script type="text/javascript" charset="utf-8"> function httpGetAsync(method, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function() { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) callback(xmlHttp.responseText); } xmlHttp.open("GET", window.location.href + method, true); xmlHttp.send(null); } function ledOn() { console.log("Led ON..."); httpGetAsync("led/on", function(){ console.log("Done"); }); } function ledOff() { console.log("Led OFF..."); httpGetAsync("led/off", function(){ console.log("Done"); }); } </script> <body> <h2>Hello from the Raspberry Pi!</h2> <p><button class="button button_led" onclick="ledOn();">Led ON</button></p> <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p> </body> </html>''' 

Wie Sie sehen, haben wir href aufgegeben und die Funktionen ledOn und ledOff aufgerufen, die wiederum die entsprechenden Servermethoden asynchron aufrufen (asynchrone Methoden sind erforderlich, damit die Seite nicht blockiert wird, bis die Antwort vom Server eintrifft).

Jetzt muss noch die Verarbeitung von Get-Anfragen zum Server hinzugefügt werden:

  def do_GET(self): print("GET request, path:", self.path) if self.path == "/": self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) elif self.path == "/led/on": self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() rasperrypi_pinout(led_pin, True) self.wfile.write(b"OK") elif self.path == "/led/off": self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() rasperrypi_pinout(led_pin, False) self.wfile.write(b"OK") else: self.send_error(404, "Page Not Found {}".format(self.path)) 

Wie Sie sehen, wird die Seite jetzt nicht mehr neu geladen, wenn Sie versuchen, die LED zu beleuchten. Jede Methode macht nur das, was sie tun sollte.

Methode 3: Richtiger


Alles scheint schon zu funktionieren. Aber natürlich kann (und sollte) der obige Code verbessert werden. Tatsache ist, dass wir GET-Anfragen verwenden, um die LED zu steuern. Dies spart uns Platz im Code, ist jedoch methodisch nicht ganz korrekt. GET-Anforderungen dienen zum Lesen von Daten vom Server, können vom Browser zwischengespeichert werden und sollten im Allgemeinen nicht zum Ändern von Daten verwendet werden. Der richtige Weg ist die Verwendung von POST (für diejenigen, die an Details interessiert sind, weitere Details hier ).

Wir werden die Aufrufe in HTML von get to post ändern. Da unser Code jedoch asynchron ist, wird der Status des Wartens auf die Antwort des Servers und der Arbeitsergebnisse angezeigt. Für ein lokales Netzwerk wird dies nicht bemerkt, aber für eine langsame Verbindung ist es sehr praktisch. Um es interessanter zu machen, werden wir JSON verwenden, um Parameter zu übergeben.

Die endgültige Version sieht folgendermaßen aus:

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <script type="text/javascript" charset="utf-8"> function httpPostAsync(method, params, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function() { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) callback(xmlHttp.responseText); else callback(`Error ${xmlHttp.status}`) } xmlHttp.open("POST", window.location.href + method, true); xmlHttp.setRequestHeader("Content-Type", "application/json"); xmlHttp.send(params); } function ledOn() { document.getElementById("textstatus").textContent = "Making LED on..."; httpPostAsync("led", JSON.stringify({ "on": true }), function(resp) { document.getElementById("textstatus").textContent = `Led ON: ${resp}`; }); } function ledOff() { document.getElementById("textstatus").textContent = "Making LED off..."; httpPostAsync("led", JSON.stringify({ "on": false }), function(resp) { document.getElementById("textstatus").textContent = `Led OFF: ${resp}`; }); } </script> <body> <h2>Hello from the Raspberry Pi!</h2> <p><button class="button button_led" onclick="ledOn();">Led ON</button></p> <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p> <span id="textstatus">Status: Ready</span> </body> </html>''' 

Unterstützung für GET- und POST-Anforderungen zum Server hinzufügen:

 import json class ServerHandler(BaseHTTPRequestHandler): def do_GET(self): print("GET request, path:", self.path) if self.path == "/": self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) else: self.send_error(404, "Page Not Found {}".format(self.path)) def do_POST(self): content_length = int(self.headers['Content-Length']) body = self.rfile.read(content_length) try: print("POST request, path:", self.path, "body:", body.decode('utf-8')) if self.path == "/led": data_dict = json.loads(body.decode('utf-8')) if 'on' in data_dict: rasperrypi_pinout(led_pin, data_dict['on']) self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write(b"OK") else: self.send_response(400, 'Bad Request: Method does not exist') self.send_header('Content-Type', 'application/json') self.end_headers() except Exception as err: print("do_POST exception: %s" % str(err)) 

Wie Sie sehen können, verwenden wir jetzt eine LED-Funktion, an die der Parameter on mit json übergeben wird, das True oder False akzeptiert (beim Aufruf in HTML wird jeweils eine json-Zeichenfolge der Form {"on": true} übertragen). Es lohnt sich auch, auf try-catch zu achten - dies verhindert, dass der Server herunterfällt, beispielsweise wenn jemand eine Zeichenfolge mit ungültigem json an den Server sendet.

Wenn alles richtig gemacht wurde, bekommen wir einen Server mit Feedback, der ungefähr so ​​aussehen sollte:



Feedback, in unserem Fall die Meldung "OK", ermöglicht es Ihnen, die Bestätigung vom Server zu sehen, dass der Code tatsächlich verarbeitet wurde.

Kann dieser Server noch verbessert werden? Sie können beispielsweise sinnvoll sein, die Verwendung der Druckfunktion durch die Verwendung der Protokollierung zu ersetzen. Dies ist korrekter und ermöglicht es Ihnen, Serverprotokolle nicht nur auf dem Bildschirm anzuzeigen, sondern auch, wenn Sie sie in eine Datei mit automatischer Rotation schreiben möchten. Wer möchte, kann dies selbst tun.

Fazit


Wenn alles richtig gemacht wurde, erhalten wir einen Mini-Server, mit dem Sie die LED oder ein anderes Gerät über einen Browser von jedem Netzwerkgerät aus steuern können.

Wichtig: Sicherheitsvorkehrungen

Ich stelle noch einmal fest, dass es hier keinen Schutz oder keine Authentifizierung gibt, daher sollten Sie eine solche Seite nicht im Internet "posten", wenn Sie vorhaben, eine mehr oder weniger verantwortungsvolle Last zu verwalten. Obwohl mir die Fälle von Angriffen auf solche Server unbekannt sind, lohnt es sich immer noch nicht, jemandem etwas zu geben, der das Garagentor aus der Ferne öffnen oder die Kilowattheizung einschalten möchte. Wenn Sie eine Fernsteuerung über eine solche Seite wünschen, lohnt es sich, ein VPN oder ähnliches einzurichten.

Abschließend wiederhole ich, dass das Material für Anfänger gedacht ist, und ich hoffe, dass dies mehr oder weniger nützlich war. Es ist klar, dass nicht jeder auf Khabr mit der Verfügbarkeit von Artikeln "für Dummies" zufrieden ist. Ob der nächste Teil also von den Abschlussnoten abhängt oder nicht. Wenn dies der Fall ist, werden die Flask- und WSGI-Frameworks sowie die grundlegenden Authentifizierungsmethoden berücksichtigt.

Alle erfolgreichen Experimente.

Source: https://habr.com/ru/post/de472464/


All Articles