Hébergez votre site Web avec du contenu dynamique et une base de données sur un Raspberry Pi
Vous pouvez utiliser un logiciel gratuit pour prendre en charge une application Web sur un ordinateur très léger.
Les machines monocarte de Raspberry Pi ont établi la référence en matière d'informatique bon marché et réelle. Avec son modèle 4, le Raspberry Pi peut héberger des applications Web avec un serveur Web de niveau production, un système de base de données transactionnelle et du contenu dynamique via des scripts. Cet article explique les détails d'installation et de configuration avec un exemple de code complet. Bienvenue dans les applications Web hébergées sur un ordinateur très léger.
L'application chutes de neige
Imaginez un domaine de ski alpin suffisamment grand pour avoir des microclimats, ce qui peut entraîner des chutes de neige radicalement différentes dans la région. La zone est divisée en régions, chacune disposant d'appareils qui enregistrent les chutes de neige en centimètres ; les informations enregistrées guident ensuite les décisions concernant l'enneigement, le toilettage et d'autres opérations d'entretien. Les appareils communiquent, par exemple, toutes les 20 minutes avec un serveur qui met à jour une base de données prenant en charge les rapports. De nos jours, le logiciel côté serveur pour une telle application peut être gratuit et de qualité production.
Cette application de chute de neige utilise les technologies suivantes :
- Un Raspberry Pi 4 sous Debian
- Serveur Web Nginx : La version gratuite héberge plus de 400 millions de sites Web. Ce serveur Web est facile à installer, à configurer et à utiliser.
- Système de base de données relationnelle SQLite, basé sur des fichiers : une base de données, qui peut contenir de nombreuses tables, est un fichier sur le système local. SQLite est léger mais également conforme à ACID ; il convient aux volumes faibles à modérés. SQLite est probablement le système de base de données le plus utilisé au monde et le code source de SQLite est dans le domaine public. La version actuelle est la 3. Une option plus puissante (mais toujours gratuite) est PostgreSQL.
- Python : Le langage de programmation Python peut interagir avec des bases de données telles que SQLite et des serveurs Web tels que Nginx. Python (version 3) est fourni avec les systèmes Linux et macOS.
Python inclut un pilote logiciel pour communiquer avec SQLite. Il existe des options pour connecter des scripts Python à Nginx et à d'autres serveurs Web. Une option est uWSGI (Web Server Gateway Interface), qui met à jour l'ancien CGI (Common Gateway Interface) des années 1990.
Plusieurs facteurs plaident en faveur de uWSGI :
- uWSGI est flexible. Il peut être utilisé soit comme serveur Web simultané léger, soit comme serveur d'applications back-end connecté à un serveur Web tel que Nginx.
- Sa configuration est minime.
- L'application Snowfall implique un volume faible à modéré de visites sur le serveur Web et le système de base de données. En général, les technologies CGI ne sont pas rapides par rapport aux normes modernes, mais CGI fonctionne suffisamment bien pour les applications Web au niveau département telles que celle-ci.
Divers acronymes décrivent l'option uWSGI. Voici un aperçu des trois principaux :
- WSGI est une spécification Python pour une interface entre un serveur Web d'un côté et une application ou un framework d'application (par exemple, Django) de l'autre côté. Cette spécification définit une API dont l'implémentation est laissée ouverte.
- uWSGI implémente l'interface WSGI en fournissant un serveur d'applications qui connecte les applications à un serveur Web. La tâche principale d'un serveur d'applications uWSGI consiste à traduire les requêtes HTTP dans un format qu'une application Web peut consommer et, ensuite, à formater la réponse de l'application dans un message HTTP.
- uwsgi est un protocole binaire implémenté par un serveur d'applications uWSGI pour communiquer avec un serveur Web complet tel que Nginx ; il comprend également des utilitaires tels qu'un serveur Web léger. Le serveur Web Nginx "parle" uwsgi dès le départ.
Pour plus de commodité, j'utiliserai "uwsgi" comme raccourci pour le protocole binaire, le serveur d'applications et le serveur Web très léger.
Mise en place de la base de données
Sur un système basé sur Debian, vous pouvez installer SQLite de la manière habituelle (avec %
représentant l'invite de ligne de commande) :
% sudo apt-get install sqlite3
Ce système de base de données est une collection de bibliothèques et d'utilitaires C, d'une taille d'environ 500 Ko. Il n'y a aucun serveur de base de données à démarrer, arrêter ou maintenir.
Une fois SQLite installé, créez une base de données à l'invite de ligne de commande :
% sqlite3 snowfall.db
Si cela réussit, la commande crée le fichier snowfall.db
dans le répertoire de travail actuel. Le nom de la base de données est arbitraire (par exemple, aucune extension n'est requise) et la commande ouvre l'utilitaire client SQLite avec >sqlite
comme invite :
Enter ".help" for usage hints.
sqlite>
Créez la table des chutes de neige dans la base de données des chutes de neige avec la commande suivante. Le nom de la table, comme celui de la base de données, est arbitraire :
sqlite> CREATE TABLE snowfall (id INTEGER PRIMARY KEY AUTOINCREMENT,
region TEXT NOT NULL,
device TEXT NOT NULL,
amount DECIMAL NOT NULL,
tstamp DECIMAL NOT NULL);
Les commandes SQLite ne sont pas sensibles à la casse, mais il est traditionnel d'utiliser des majuscules pour les termes SQL et des minuscules pour les termes utilisateur. Vérifiez que la table a été créée :
sqlite> .schema
La commande fait écho à l'instruction CREATE TABLE
.
La base de données est maintenant prête à fonctionner, même si la chute de neige d'une seule table est vide. Vous pouvez ajouter des lignes de manière interactive au tableau, mais un tableau vide convient pour le moment.
Un premier aperçu de l'architecture globale
Rappelons que uwsgi peut être utilisé de deux manières : soit comme serveur Web léger, soit comme serveur d'applications connecté à un serveur Web de production tel que Nginx. La deuxième utilisation est l'objectif, mais la première est adaptée au développement et au test du code de gestion des requêtes du programmeur. Voici l'architecture avec Nginx en jeu comme serveur web :
HTTP uwsgi
client<---->Nginx<----->appServer<--->request-handling code<--->SQLite
Le client peut être un navigateur, un utilitaire tel que curl ou un programme artisanal maîtrisant HTTP. Les communications entre le client et Nginx s'effectuent via HTTP, mais uwsgi prend ensuite le relais en tant que protocole de transport binaire entre Nginx et le serveur d'applications, qui interagit avec le code de gestion des requêtes tel que requestHandler.py
(décrit ci-dessous ). Cette architecture offre une division nette du travail. Nginx gère seul le client et seul le code de gestion des requêtes interagit avec la base de données. À son tour, le serveur d'applications sépare le serveur Web du code écrit par le programmeur, qui dispose d'une API de haut niveau pour lire et écrire les messages HTTP transmis via uwsgi.
J'examinerai ces éléments architecturaux et couvrirai les étapes d'installation, de configuration et d'utilisation d'uwsgi et Nginx dans les sections suivantes.
Le code d'application des chutes de neige
Vous trouverez ci-dessous le fichier de code source requestHandler.py
pour l'application Snowfall. (Il est également disponible sur mon site Web.) Différentes fonctions de ce code aident à clarifier l'architecture logicielle qui connecte SQLite, Nginx et uwsgi.
Le programme de traitement des requêtes
import sqlite3
import cgi
PATH_2_DB = '/home/marty/wsgi/snowfall.db'
## Dispatches HTTP requests to the appropriate handler.
def application(env, start_line):
if env['REQUEST_METHOD'] == 'POST': ## add new DB record
return handle_post(env, start_line)
elif env['REQUEST_METHOD'] == 'GET': ## create HTML-fragment report
return handle_get(start_line)
else: ## no other option for now
start_line('405 METHOD NOT ALLOWED', [('Content-Type', 'text/plain')])
response_body = 'Only POST and GET verbs supported.'
return [response_body.encode()]
def handle_post(env, start_line):
form = get_field_storage(env) ## body of an HTTP POST request
## Extract fields from POST form.
region = form.getvalue('region')
device = form.getvalue('device')
amount = form.getvalue('amount')
tstamp = form.getvalue('tstamp')
## Missing info?
if (region is not None and
device is not None and
amount is not None and
tstamp is not None):
add_record(region, device, amount, tstamp)
response_body = "POST request handled.\n"
start_line('201 OK', [('Content-Type', 'text/plain')])
else:
response_body = "Missing info in POST request.\n"
start_line('400 Bad Request', [('Content-Type', 'text/plain')])
return [response_body.encode()]
def handle_get(start_line):
conn = sqlite3.connect(PATH_2_DB) ## connect to DB
cursor = conn.cursor() ## get a cursor
cursor.execute("select * from snowfall")
response_body = "<h3>Snowfall report</h3><ul>"
rows = cursor.fetchall()
for row in rows:
response_body += "<li>" + str(row[0]) + '|' ## primary key
response_body += row[1] + '|' ## region
response_body += row[2] + '|' ## device
response_body += str(row[3]) + '|' ## amount
response_body += str(row[4]) + "</li>" ## timestamp
response_body += "</ul>"
conn.commit() ## commit
conn.close() ## cleanup
start_line('200 OK', [('Content-Type', 'text/html')])
return [response_body.encode()]
## Add a record from a device to the DB.
def add_record(reg, dev, amt, tstamp):
conn = sqlite3.connect(PATH_2_DB) ## connect to DB
cursor = conn.cursor() ## get a cursor
sql = "INSERT INTO snowfall(region,device,amount,tstamp) values (?,?,?,?)"
cursor.execute(sql, (reg, dev, amt, tstamp)) ## execute INSERT
conn.commit() ## commit
conn.close() ## cleanup
def get_field_storage(env):
input = env['wsgi.input']
form = env.get('wsgi.post_form')
if (form is not None and form[0] is input):
return form[2]
fs = cgi.FieldStorage(fp = input,
environ = env,
keep_blank_values = 1)
return fs
Une constante au début du fichier source définit le chemin d'accès au fichier de base de données :
PATH_2_DB = '/home/marty/wsgi/snowfall.db'
Assurez-vous de mettre à jour le chemin de votre Raspberry Pi.
Comme indiqué précédemment, uwsgi inclut un serveur Web léger qui peut héberger cette application de gestion des requêtes. Pour commencer, installez uwsgi avec ces deux commandes (##
introduit mes commentaires) :
% sudo apt-get install build-essential python-dev ## C header files, etc.
% pip install uwsgi ## pip = Python package manager
Ensuite, lancez une application simple de chute de neige en utilisant uwsgi comme serveur Web :
% uwsgi --http 127.0.0.1:9999 --wsgi-file requestHandler.py
L'indicateur --http
exécute uwsgi en mode serveur Web, avec 9999 comme port d'écoute du serveur Web sur localhost (127.0.0.1). Par défaut, uwsgi distribue les requêtes HTTP à une fonction définie par le programmeur nommée application
. Pour examen, voici la fonction complète en haut du code requestHandler.py
:
def application(env, start_line):
if env['REQUEST_METHOD'] == 'POST': ## add new DB record
return handle_post(env, start_line)
elif env['REQUEST_METHOD'] == 'GET': ## create HTML-fragment report
return handle_get(start_line)
else: ## no other option for now
start_line('405 METHOD NOT ALLOWED', [('Content-Type', 'text/plain')])
response_body = 'Only POST and GET verbs supported.'
return [response_body.encode()]
L'application chutes de neige n'accepte que deux types de demandes :
- Une requête POST, si elle est à la hauteur, crée une nouvelle entrée dans la table des chutes de neige. La demande doit inclure la région du domaine skiable, l'appareil dans la région, la quantité de neige en centimètres et un horodatage de style Unix. Une requête POST est envoyée à la fonction
handle_post
(que je clarifierai sous peu). - Une requête GET renvoie un fragment HTML (une liste non ordonnée) avec les enregistrements actuellement dans la table des chutes de neige.
Les requêtes avec un verbe HTTP autre que POST et GET généreront un message d'erreur.
Vous pouvez utiliser un utilitaire tel que curl pour générer des requêtes HTTP à des fins de test. Voici trois exemples de requêtes POST pour commencer à remplir la base de données :
% curl -X POST -d "region=R1&device=D9&amount=1.42&tstamp=1604722088.0158753" localhost:9999/
% curl -X POST -d "region=R7&device=D4&amount=2.11&tstamp=1604722296.8862638" localhost:9999/
% curl -X POST -d "region=R5&device=D1&amount=1.12&tstamp=1604942236.1013834" localhost:9999/
Ces commandes ajoutent trois enregistrements au tableau des chutes de neige. Une requête GET ultérieure de curl ou d'un navigateur affiche un fragment HTML qui répertorie les lignes du tableau des chutes de neige. Voici l'équivalent en texte non HTML :
Snowfall report
1|R1|D9|1.42|1604722088.0158753
2|R7|D4|2.11|1604722296.8862638
3|R5|D1|1.12|1604942236.1013834
Un rapport professionnel convertirait les horodatages numériques en horodatages lisibles par l'homme. Mais pour l’instant, l’accent est mis sur les composants architecturaux de l’application Snowfall, et non sur l’interface utilisateur.
L'utilitaire uwsgi accepte divers indicateurs, qui peuvent être donnés soit via un fichier de configuration, soit dans la commande de lancement. Par exemple, voici un lancement plus riche d'uwsgi en tant que serveur Web :
% uwsgi --master --processes 2 --http 127.0.0.1:9999 --wsgi-file requestHandler.py
Cette version crée un processus maître (supervision) et deux processus de travail, qui peuvent gérer les requêtes HTTP simultanément.
Dans l'application Snowfall, les fonctions handle_post
et handle_get
traitent respectivement les requêtes POST et GET. Voici la fonction handle_post
dans son intégralité :
def handle_post(env, start_line):
form = get_field_storage(env) ## body of an HTTP POST request
## Extract fields from POST form.
region = form.getvalue('region')
device = form.getvalue('device')
amount = form.getvalue('amount')
tstamp = form.getvalue('tstamp')
## Missing info?
if (region is not None and
device is not None and
amount is not None and
tstamp is not None):
add_record(region, device, amount, tstamp)
response_body = "POST request handled.\n"
start_line('201 OK', [('Content-Type', 'text/plain')])
else:
response_body = "Missing info in POST request.\n"
start_line('400 Bad Request', [('Content-Type', 'text/plain')])
return [response_body.encode()]
Les deux arguments de la fonction handle_post
(env
et start_line
) représentent respectivement l'environnement système et un canal de communication. Le canal start_line
envoie la ligne de démarrage HTTP (dans ce cas, soit 400 Bad Request
ou 201 OK
) et tous les en-têtes HTTP (dans ce cas , juste Content-Type: text/plain
) d'une réponse HTTP.
La fonction handle_post
tente d'extraire les données pertinentes de la requête HTTP POST et, si elle réussit, appelle la fonction add_record
pour ajouter une autre ligne à la table des chutes de neige :
def add_record(reg, dev, amt, tstamp):
conn = sqlite3.connect(PATH_2_DB) ## connect to DB
cursor = conn.cursor() ## get a cursor
sql = "INSERT INTO snowfall(region,device,amount,tstamp) VALUES (?,?,?,?)"
cursor.execute(sql, (reg, dev, amt, tstamp)) ## execute INSERT
conn.commit() ## commit
conn.close() ## cleanup
SQLite encapsule automatiquement des instructions SQL uniques (telles que INSERT
ci-dessus) dans une transaction, ce qui explique l'appel à conn.commit()
dans le code. SQLite prend également en charge les transactions multi-instructions. Après avoir appelé add_record
, la fonction handle_post
termine son travail en envoyant un message de confirmation de réponse HTTP au demandeur.
La fonction handle_get
touche également la base de données, mais uniquement pour lire les enregistrements de la table des chutes de neige :
def handle_get(start_line):
conn = sqlite3.connect(PATH_2_DB) ## connect to DB
cursor = conn.cursor() ## get a cursor
cursor.execute("SELECT * FROM snowfall")
response_body = "<h3>Snowfall report</h3><ul>"
rows = cursor.fetchall()
for row in rows:
response_body += "<li>" + str(row[0]) + '|' ## primary key
response_body += row[1] + '|' ## region
response_body += row[2] + '|' ## device
response_body += str(row[3]) + '|' ## amount
response_body += str(row[4]) + "</li>" ## timestamp
response_body += "</ul>"
conn.commit() ## commit
conn.close() ## cleanup
start_line('200 OK', [('Content-Type', 'text/html')])
return [response_body.encode()]
Une version conviviale de l'application Snowfall prendrait en charge des rapports supplémentaires (et plus sophistiqués), mais même cette version de handle_get
souligne l'interface claire entre Python et SQLite. À propos, uwsgi s'attend à ce qu'un corps de réponse soit une liste d'octets. Dans l'instruction return
, l'appel à response_body.encode()
entre crochets génère la liste d'octets à partir de la chaîne response_body
.
Passer à Nginx
Le serveur Web Nginx peut être installé sur un système basé sur Debian avec une seule commande :
% sudo apt-get install nginx
En tant que serveur Web, Nginx fournit les services attendus, tels que la sécurité au niveau filaire, HTTPS, l'authentification des utilisateurs, l'équilibrage de charge, le streaming multimédia, la compression des réponses, le téléchargement de fichiers, etc. Le moteur Nginx est performant et stable, et ce serveur peut prendre en charge du contenu dynamique via une variété de langages de programmation. Utiliser uwsgi comme serveur Web très léger est une option intéressante, mais passer à Nginx constitue une transition vers un hébergement Web de puissance industrielle avec une capacité de volume élevé. Nginx et uwsgi sont tous deux implémentés en C.
Avec Nginx en jeu, uwsgi assume les rôles restreints d'un protocole de communication et d'un serveur d'applications ; il n'agit plus comme un serveur Web HTTP. Voici l'architecture révisée :
HTTP uwsgi
requester<---->Nginx<----->app server<--->requestHandler.py
Comme indiqué précédemment, Nginx inclut la prise en charge d'uwsgi et agit désormais comme un serveur proxy inverse qui transmet les requêtes HTTP désignées au serveur d'applications uwsgi, qui à son tour interagit avec le script Python requestHandler.py
. Les réponses du script Python se déplacent dans le sens inverse afin que Nginx renvoie la réponse HTTP au client demandeur.
Deux changements donnent vie à cette nouvelle architecture. Le premier lance uwsgi en tant que serveur d'applications :
% uwsgi --socket 127.0.0.1:8001 --wsgi-file requestHandler.py
Le socket 8001 est la valeur par défaut de Nginx pour les communications uwsgi. Pour des raisons de robustesse, vous pouvez utiliser le chemin complet du script Python afin que la commande ci-dessus ne doive pas être exécutée dans le répertoire qui héberge le script Python. Dans un environnement de production, uwsgi démarrerait et s'arrêterait automatiquement ; pour l’instant, cependant, l’accent reste mis sur la façon dont les pièces architecturales s’emboîtent.
Le deuxième changement concerne la configuration de Nginx, qui peut être délicate sur les systèmes basés sur Debian. Le fichier de configuration principal de Nginx est /etc/nginx/nginx.conf
, mais ce fichier peut avoir des directives include
pour d'autres fichiers, en particulier les fichiers dans l'un des trois < Sous-répertoires/etc/nginx : nginx.d
, sites-available
et sites-enabled
. Les directives include
peuvent être éliminées pour simplifier les choses ; dans ce cas, la configuration s'effectue uniquement dans nginx.conf
. Je recommande l'approche simple.
Quelle que soit la configuration distribuée, la section clé permettant à Nginx de communiquer avec le serveur d'applications uwsgi commence par http
et comporte une ou plusieurs sous-sections server
, qui à leur tour ont emplacement
sous-sections. Voici un exemple tiré de la documentation Nginx :
...
http {
# Configuration specific to HTTP and affecting all virtual servers
...
server { # simple reverse-proxy
listen 80;
server_name domain2.com www.domain2.com;
access_log logs/domain2.access.log main;
# serve static files
location ~ ^/(images|javascript|js|css|flash|media|static)/ {
root /var/www/virtual/big.server.com/htdocs;
expires 30d;
}
# pass requests for dynamic content to rails/turbogears/zope, et al
location / {
proxy_pass http://127.0.0.1:8080;
}
}
...
}
Les sous-sections location
sont celles qui nous intéressent. Pour l'application snowfall, voici l'entrée location
ajoutée avec ses deux lignes de configuration :
...
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;
### key addition for uwsgi communication
location /snowfall {
include uwsgi_params; ## comes with Nginx
uwsgi_pass 127.0.0.1:8001; ## 8001 is the default for uwsgi
}
...
}
...
Pour garder les choses simples pour l'instant, faites de /snowfall
le seul emplacement
dans la configuration. Avec cette configuration en place, Nginx écoute sur le port 80 et envoie les requêtes HTTP se terminant par le chemin /snowfall
vers le serveur d'applications uwsgi :
% curl -X POST -d "..." localhost/snowfall ## new POST
% curl -X GET localhost/snowfall ## new GET
Le numéro de port 80 peut être supprimé de la requête car 80 est le port de serveur par défaut pour les requêtes HTTP.
Si l'emplacement configuré était simplement /
au lieu de /snowfall
, alors toute requête HTTP avec /
au début du chemin serait envoyée au serveur d'applications uwsgi. En conséquence, le chemin /snowfall
laisse la place à d'autres emplacements et, par conséquent, à d'autres actions en réponse aux requêtes HTTP.
Une fois que vous avez modifié la configuration de Nginx avec la sous-section location
ajoutée, vous pouvez démarrer le serveur Web :
% sudo systemctl start nginx
Il existe d'autres commandes similaires à stop
et restart
Nginx. Dans un environnement de production, vous pouvez automatiser ces actions afin que Nginx démarre au démarrage du système et s'arrête lors d'un arrêt du système.
Avec uwsgi et Nginx en cours d'exécution, vous pouvez utiliser un navigateur pour tester si les composants architecturaux coopèrent comme prévu. Par exemple, si vous saisissez l'URL localhost/
dans la fenêtre de saisie du navigateur, la page d'accueil de Nginx devrait apparaître avec un contenu (HTML) similaire à celui-ci :
Welcome to nginx!
...
Thank you for using nginx.
En revanche, l'URL localhost/snowfall
doit afficher les lignes actuellement dans le tableau des chutes de neige :
Snowfall report
1|R1|D9|1.42|1604722088.0158753
2|R7|D4|2.11|1604722296.8862638
3|R5|D1|1.12|1604942236.1013834
Emballer
L'application Snowfall montre comment les composants logiciels libres (un serveur Web puissant, un système de base de données compatible ACID et des scripts pour le contenu dynamique) peuvent prendre en charge une application Web réaliste sur une plate-forme Raspberry Pi 4. Cette machine légère dépasse sa catégorie de poids, et Debian facilite le levage.
Les composants logiciels de l'application Web fonctionnent bien ensemble et nécessitent très peu de configuration. Pour les appels à volume plus important sur une base de données relationnelle, rappelez-vous qu'une alternative gratuite et riche en fonctionnalités à SQLite est PostgreSQL. Si vous avez hâte de jouer sur le Raspberry Pi 4, en particulier pour explorer la programmation Web côté serveur sur cette plate-forme, alors Nginx, SQLite ou PostgreSQL, uwsgi et Python valent la peine d'être pris en considération.