WSGI: l'interfaccia dell'applicazione server per Python

Pubblicato: 2022-03-11

Nel 1993 il web era ancora agli albori, con circa 14 milioni di utenti e 100 siti web. Le pagine erano statiche ma c'era già la necessità di produrre contenuti dinamici, come notizie e dati aggiornati. In risposta a ciò, Rob McCool e altri collaboratori hanno implementato la Common Gateway Interface (CGI) nel server Web HTTPd del National Center for Supercomputing Applications (NCSA) (il precursore di Apache). Questo è stato il primo server Web in grado di fornire contenuti generati da un'applicazione separata.

Da allora, il numero di utenti su Internet è esploso e i siti Web dinamici sono diventati onnipresenti. Quando si impara per la prima volta una nuova lingua o anche per la prima volta a programmare, gli sviluppatori, abbastanza presto, vogliono sapere come agganciare il loro codice al web.

Python sul Web e l'ascesa di WSGI

Dalla creazione della CGI, molto è cambiato. L'approccio CGI divenne impraticabile, poiché richiedeva la creazione di un nuovo processo ad ogni richiesta, sprecando memoria e CPU. Sono emersi altri approcci di basso livello, come FastCGI](http://www.fastcgi.com/) (1996) e mod_python (2000), che forniscono interfacce diverse tra i framework Web Python e il server Web. Con il proliferare di approcci diversi, la scelta del framework da parte dello sviluppatore ha finito per limitare le scelte dei server Web e viceversa.

Per affrontare questo problema, nel 2003 Phillip J. Eby ha proposto PEP-0333, Python Web Server Gateway Interface (WSGI). L'idea era di fornire un'interfaccia universale di alto livello tra le applicazioni Python e i server web.

Nel 2003, PEP-3333 ha aggiornato l'interfaccia WSGI per aggiungere il supporto per Python 3. Al giorno d'oggi, quasi tutti i framework Python utilizzano WSGI come mezzo, se non l'unico, per comunicare con i propri server web. Questo è il modo in cui Django, Flask e molti altri framework popolari lo fanno.

Questo articolo intende fornire al lettore uno sguardo su come funziona WSGI e consentire al lettore di creare una semplice applicazione o server WSGI. Tuttavia, non intende essere esaustivo e gli sviluppatori che intendono implementare server o applicazioni pronti per la produzione dovrebbero dare un'occhiata più approfondita alle specifiche WSGI.

L'interfaccia Python WSGI

WSGI specifica regole semplici a cui il server e l'applicazione devono conformarsi. Iniziamo esaminando questo schema generale.

L'interfaccia dell'applicazione server Python WSGI.

Interfaccia dell'applicazione

In Python 3.5, le interfacce dell'applicazione funzionano così:

 def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]

In Python 2.7, questa interfaccia non sarebbe molto diversa; l'unico cambiamento sarebbe che il corpo è rappresentato da un oggetto str , invece che da un bytes .

Sebbene in questo caso abbiamo usato una funzione, qualsiasi chiamabile funzionerà. Le regole per l'oggetto applicazione qui sono:

  • Deve essere un callable con parametri environ e start_response .
  • Deve chiamare il callback start_response prima di inviare il corpo.
  • Deve restituire un iterabile con parti del corpo del documento.

Un altro esempio di oggetto che soddisfa queste regole e produrrebbe lo stesso effetto è:

 class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body

Interfaccia server

Un server WSGI potrebbe interfacciarsi con questa applicazione in questo modo:

 def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()

Come avrai notato, il callable start_response restituito un callable di write che l'applicazione può utilizzare per inviare i dati al client, ma che non è stato utilizzato dal nostro esempio di codice dell'applicazione. Questa interfaccia di write è obsoleta e per ora possiamo ignorarla. Se ne parlerà brevemente più avanti nell'articolo.

Un'altra particolarità delle responsabilità del server è chiamare il metodo facoltativo close sull'iteratore di risposta, se esiste. Come sottolineato nell'articolo di Graham Dumpleton qui, è una caratteristica spesso trascurata di WSGI. La chiamata a questo metodo, se esiste , consente all'applicazione di rilasciare tutte le risorse che potrebbe ancora contenere.

L'argomento environ di Application Callable

Il parametro environ dovrebbe essere un oggetto dizionario. Viene utilizzato per passare le informazioni sulla richiesta e sul server all'applicazione, più o meno allo stesso modo in cui fa CGI. In effetti, tutte le variabili di ambiente CGI sono valide in WSGI e il server dovrebbe passare tutto ciò che si applica all'applicazione.

