WSGI: Interfața server-aplicație pentru Python
Publicat: 2022-03-11În 1993, web-ul era încă la început, cu aproximativ 14 milioane de utilizatori și 100 de site-uri web. Paginile erau statice, dar era deja nevoie să se producă conținut dinamic, cum ar fi știri și date actualizate. Răspunzând la aceasta, Rob McCool și alți contribuitori au implementat Common Gateway Interface (CGI) în serverul web HTTPd Centrul Național pentru Aplicații de Supercomputare (NCSA) (precursorul Apache). Acesta a fost primul server web care a putut servi conținut generat de o aplicație separată.
De atunci, numărul utilizatorilor de pe Internet a explodat, iar site-urile web dinamice au devenit omniprezente. Când învață pentru prima dată o nouă limbă sau chiar învață pentru prima dată să codifice, dezvoltatorii, destul de curând, vor să știe cum să-și conecteze codul în web.
Python pe web și ascensiunea WSGI
De la crearea CGI, multe s-au schimbat. Abordarea CGI a devenit nepractică, deoarece a necesitat crearea unui nou proces la fiecare solicitare, irosind memorie și CPU. Au apărut și alte abordări de nivel scăzut, cum ar fi FastCGI](http://www.fastcgi.com/) (1996) și mod_python (2000), oferind interfețe diferite între cadrele web Python și serverul web. Pe măsură ce abordările diferite au proliferat, alegerea dezvoltatorului a cadrului a ajuns să restrângă opțiunile serverelor web și invers.
Pentru a rezolva această problemă, în 2003 Phillip J. Eby a propus PEP-0333, Python Web Server Gateway Interface (WSGI). Ideea a fost de a oferi o interfață universală la nivel înalt între aplicațiile Python și serverele web.
În 2003, PEP-3333 a actualizat interfața WSGI pentru a adăuga suport pentru Python 3. În zilele noastre, aproape toate cadrele Python folosesc WSGI ca mijloc, dacă nu singurul mijloc, de a comunica cu serverele lor web. Așa procedează Django, Flask și multe alte framework-uri populare.
Acest articol intenționează să ofere cititorului o privire asupra modului în care funcționează WSGI și să permită cititorului să construiască o aplicație sau un server WSGI simplu. Totuși, nu este menit să fie exhaustiv, iar dezvoltatorii care intenționează să implementeze servere sau aplicații pregătite pentru producție ar trebui să analizeze mai amănunțit specificația WSGI.
Interfața Python WSGI
WSGI specifică reguli simple pe care serverul și aplicația trebuie să le respecte. Să începem prin a revizui acest model general.
Interfața aplicației
În Python 3.5, interfețele aplicației merg astfel:
def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body] În Python 2.7, această interfață nu ar fi foarte diferită; singura modificare ar fi că corpul este reprezentat de un obiect str , în loc de unul de bytes .
Deși am folosit o funcție în acest caz, orice apelabil va funcționa. Regulile pentru obiectul aplicației de aici sunt:
- Trebuie să fie apelabil cu parametrii
environșistart_response. - Trebuie să apelați returnarea
start_responseînainte de a trimite corpul. - Trebuie să returneze un iterabil cu bucăți din corpul documentului.
Un alt exemplu de obiect care îndeplinește aceste reguli și ar produce același efect este:
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 bodyInterfața serverului
Un server WSGI poate interfața cu această aplicație astfel::
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() După cum probabil ați observat, apelabilul start_response a returnat un apelabil de write pe care aplicația îl poate folosi pentru a trimite date înapoi către client, dar care nu a fost folosit de exemplul nostru de cod al aplicației. Această interfață de write este depreciată și o putem ignora pentru moment. Acesta va fi discutat pe scurt mai târziu în articol.
O altă particularitate a responsabilităților serverului este apelarea metodei opționale de close pe iteratorul de răspuns, dacă aceasta există. După cum s-a subliniat în articolul lui Graham Dumpleton de aici, este o caracteristică adesea trecută cu vederea a WSGI. Apelarea acestei metode, dacă există , permite aplicației să elibereze orice resurse pe care le poate deține în continuare.
Argumentul de environ al aplicației apelabile
Parametrul environ ar trebui să fie un obiect dicționar. Este folosit pentru a transmite informații despre solicitare și server către aplicație, în același mod în care face CGI. De fapt, toate variabilele de mediu CGI sunt valide în WSGI și serverul ar trebui să treacă tot ceea ce se aplică aplicației.
Deși există multe chei opționale care pot fi transmise, mai multe sunt obligatorii. Luând ca exemplu următoarea cerere GET :
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'Acestea sunt cheile pe care serverul trebuie să le furnizeze și valorile pe care le-ar lua:
| Cheie | Valoare | Comentarii |
|---|---|---|
REQUEST_METHOD | "GET" | |
SCRIPT_NAME | "" | depinde de configurarea serverului |
PATH_INFO | "/auth" | |
QUERY_STRING | "token=123" | |
CONTENT_TYPE | "" | |
CONTENT_LENGTH | "" | |
SERVER_NAME | "127.0.0.1" | depinde de configurarea serverului |
SERVER_PORT | "8000" | |
SERVER_PROTOCOL | "HTTP/1.1" | |
HTTP_(...) | Antetele HTTP furnizate de client | |
wsgi.version | (1, 0) | tuplu cu versiunea WSGI |
wsgi.url_scheme | "http" | |
wsgi.input | Obiect asemănător fișierului | |
wsgi.errors | Obiect asemănător fișierului | |
wsgi.multithread | False | True dacă serverul are mai multe fire |
wsgi.multiprocess | False | True dacă serverul rulează mai multe procese |
wsgi.run_once | False | True dacă serverul se așteaptă ca acest script să ruleze o singură dată (de exemplu: într-un mediu CGI) |
Excepția de la această regulă este că, dacă una dintre aceste chei ar fi goală (cum ar fi CONTENT_TYPE în tabelul de mai sus), atunci pot fi omise din dicționar și se va presupune că corespund șirului gol.
wsgi.input și wsgi.errors
Cele mai multe chei de environ sunt simple, dar două dintre ele merită puțin mai multe clarificări: wsgi.input , care trebuie să conțină un flux cu corpul solicitării de la client și wsgi.errors , unde aplicația raportează orice erori pe care le întâlnește. Erorile trimise de la aplicație către wsgi.errors ar fi de obicei trimise în jurnalul de erori de server.
Aceste două chei trebuie să conțină obiecte asemănătoare fișierelor; adică obiecte care oferă interfețe pentru a fi citite sau scrise ca fluxuri, la fel ca obiectul pe care îl primim când deschidem un fișier sau un socket în Python. Acest lucru poate părea dificil la început, dar, din fericire, Python ne oferă instrumente bune pentru a gestiona acest lucru.
În primul rând, despre ce fel de fluxuri vorbim? Conform definiției WSGI, wsgi.input și wsgi.errors trebuie să gestioneze obiectele bytes în Python 3 și obiectele str în Python 2. În orice caz, dacă dorim să folosim un buffer în memorie pentru a trece sau a obține date prin WSGI interfață, putem folosi clasa io.BytesIO .

