5 formas de hacer un servidor Python en una Raspberry Pi Parte 2

Hola Habr

Hoy continuaremos estudiando las capacidades de red de Raspberry Pi, o más bien su implementación en Python. En la primera parte, examinamos las funciones básicas del servidor web más simple que se ejecuta en Raspberry Pi. Ahora iremos más allá y consideraremos varias formas de hacer que nuestro servidor sea interactivo.



El artículo está diseñado para principiantes.

Antes de comenzar un par de notas.

En primer lugar, yo mismo dudaba si continuaría y esperaba un mayor flujo de críticas y bajas calificaciones, pero como lo mostró la encuesta en la primera parte, el 85% de los lectores encontraron útil la información proporcionada allí. Entiendo que algunos artículos profesionales para "dummies" son molestos, pero todos comenzaron una vez, así que tienes que esperar.

En segundo lugar, escribiré sobre programación, no sobre administración. Entonces, los problemas de configuración de Raspbian, configuraciones, VPN, seguridad y otras cosas no se considerarán aquí. Aunque esto también es importante, uno no puede abrazar lo inmenso. Solo se tratará de Python y de cómo crear un servidor en él.

Aquellos que no estén interesados ​​pueden hacer clic en el botón Atrás en el navegador en este momento y no perder su valioso tiempo;)

Y comenzaremos.

Permítame recordarle que en la parte anterior terminamos lanzando un servidor web simple en la Raspberry Pi, que muestra una página estática:

imagen

Ahora iremos más allá y haremos que nuestro servidor sea interactivo, agreguemos control LED a la página web. Por supuesto, en lugar del LED, puede haber cualquier otro dispositivo que pueda ser controlado por GPIO, pero con el LED es más fácil realizar un experimento.

Preparación


No describiré cómo conectar el LED a la Raspberry Pi, cualquiera puede encontrarlo en Google en 5 minutos. Escribiremos varias funciones para usar GPIO a la vez, que luego insertaremos en nuestro servidor.

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 

Como puede ver, cada función de llamada GPIO está "envuelta" en un bloque try-catch. ¿Por qué se hace esto? Esto le permite depurar el servidor en cualquier PC, incluido Windows, lo cual es bastante conveniente. Ahora podemos insertar estas funciones en el código del servidor web.

Nuestra tarea es agregar botones a la página web que nos permitan controlar el LED desde el navegador. Se considerarán 3 formas de implementación.

Método 1: incorrecto


Este método no se puede llamar hermoso, pero es corto y el más fácil de entender.

Crea una línea con una página HTML.

 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>''' 

Hay 3 puntos a tener en cuenta aquí:

  • Usamos CSS para especificar el estilo de los botones. No podría hacer esto y llevarse bien con solo 4 líneas de código HTML, pero luego nuestra página se vería como "saludos desde los 90":

  • Para cada botón, creamos un enlace local como / led / on y / led / off
  • Mezclar recursos y código es un mal estilo de programación, e idealmente, HTML se mantiene separado del código de Python. Pero mi objetivo es mostrar un código de trabajo mínimo en el que haya un mínimo de superfluo, por lo que algunas cosas se omiten por simplicidad. Además, es conveniente cuando el código simplemente se puede copiar de un artículo, sin problemas con archivos adicionales.

Ya examinamos el servidor en la parte anterior, queda por agregar el procesamiento de las líneas '/ led / on' y '/ led / off'. Código completo actualizado:

 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() 

Lo iniciamos, y si todo se hizo correctamente, entonces podemos controlar el LED a través de nuestro servidor web:



Puede probar el servidor no solo en Raspberry Pi, sino también en Windows u OSX, en la consola habrá mensajes LED ON, LED OFF cuando haga clic en el botón correspondiente:



Ahora descubrimos por qué este método es malo y por qué está "mal". Este ejemplo funciona bastante y, a menudo, se copia en diferentes tutoriales. Pero hay dos problemas: en primer lugar, es incorrecto volver a cargar toda la página cuando solo queremos iluminar el LED. Pero esto sigue siendo la mitad del problema. El segundo problema, y ​​más grave, es que cuando presionamos el botón para encender el LED, la dirección de la página se convierte en http://192.168.1.106:8000/led/on . Los navegadores generalmente recuerdan la última página abierta, y la próxima vez que abra el navegador, el comando para encender el LED volverá a funcionar, incluso si no quisiéramos. Por lo tanto, pasaremos a la siguiente forma más correcta.

Método 2: derecho


Para hacer todo bien, colocamos las funciones de encendido y apagado del LED en solicitudes separadas, y las llamaremos asincrónicamente usando Javascript. El código HTML de la página ahora se verá así:

 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>''' 