Sebbene ci siano molte chiavi facoltative che possono essere passate, molte sono obbligatorie. Prendendo come esempio la seguente richiesta GET :

 $ curl 'http://localhost:8000/auth?user=obiwan&token=123'

Queste sono le chiavi che il server deve fornire e i valori che assumerebbero:

Chiave Valore Commenti
REQUEST_METHOD "GET"
SCRIPT_NAME "" dipendente dalla configurazione del server
PATH_INFO "/auth"
QUERY_STRING "token=123"
CONTENT_TYPE ""
CONTENT_LENGTH ""
SERVER_NAME "127.0.0.1" dipendente dalla configurazione del server
SERVER_PORT "8000"
SERVER_PROTOCOL "HTTP/1.1"
HTTP_(...) Intestazioni HTTP fornite dal client
wsgi.version (1, 0) tupla con versione WSGI
wsgi.url_scheme "http"
wsgi.input Oggetto simile a un file
wsgi.errors Oggetto simile a un file
wsgi.multithread False True se il server è multithread
wsgi.multiprocess False True se il server esegue più processi
wsgi.run_once False True se il server prevede che questo script venga eseguito una sola volta (ad esempio: in un ambiente CGI)

L'eccezione a questa regola è che se una di queste chiavi dovesse essere vuota (come CONTENT_TYPE nella tabella sopra), allora possono essere omesse dal dizionario e si presumerà che corrispondano alla stringa vuota.

wsgi.input e wsgi.errors

La maggior parte delle chiavi environ sono semplici, ma due di esse meritano un po' più di chiarimento: wsgi.input , che deve contenere un flusso con il corpo della richiesta dal client, e wsgi.errors , in cui l'applicazione segnala eventuali errori riscontrati. Gli errori inviati dall'applicazione a wsgi.errors in genere vengono inviati al registro degli errori del server.

Queste due chiavi devono contenere oggetti simili a file; ovvero oggetti che forniscono interfacce da leggere o scrivere come flussi, proprio come l'oggetto che otteniamo quando apriamo un file o un socket in Python. All'inizio può sembrare complicato, ma fortunatamente Python ci offre buoni strumenti per gestirlo.

Innanzitutto, di che tipo di stream stiamo parlando? Secondo la definizione WSGI, wsgi.input e wsgi.errors devono gestire oggetti bytes in Python 3 e oggetti str in Python 2. In entrambi i casi, se desideriamo utilizzare un buffer in memoria per passare o ottenere dati tramite WSGI interfaccia, possiamo usare la classe io.BytesIO .

Ad esempio, se stiamo scrivendo un server WSGI, potremmo fornire il corpo della richiesta all'applicazione in questo modo:

  • Per Python 2.7
 import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
  • Per Python 3.5
 import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)

Sul lato dell'applicazione, se volessimo trasformare un input di flusso che abbiamo ricevuto in una stringa, vorremmo scrivere qualcosa del genere:

  • Per Python 2.7
 readstr = environ['wsgi.input'].read() # returns str object
  • Per Python 3.5
 readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object

Il flusso wsgi.errors deve essere utilizzato per segnalare gli errori dell'applicazione al server e le righe devono essere terminate da un \n . Il server web dovrebbe occuparsi della conversione in una terminazione di riga diversa a seconda del sistema.

L'argomento start_response di Application Callable

L'argomento start_response deve essere un richiamabile con due argomenti obbligatori, ovvero status e headers e un argomento facoltativo, exc_info . Deve essere richiamato dall'applicazione prima che qualsiasi parte del corpo venga rispedita al server web.

Nel primo esempio di applicazione all'inizio di questo articolo, abbiamo restituito il corpo della risposta come un elenco e, pertanto, non abbiamo alcun controllo su quando l'elenco verrà ripetuto. Per questo motivo, abbiamo dovuto chiamare start_response prima di restituire l'elenco.

Nel secondo, abbiamo chiamato start_response appena prima di produrre il primo (e, in questo caso, l'unico) pezzo del corpo della risposta. In entrambi i casi è valido all'interno delle specifiche WSGI.

Dal lato del server Web, la chiamata di start_response non dovrebbe effettivamente inviare le intestazioni al client, ma ritardarla fino a quando non è presente almeno una stringa di byte non vuota nel corpo della risposta da inviare al client. Questa architettura consente di riportare correttamente gli errori fino all'ultimo momento possibile dell'esecuzione dell'applicazione.

Lo status Argomento di start_response

L'argomento status passato al callback start_response deve essere una stringa costituita da un codice di stato HTTP e una descrizione, separati da un singolo spazio. Esempi validi sono: '200 OK' o '404 Not Found' .

Le headers Argomento di start_response

L'argomento headers passato al callback start_response deve essere un list Python di tuple s, con ogni tupla composta come (header_name, header_value) . Sia il nome che il valore di ciascuna intestazione devono essere stringhe (indipendentemente dalla versione di Python). Questo è un raro esempio in cui il tipo è importante, poiché è effettivamente richiesto dalla specifica WSGI.

Ecco un valido esempio di come potrebbe apparire un argomento di header :

 response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]