De exemplu, dacă scriem un server WSGI, am putea furniza corpul cererii aplicației astfel:
- Pentru Python 2.7
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)- Pentru Python 3.5
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)Din partea aplicației, dacă dorim să transformăm o intrare de flux pe care am primit-o într-un șir, am dori să scriem ceva de genul acesta:
- Pentru Python 2.7
readstr = environ['wsgi.input'].read() # returns str object- Pentru Python 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object Fluxul wsgi.errors ar trebui folosit pentru a raporta erorile de aplicație către server, iar liniile ar trebui să se încheie cu un \n . Serverul web ar trebui să se ocupe de conversia la un alt sfârșit de linie în funcție de sistem.
Argumentul start_response al aplicației apelabile
Argumentul start_response trebuie să fie apelabil cu două argumente necesare, și anume status și headers , și un argument opțional, exc_info . Acesta trebuie să fie apelat de aplicație înainte ca orice parte a corpului să fie trimisă înapoi la serverul web.
În primul exemplu de aplicație de la începutul acestui articol, am returnat corpul răspunsului ca o listă și, prin urmare, nu avem control asupra momentului în care lista va fi iterată. Din această cauză, a trebuit să apelăm start_response înainte de a returna lista.
În al doilea, am numit start_response chiar înainte de a obține prima (și, în acest caz, singura) bucată din corpul răspunsului. Oricare dintre mod este valabil în cadrul specificațiilor WSGI.
Din partea serverului web, apelarea start_response nu ar trebui să trimită de fapt anteturile către client, ci să o întârzie până când există cel puțin un șir de octeți negol în corpul răspunsului pentru a trimite înapoi clientului. Această arhitectură permite raportarea corectă a erorilor până în ultimul moment posibil al execuției aplicației.
Argumentul de status al start_response
Argumentul de status transmis callback- start_response trebuie să fie un șir format dintr-un cod de stare HTTP și o descriere, separate printr-un singur spațiu. Exemplele valide sunt: '200 OK' sau '404 Not Found' .
Argumentul headers start_response
Argumentul headers transmis callback- start_response trebuie să fie o list Python de tuple , fiecare tuplu compus ca (header_name, header_value) . Atât numele, cât și valoarea fiecărui antet trebuie să fie șiruri de caractere (indiferent de versiunea Python). Acesta este un exemplu rar în care tipul contează, deoarece acest lucru este într-adevăr cerut de specificația WSGI.
Iată un exemplu valid despre cum poate arăta un argument de header :
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]Antetele HTTP nu țin cont de majuscule și minuscule, iar dacă scriem un server web compatibil WSGI, este ceva de care trebuie să țineți cont atunci când verificăm aceste anteturi. De asemenea, lista de anteturi furnizată de aplicație nu trebuie să fie exhaustivă. Este responsabilitatea serverului să se asigure că există toate anteturile HTTP necesare înainte de a trimite răspunsul înapoi către client, completând orice antete care nu sunt furnizate de aplicație.
Argumentul exc_info pentru start_response
Callback-ul start_response ar trebui să accepte un al treilea argument exc_info , folosit pentru tratarea erorilor. Utilizarea și implementarea corectă a acestui argument este de cea mai mare importanță pentru serverele și aplicațiile web de producție, dar este în afara domeniului de aplicare al acestui articol.
Mai multe informații despre acesta pot fi obținute în specificația WSGI, aici.
Valoarea start_response – Callback de write
Pentru scopuri de compatibilitate cu versiunea anterioară, serverele web care implementează WSGI ar trebui să returneze o write apelabilă. Acest apel invers ar trebui să permită aplicației să scrie datele răspunsului corpului direct înapoi către client, în loc să le transmită serverului printr-un iterator.
În ciuda prezenței sale, aceasta este o interfață depreciată și aplicațiile noi ar trebui să se abțină de la a o folosi.
Generarea Corpului de Răspuns
Aplicațiile care implementează WSGI ar trebui să genereze corpul de răspuns prin returnarea unui obiect iterabil. Pentru majoritatea aplicațiilor, corpul răspunsului nu este foarte mare și se potrivește cu ușurință în memoria serverului. În acest caz, cel mai eficient mod de a-l trimite este o dată, cu un element iterabil. În cazuri speciale, în care încărcarea întregului corp în memorie este imposibilă, aplicația îl poate returna parțial prin această interfață iterabilă.
Există doar o mică diferență aici între WSGI al lui Python 2 și al lui Python 3: în Python 3, corpul răspunsului este reprezentat de obiecte bytes ; în Python 2, tipul corect pentru aceasta este str .
Convertirea șirurilor UTF-8 în bytes sau str este o sarcină ușoară:
- Python 3.5:
body = 'unicode stuff'.encode('utf-8')- Python 2.7:
body = u'unicode stuff'.encode('utf-8')Dacă doriți să aflați mai multe despre gestionarea unicode și bytestring din Python 2, există un tutorial frumos pe YouTube.
Serverele web care implementează WSGI ar trebui să accepte, de asemenea, callback-ul de write pentru compatibilitate inversă, așa cum este descris mai sus.
Testarea aplicației dvs. fără un server web
Cu o înțelegere a acestei interfețe simple, putem crea cu ușurință scripturi pentru a ne testa aplicațiile fără a fi nevoie de fapt să pornim un server.
Luați acest mic script, de exemplu:
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} În acest fel, am putea, de exemplu, să inițializam unele date de testare și să simulam module în aplicația noastră și să facem apeluri GET pentru a testa dacă răspunde în consecință. Putem vedea că nu este un server web real, ci interfață cu aplicația noastră într-un mod comparabil, oferind aplicației un apel invers start_response și un dicționar cu variabilele noastre de mediu. La sfârșitul solicitării, consumă iteratorul corpului răspunsului și returnează un șir cu tot conținutul său. Metode similare (sau una generală) pot fi create pentru diferite tipuri de solicitări HTTP.
Învelire
În acest articol, nu am abordat modul în care WSGI tratează încărcările de fișiere, deoarece aceasta ar putea fi considerată o caracteristică mai „avansată”, nepotrivită pentru un articol introductiv. Dacă doriți să aflați mai multe despre acesta, aruncați o privire la secțiunea PEP-3333 care se referă la gestionarea fișierelor.
Sper că acest articol este util pentru a ajuta la o mai bună înțelegere a modului în care Python vorbește cu serverele web și le permite dezvoltatorilor să folosească această interfață în moduri interesante și creative.
Mulțumiri
Aș dori să mulțumesc editorului meu Nick McCrea pentru că m-a ajutat cu acest articol. Datorită muncii sale, textul original a devenit mult mai clar și mai multe erori nu au rămas necorectate.
