http.server - Classe Base per Implementare Server Web
Scopo: Include classi che possono formare le basi un server web.
Il modulo http.server usa classi dal modulo socketserver per creare classi base per costruire server HTTP. HTTPServer
può essere usato direttamente, ma la classe BaseHTTPRequestHandler
è concepita per essere estesa per gestire ciascun metodo del protocollo (GET, POST, ecc.).
HTTP GET
Per aggiungere supporto ad un metodo HTTP in una classe per la gestione di richieste, si implementi il metodo do_METODO)
sostituendo METODO con il nome del metodo HTTP (es. do_GET()
, do_POST()
, ecc.). Per consistenza, i metodi per la gestione della richiesta non richiedono argomenti. Tutti i parametri per la richiesta sono elaborati da BaseHTTPRequestHandler
e conservati come attributi di istanza nell'istanza della richiesta.
Questo esempio di gestore di richiesta illustra come ritornare una risposta al client, ed alcuni degli attributi locali che possono essere utili nella costruzione della risposta.
# http_server_GET.py
from http.server import BaseHTTPRequestHandler
from urllib import parse
class GetHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed_path = parse.urlparse(self.path)
message_parts = [
'VALORI DEL CLIENT:',
'indirizzi client={} ({})'.format(
self.client_address,
self.address_string()),
'comando={}'.format(self.command),
'percorso={}'.format(self.path),
'percorso reale={}'.format(parsed_path.path),
'query={}'.format(parsed_path.query),
'versione richiesta={}'.format(self.request_version),
'',
'VALORI DEL SERVER:',
'versione server={}'.format(self.server_version),
'versione sys={}'.format(self.sys_version),
'versione protocollo={}'.format(self.protocol_version),
'',
'INTESTAZIONI RICEVUTE:',
]
for name, value in sorted(self.headers.items()):
message_parts.append(
'{}={}'.format(name, value.rstrip())
)
message_parts.append('')
message = '\r\n'.join(message_parts)
self.send_response(200)
self.send_header('Content-Type',
'text/plain; charset=utf-8')
self.end_headers()
self.wfile.write(message.encode('utf-8'))
if __name__ == '__main__':
from http.server import HTTPServer
server = HTTPServer(('localhost', 8080), GetHandler)
print('Avvio del server, usare <Ctrl-C> per interrompere')
server.serve_forever()
Il testo del messaggio viene assemblato, quindi scritto a wfile
, l'handle che incapsula il socket di risposta. Ogni risposta deve avere un codice di risposta, impostato tramite send_response()
. Se viene usato un codice di errore (404, 501, ecc.), viene incluso nell'intestazione un appropriato messaggio di errore predefinito, oppure un messaggio può essere passato con il codice di errore.
Per eseguire il gestore di richiesta nel server, lo si passi al costruttore di HTTPServer
, come indicato nella parte __main__
del codice di esempio qui sopra.
Per far partire il server:
$ python3 http_server_GET.py Avvio del server, usare <Ctrl-C> per interrompere
In un altro terminale usare il programma curl per accedere:
$ curl -v -i http://127.0.0.1:8080/?foo=bar * Trying 127.0.0.1... * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > GET /?foo=bar HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.50.1 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK HTTP/1.0 200 OK < Server: BaseHTTP/0.6 Python/3.5.2+ Server: BaseHTTP/0.6 Python/3.5.2+ < Date: Sun, 04 Jun 2017 08:44:41 GMT Date: Sun, 04 Jun 2017 08:44:41 GMT < Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8 < VALORI DEL CLIENT: indirizzi client=('127.0.0.1', 55180) (127.0.0.1) comando=GET percorso=/?foo=bar percorso reale=/ query=foo=bar versione richiesta=HTTP/1.1 VALORI DEL SERVER: versione server=BaseHTTP/0.6 versione sys=Python/3.5.2+ versione protocollo=HTTP/1.0 INTESTAZIONI RICEVUTE: Accept=*/* Host=127.0.0.1:8080 User-Agent=curl/7.50.1 * Closing connection 0
curl
. Se l'esecuzione degli esempi produce output diversi, verificare il numero di versione riportato da curl
.HTTP POST
Per supportare richieste POST è necessario un poco più di lavoro, visto che la classe base non elabora automaticamente i dati del form. Il modulo cgi fornisce la classe FieldStorage
che è in grado di elaborare il form, se fornita dei corretti input.
# http_server_POST.py
import cgi
from http.server import BaseHTTPRequestHandler
import io
class PostHandler(BaseHTTPRequestHandler):
def do_POST(self):
# Parse the form data posted
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={
'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': self.headers['Content-Type'],
}
)
# Inizio della risposta
self.send_response(200)
self.send_header('Content-Type',
'text/plain; charset=utf-8')
self.end_headers()
out = io.TextIOWrapper(
self.wfile,
encoding='utf-8',
line_buffering=False,
write_through=True,
)
out.write('Client: {}\n'.format(self.client_address))
out.write('User-agent: {}\n'.format(
self.headers['user-agent']))
out.write('Percorso: {}\n'.format(self.path))
out.write('Dati del form:\n')
# Ritorna le informazioni di ciò che era stato inviato nel form
for field in form.keys():
field_item = form[field]
if field_item.filename:
# Il campo contiene un file inviato
file_data = field_item.file.read()
file_len = len(file_data)
del file_data
out.write(
'\tUploaded {} as {!r} ({} bytes)\n'.format(
field, field_item.filename, file_len)
)
else:
# Valori del normali
out.write('\t{}={}\n'.format(
field, form[field].value))
# Disconnette il wrapper di codifica dal buffer sottostante
# in modo che l'eliminazione del wrapper non chiuda
# il socket, che è ancora in uso dal server.
out.detach()
if __name__ == '__main__':
from http.server import HTTPServer
server = HTTPServer(('localhost', 8080), PostHandler)
print('Avvio del server, usare <Ctrl-C> per interrompere')
server.serve_forever()
Eseguire il server in un terminale:
$ python3 http_server_POST.py Server in esecuzione, usare <Ctrl-C> per terminare
Gli argomenti per curl
possono includere dati di un form da inviare al server tramite l'opzione -F
. L'ultimo argomento, -F datafile=@http_server_GET.py
, invia il contenuto del file http_server_GET.py
per illustrare la lettura di un file di dati dal form.
$ curl -v http://127.0.0.1:8080/ -F name=dhellmann -F foo=bar -F datafile=@http_server_GET.py * Trying 127.0.0.1... * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > POST / HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.50.1 > Accept: */* > Content-Length: 2025 > Expect: 100-continue > Content-Type: multipart/form-data; boundary=------------------------bd06a03b251d489f > * Done waiting for 100-continue * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Server: BaseHTTP/0.6 Python/3.5.2+ < Date: Sun, 04 Jun 2017 08:55:23 GMT < Content-Type: text/plain; charset=utf-8 < Client: ('127.0.0.1', 34358) User-agent: curl/7.50.1 Percorso: / Dati del form: foo=bar Uploaded datafile as 'http_server_GET.py' (1614 bytes) name=dhellmann * Closing connection 0
Threading e Forking
Per aggiungere caratteristiche di threading e forking, occorre creare una nuova classe usando il mix-in appropriato da socketserver, visto che HTTPServer
è una semplice sottoclasse di socketserver.TCPServer
e non usa thread multipli o processi per gestire le richieste.
# http_server_threads.py
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type',
'text/plain; charset=utf-8')
self.end_headers()
message = threading.currentThread().getName()
self.wfile.write(message.encode('utf-8'))
self.wfile.write(b'\n')
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""Gestisce le richieste in un thread separato."""
if __name__ == '__main__':
server = ThreadedHTTPServer(('localhost', 8080), Handler)
print('Avvio del server, usare <Ctrl-C> per interrompere')
server.serve_forever()
Eseguire il server allo stesso modo degli altri esempi.
$ python3 http_server_threads.py Server in esecuzione, usare <Ctrl-C> per terminare
Ogni volta che il server riceve una richiesta, inizia un nuovo thread o processo per gestirla.
$ curl http://127.0.0.1:8080/ Thread-1 $ curl http://127.0.0.1:8080/ Thread-2 $ curl http://127.0.0.1:8080/ Thread-3
Scambiando ForkingMixIn
con ThreadingMixIn
si sarebbero ottenuti gli stessi risultati, usando processi separati in luogo dei thread.
Gestione degli Errori
Si gestiscano gli errori chiamando send_error()
, passando il codice di errore appropriato ed il messaggio di errore opzionale. L'intera risposta (con intestazioni, codice di stato e corpo) viene generata automaticamente.
# http_server_errors.py
from http.server import BaseHTTPRequestHandler
class ErrorHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_error(404)
if __name__ == '__main__':
from http.server import HTTPServer
server = HTTPServer(('localhost', 8080), ErrorHandler)
print('Avvio del server, usare <Ctrl-C> per interrompere')
server.serve_forever()
In questo caso, viene sempre ritornato un errore 404.
$ python3 http_server_errors.py Server in esecuzione, usare <Ctrl-C> per terminare
Il messaggio di errore viene riportato al client usando un documento HTML assieme all'intestazione per indicare un codice di errore.
$ curl -i http://127.0.0.1:8080/ HTTP/1.0 404 Not Found Server: BaseHTTP/0.6 Python/3.5.2+ Date: Sun, 04 Jun 2017 09:15:25 GMT Connection: close Content-Type: text/html;charset=utf-8 Content-Length: 447 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> <title>Error response</title> </head> <body> <h1>Error response</h1> <p>Error code: 404</p> <p>Message: Not Found.</p> <p>Error code explanation: 404 - Nothing matches the given URI.</p> </body> </html>
Impostare Intestazioni
Il metodo send_header()
aggiunge dati di intestazione alla risposta HTTP. Richiede due argomenti: il nome dell'intestazione ed il valore.
# http_server_send_header.py
from http.server import BaseHTTPRequestHandler
import time
class GetHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header(
'Content-Type',
'text/plain; charset=utf-8',
)
self.send_header(
'Last-Modified',
self.date_time_string(time.time())
)
self.end_headers()
self.wfile.write('Response body\n'.encode('utf-8'))
if __name__ == '__main__':
from http.server import HTTPServer
server = HTTPServer(('localhost', 8080), GetHandler)
print('Avvio del server, usare <Ctrl-C> per interrompere')
server.serve_forever()
Questo esempio imposta l'intestazione Last-Modified
all'orario corrente, formattato secondo le specifiche RFC 7231.
$ curl -i http://127.0.0.1:8080/ HTTP/1.0 200 OK Server: BaseHTTP/0.6 Python/3.5.2+ Date: Sun, 04 Jun 2017 09:20:47 GMT Content-Type: text/plain; charset=utf-8 Last-Modified: Sun, 04 Jun 2017 09:20:47 GMT Response body
Il server registra la richiesta nel terminale, come negli altri esempi.
$ python3 http_server_send_header.py Server in esecuzione, usare <Ctrl-C> per terminare 127.0.0.1 - - [04/Jun/2017 11:20:47] "GET / HTTP/1.1" 200 -
Utilizzo da Riga di Comando
http.server
include un server built-in per servire file dal file system locale. Per farlo partire dalla riga di comando usare l'opzione -m
per l'interprete Python.
$ python3 -m http.server 8080 Serving HTTP on 0.0.0.0 port 8080 ... 127.0.0.1 - - [04/Jun/2017 11:26:32] "HEAD /http_server_GET.py HTTP/1.1" 200 -
La directory radice del server è la directory di lavoro dalla quale il server è stato fatto partire.
$ curl -I http://127.0.0.1:8080/http_server_GET.py HTTP/1.0 200 OK Server: SimpleHTTP/0.6 Python/3.5.2+ Date: Sun, 04 Jun 2017 09:26:32 GMT Content-type: text/plain Content-Length: 1614 Last-Modified: Sun, 04 Jun 2017 08:32:47 GMT
Vedere anche:
- http.server
- La documentazione della libreria standard per questo modulo
- socketserver
- Creare server di rete
- RFC 7231
- "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content" include specifiche per il formato delle intestazioni HTTP e delle date.