>>> LINK A PAGINA AGGIORNATA <<<
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
- .
Importiamo le classi necessarie
Per realizzare un web server minimale sono necessarie le classi HTTPServer e BaseHTTPRequestHandler contenute nel modulo http.server (BaseHTTPServer in Python2).
from http.server import HTTPServer from http.server import BaseHTTPRequestHandler
Creiamo un handler vuoto e un server in ascolto
L’handler è la parte di programma che deve gestire la richiesta. È una classe che eredita da BaseHTTPRequestHandler e ne ridefinisce i metodi do_GET e do_POST.
Il server invece è un’istanza di HTTPServer, creata passando l’indirizzo locale (IP + porta in ascolto) e la classe handler. La stringa dell’IP può essere impostata uguale all’IP LAN della macchina su cui gira il server, oppure può essere lasciata vuota (l’IP viene preso automaticamente).
La porta di default su cui un WEB server attende richieste è la 80, ma per applicazioni particolari se ne possono usare altre, casi comuni sono 8000, 8080, 8800.
Il server normalmente può essere fermato solo con un’interruzione da tastiera (CTRL + C).
class Gestore(BaseHTTPRequestHandler): def do_GET(self): pass def do_POST(self): pass #___________________________________________________________ indirizzo = '', 80 server = HTTPServer(indirizzo, Gestore) try: server.serve_forever() except KeyboardInterrupt: server.socket.close()
Oggetti disponibili nell’handler
Ad ogni request ricevuta dal server, viene chiamato il metodo do_GET oppure do_POST dell’handler. Questi sono gli oggetti principali con cui lavorare:
OGGETTO | DESCRIZIONE |
---|---|
self.client_address | Tupla (“ip”, porta) |
self.path | Stringa, es: “/pagine/file1.html”. Solo “/” se non si specifica niente nella richiesta. Da qui si leggono i dati ricevuti tramite una richiesta di tipo GET. |
self.headers | Tutti gli header inviati nella richiesta |
self.server | Istanza del server |
self.rfile | Stream in arrivo, da qui si leggono i dati ricevuti tramite una richiesta di tipo POST. |
self.wfile | Stream in uscita, qui si scrive il corpo della risposta (il testo HTML, altri file richiesti ecc) |
Il server (istanza di HTTPServer) è costruito partendo dalla classe TCPServer del modulo socketserver (SocketServer in Python2). Quindi dispone di tutti gli attributi/metodi di TCPServer, ad esempio il metodo serve_forever per avviarlo.
Nota: se tra due applicazioni si vuole solo comunicare tramite TCP/IP scambiando messaggi con protocollo proprietario e non HTTP, allora va usato direttamente il modulo di livello più basso socketserver (istanziando un TCPServer). Il modulo http.server si trova infatti ad un livello di astrazione superiore, creato per gestire in modo semplificato le transazioni HTTP da/verso un browser.
Una risposta minimale
Prima l’esempio completo, poi la spiegazione…
from http.server import HTTPServer from http.server import BaseHTTPRequestHandler #___________________________________________________________ pagina = u'''<!DOCTYPE html> <html lang="it"> <head> <meta charset="UTF-8"> </head> <body> OK </body> </html>''' #___________________________________________________________ def trasmetti(hdl, codice, tipo, contenuto): hdl.send_response(codice) hdl.send_header('Content-Type', tipo) hdl.send_header('Content-Length', len(contenuto)) hdl.end_headers() hdl.wfile.write(contenuto) hdl.wfile.flush() hdl.connection.shutdown(1) #___________________________________________________________ class Gestore(BaseHTTPRequestHandler): def do_GET(self): contenuto = pagina.encode('utf-8') tipo = 'text/html' trasmetti(self, 200, tipo, contenuto) def do_POST(self): pass #___________________________________________________________ indirizzo = '', 80 server = HTTPServer(indirizzo, Gestore) try: server.serve_forever() except KeyboardInterrupt: server.socket.close()
Per dare al browser una risposta minimale (quella che comunemente viene chiamata “pagina WEB”) prepariamo una “pagina” minima ma completa in HTML5 sotto forma di stringa di testo.
È importante trattare sempre il testo in forma unicode in modo da poter rappresentare qualsiasi carattere. Questo è il default in Python3, mentre Python2 richiede di specificare esplicitamente il tipo di stringa anteponendo il carattere ‘u’ alle stringhe unicode.
Il prefisso ‘u’ è comunque accettato anche da Python3, quindi una stringa iniziante con ‘u’ va sempre bene ed evita ambiguità.
Il protocollo HTTP prevede che la risposta inizi con un codice di risposta, seguito da uno o più header che specificano informazioni sul contenuto, da una riga vuota che separa header da contenuto, ed infine dal contenuto vero e proprio (la pagina o qualsiasi altro dato richiesto).
L’invio del codice di risposta, degli header e della riga vuota, si ottengono con i metodi send_response, send_header, end_headers. Il contenuto vero e proprio invece si scrive sullo stream di uscita.
Per comodità si può creare una funzione ‘trasmetti’ con tutto il necessario per trasmettere la risposta. Il primo parametro è l’istanza dell’handler, il secondo il codice di risposta, il terzo il mimetype, il quarto i dati veri e propri.
Ogni contenuto va trasmesso sotto forma di byte, quindi il testo della pagina va codificato (encode). Va usato lo stesso encoding dichiarato nella sezione header della pagina HTML. In questo modo il browser che riceve i byte dei caratteri, li interpreta e visualizza sempre correttamente.
…e con questo abbiamo creato un WEB server che ad ogni richiesta di tipo GET risponde sempre con il testo “OK”.
Forse poco utile, ma necessario per vedere le basi e verificare il corretto funzionamento. Su qualche sistema può essere necessario avviare il server con i permessi di amministratore, ad esempio scrivendo a terminale:
sudo python3 webserver.py
Servire i file statici
Più interessante è un WEB server in grado di fornire i contenuti presenti in una specifica directory (e sottodirectory), pagine HTML, fogli di stile CSS, script Javascript, immagini ecc:
import os from urllib.parse import unquote_plus from http.server import HTTPServer from http.server import BaseHTTPRequestHandler BASEDIR = r'/home/a/Scrivania' #___________________________________________________________ def trasmetti(hdl, codice, tipo, contenuto): hdl.send_response(codice) hdl.send_header('Content-Type', tipo) hdl.send_header('Content-Length', len(contenuto)) hdl.end_headers() hdl.wfile.write(contenuto) hdl.wfile.flush() hdl.connection.shutdown(1) #___________________________________________________________ TIPI = { '.css': 'text/css', '.gif': 'image/gif', '.jpg': 'image/jpg', '.png': 'image/png', '.ico': 'image/x-icon', '.html': 'text/html', '.htm': 'text/html', '.txt': 'text/plain', '.js': 'application/javascript' } def determina_tipo(risorsa): for estensione in TIPI: if risorsa.lower().endswith(estensione): return TIPI[estensione] return 'text/plain' #___________________________________________________________ def invia_risorsa(hdl, risorsa): tipo = determina_tipo(risorsa) try: with open(BASEDIR + risorsa, 'rb') as fi: trasmetti(hdl, 200, tipo, fi.read()) except IOError: hdl.send_error(500, 'Internal server error') #___________________________________________________________ class Gestore(BaseHTTPRequestHandler): def do_GET(self): richiesta = unquote_plus(self.path) risorsa = richiesta.split('?', 1)[0] if os.path.isfile(BASEDIR + risorsa): invia_risorsa(self, risorsa) else: self.send_error( 404, '{} Not found'.format(risorsa)) def do_POST(self): pass #___________________________________________________________ indirizzo = '', 80 server = HTTPServer(indirizzo, Gestore) try: server.serve_forever() except KeyboardInterrupt: server.socket.close()
La stringa costante BASEDIR rappresenta la directory in cui il server deve cercare i file.
Nota: Sui sistemi Windows si usano i backslash ‘\’ per comporre i percorsi, e questi potrebbero essere interpretati da Python come sequenze di escape. Per scongiurare del tutto questa possibilità basta definire come “raw” tutte le stringhe che contengono percorsi, anteponendo il simbolo ‘r’.
La stringa self.path potrebbe presentarsi “quotata” con alcuni caratteri sostituiti da altri caratteri, inizianti con il simbolo ‘%’, ad esempio:
/archivio/1995/pag2.html?p=126&r=%22ab%20cd%22
Va quindi preventivamente “unquotata” con la funzione ‘unquote_plus’ del modulo urllib.parse (urllib in Python2).
Poi dobbiamo separare il percorso della risorsa da tutto quello che eventualmente si trova dopo il simbolo ‘?’ (che serve per la creazione dinamica dei contenuti, vedi articolo: Un WEB server più dinamico).
Se la risorsa corrisponde ad un nome di file presente, viene chiamata la funzione ‘invia_risorsa’, altrimenti viene trasmessa una pagina “non trovato” con codice errore 404.
Con la funzione ‘determina_tipo’, si determina il mimetype della risorsa grazie all’estensione del nome del file. Un tipo sconosciuto viene considerato text/plain.
(7/2018)