Le intestazioni HTTP non fanno distinzione tra maiuscole e minuscole e se stiamo scrivendo un server Web conforme a WSGI, è qualcosa di cui tenere conto quando si controllano queste intestazioni. Inoltre, l'elenco delle intestazioni fornito dall'applicazione non dovrebbe essere esaustivo. È responsabilità del server assicurarsi che tutte le intestazioni HTTP richieste esistano prima di inviare la risposta al client, compilando le intestazioni non fornite dall'applicazione.

L'argomento start_response di exc_info

Il callback start_response dovrebbe supportare un terzo argomento exc_info , utilizzato per la gestione degli errori. L'utilizzo e l'implementazione corretti di questo argomento sono della massima importanza per i server Web di produzione e le applicazioni, ma non rientrano nell'ambito di questo articolo.

Ulteriori informazioni su di esso possono essere ottenute nelle specifiche WSGI, qui.

Il valore di ritorno start_response – La callback di write

Per motivi di compatibilità con le versioni precedenti, i server Web che implementano WSGI dovrebbero restituire un callable di write . Questo callback dovrebbe consentire all'applicazione di scrivere i dati della risposta del corpo direttamente sul client, invece di cederli al server tramite un iteratore.

Nonostante la sua presenza, questa è un'interfaccia obsoleta e le nuove applicazioni dovrebbero astenersi dall'usarla.

Generazione del corpo di risposta

Le applicazioni che implementano WSGI dovrebbero generare il corpo della risposta restituendo un oggetto iterabile. Per la maggior parte delle applicazioni, il corpo della risposta non è molto grande e si adatta facilmente alla memoria del server. In tal caso, il modo più efficiente per inviarlo è tutto in una volta, con un elemento iterabile. In casi speciali, in cui non è possibile caricare l'intero corpo in memoria, l'applicazione può restituirlo parte per parte tramite questa interfaccia iterabile.

C'è solo una piccola differenza qui tra WSGI di Python 2 e Python 3: in Python 3, il corpo della risposta è rappresentato da oggetti bytes ; in Python 2, il tipo corretto per questo è str .

La conversione di stringhe UTF-8 in bytes o str è un compito facile:

  • Python 3.5:
 body = 'unicode stuff'.encode('utf-8')
  • Python 2.7:
 body = u'unicode stuff'.encode('utf-8')

Se desideri saperne di più sulla gestione di unicode e bytestring di Python 2, c'è un bel tutorial su YouTube.

I server Web che implementano WSGI dovrebbero anche supportare il callback di write per la compatibilità con le versioni precedenti, come descritto sopra.

Testare la tua applicazione senza un server web

Con la comprensione di questa semplice interfaccia, possiamo facilmente creare script per testare le nostre applicazioni senza dover effettivamente avviare un server.

Prendi questo piccolo script, ad esempio:

 from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}

In questo modo, potremmo, ad esempio, inizializzare alcuni dati di test e simulare moduli nella nostra app ed effettuare chiamate GET per verificare se risponde di conseguenza. Possiamo vedere che non è un vero server web, ma si interfaccia con la nostra app in modo comparabile fornendo all'applicazione un callback start_response e un dizionario con le nostre variabili di ambiente. Alla fine della richiesta, consuma l'iteratore del corpo della risposta e restituisce una stringa con tutto il suo contenuto. Metodi simili (o generici) possono essere creati per diversi tipi di richieste HTTP.

Incartare

WSGI è una parte fondamentale di quasi tutti i framework Web Python.

In questo articolo, non abbiamo affrontato il modo in cui WSGI gestisce i caricamenti di file, poiché questa potrebbe essere considerata una funzionalità più "avanzata", non adatta per un articolo introduttivo. Se desideri saperne di più, dai un'occhiata alla sezione PEP-3333 relativa alla gestione dei file.

Spero che questo articolo sia utile per aiutare a creare una migliore comprensione di come Python comunica ai server Web e consente agli sviluppatori di utilizzare questa interfaccia in modi interessanti e creativi.

Ringraziamenti

Vorrei ringraziare il mio editore Nick McCrea per avermi aiutato con questo articolo. Grazie al suo lavoro, il testo originale è diventato molto più chiaro e diversi errori non sono stati corretti.