Como puede ver, abandonamos href y llamamos a las funciones ledOn y ledOff, que a su vez invocan los métodos del servidor correspondientes de forma asincrónica (se necesitan métodos asincrónicos para que la página no se bloquee hasta que llegue la respuesta del servidor).

Ahora queda por agregar el procesamiento de las solicitudes de obtención al servidor:

  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)) 

Ahora, como puede ver, la página ya no se recarga cuando intenta encender el LED, cada método solo hace lo que debería hacer.

Método 3: más correcto


Todo parece estar funcionando ya. Pero, por supuesto, el código anterior puede (y debería) mejorarse. El hecho es que usamos solicitudes GET para controlar el LED. Esto nos ahorra espacio en el código, pero metodológicamente esto no es del todo correcto: las solicitudes GET están diseñadas para leer datos del servidor, el navegador puede almacenarlas en caché y, por lo general, no deben usarse para modificar datos. La forma correcta es utilizar POST (para aquellos que estén interesados ​​en los detalles, más detalles aquí ).

Cambiaremos las llamadas en HTML de llegar a publicar, pero al mismo tiempo, dado que el código es asíncrono, mostraremos el estado de la respuesta del servidor y los resultados. Para una red local esto no se notará, pero para una conexión lenta es muy conveniente. Para hacerlo más interesante, usaremos JSON para pasar parámetros.

La versión final se ve así:

 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>''' 

Agregue soporte para solicitudes GET y POST al servidor:

 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)) 

Como puede ver, ahora usamos una función led, en la cual el parámetro "on" se pasa usando json, que acepta True o False (cuando se llama en HTML, se transmite una cadena json de la forma {"on": true}, respectivamente). También vale la pena prestar atención a try-catch: esto bloquea el servidor, por ejemplo, si alguien envía una cadena con json no válido al servidor.

Si todo se hizo correctamente, obtenemos un servidor con comentarios, que debería verse así:



La retroalimentación, en nuestro caso, el mensaje "OK", le permite ver la confirmación del servidor de que el código realmente ha sido procesado.

¿Se puede mejorar este servidor todavía? Puede, por ejemplo, tener sentido reemplazar el uso de la función de impresión con el uso del registro, esto es más correcto y le permite mostrar los registros del servidor no solo en la pantalla, sino también si desea escribirlos en un archivo con rotación automática. Los que lo deseen pueden hacerlo por su cuenta.

Conclusión


Si todo se hizo correctamente, obtendremos un mini servidor que le permite controlar el LED o cualquier otro dispositivo a través de un navegador desde cualquier dispositivo de red.

Importante: precauciones de seguridad

Una vez más, observo que no hay protección o autenticación aquí, por lo que no debe "publicar" dicha página en Internet si planea administrar una carga más o menos responsable. Aunque desconozco los casos de ataques en dichos servidores, todavía no vale la pena dar a nadie que quiera poder abrir remotamente la puerta del garaje o encender el calentador de kilovatios. Si desea controlar de forma remota a través de dicha página, vale la pena configurar una VPN o algo similar.

En conclusión, repito que el material está diseñado para principiantes, y espero que esto sea más o menos útil. Está claro que no todos en Khabr están satisfechos con la disponibilidad de artículos "para tontos", por lo que si la siguiente parte dependerá o no de las calificaciones finales. Si lo hay, considerará los marcos Flask y WSGI, así como los métodos básicos de autenticación.

Todos los experimentos exitosos.

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


All Articles