WSGI: interfejs serwer-aplikacja dla Pythona

Opublikowany: 2022-03-11

W 1993 r. sieć była jeszcze w powijakach, z około 14 milionami użytkowników i 100 stronami internetowymi. Strony były statyczne, ale istniała już potrzeba tworzenia dynamicznych treści, takich jak aktualne wiadomości i dane. W odpowiedzi na to Rob McCool i inni współpracownicy wdrożyli interfejs Common Gateway Interface (CGI) na serwerze WWW HTTPd National Center for Supercomputing Applications (NCSA) (poprzednik Apache). Był to pierwszy serwer WWW, który mógł obsługiwać treści generowane przez osobną aplikację.

Od tego czasu liczba użytkowników w Internecie eksplodowała, a dynamiczne strony internetowe stały się wszechobecne. Kiedy po raz pierwszy uczą się nowego języka lub nawet po raz pierwszy uczą się kodowania, programiści wkrótce chcą wiedzieć, jak podłączyć swój kod do sieci.

Python w sieci i rozwój WSGI

Od czasu powstania CGI wiele się zmieniło. Podejście CGI stało się niepraktyczne, ponieważ wymagało tworzenia nowego procesu przy każdym żądaniu, marnując pamięć i procesor. Pojawiły się inne podejścia niskopoziomowe, takie jak FastCGI](http://www.fastcgi.com/) (1996) i mod_python (2000), zapewniające różne interfejsy między frameworkami WWW Pythona a serwerem WWW. W miarę rozpowszechniania się różnych podejść, wybór frameworka przez dewelopera ograniczył wybór serwerów internetowych i na odwrót.

Aby rozwiązać ten problem, w 2003 roku Phillip J. Eby zaproponował PEP-0333, Python Web Server Gateway Interface (WSGI). Pomysł polegał na zapewnieniu wysokiego poziomu, uniwersalnego interfejsu między aplikacjami Pythona a serwerami WWW.

W 2003 roku PEP-3333 zaktualizował interfejs WSGI, aby dodać obsługę Pythona 3. Obecnie prawie wszystkie frameworki Pythona używają WSGI jako środka, jeśli nie jedynego środka do komunikacji ze swoimi serwerami sieciowymi. Tak robią to Django, Flask i wiele innych popularnych frameworków.

Ten artykuł ma na celu dać czytelnikowi wgląd w działanie WSGI i umożliwić mu zbudowanie prostej aplikacji lub serwera WSGI. Nie jest to jednak wyczerpujące, a programiści zamierzający wdrożyć serwery lub aplikacje gotowe do produkcji powinni dokładniej przyjrzeć się specyfikacji WSGI.

Interfejs WSGI w Pythonie

WSGI określa proste reguły, z którymi musi się dostosować serwer i aplikacja. Zacznijmy od przeglądu tego ogólnego wzorca.

Interfejs aplikacji serwerowej Python WSGI.

Interfejs aplikacji

W Pythonie 3.5 interfejsy aplikacji wyglądają tak:

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

W Pythonie 2.7 ten interfejs nie różniłby się zbytnio; jedyną zmianą byłoby to, że ciało jest reprezentowane przez obiekt str , a nie przez bytes jeden.

Chociaż w tym przypadku użyliśmy funkcji, wystarczy każda wywoływalna. Oto zasady dla obiektu aplikacji:

  • Musi być wywoływalną z parametrami environ i start_response .
  • Musi wywołać wywołanie zwrotne start_response przed wysłaniem treści.
  • Musi zwrócić element iterowany z fragmentami treści dokumentu.

Innym przykładem obiektu, który spełnia te zasady i przyniesie ten sam efekt, jest:

 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

Interfejs serwera

Serwer WSGI może łączyć się z tą aplikacją w następujący sposób:

 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()

Jak mogłeś zauważyć, funkcja wywoływalna start_response wywoływalną funkcję write , której aplikacja może użyć do wysłania danych z powrotem do klienta, ale nie została ona użyta w naszym przykładzie kodu aplikacji. Ten interfejs write jest przestarzały i możemy go na razie zignorować. Zostanie to pokrótce omówione w dalszej części artykułu.

Inną osobliwością odpowiedzialności serwera jest wywołanie opcjonalnej metody close w iteratorze odpowiedzi, jeśli istnieje. Jak wskazano w artykule Grahama Dumpletona, jest to często pomijana funkcja WSGI. Wywołanie tej metody, jeśli istnieje , umożliwia aplikacji zwolnienie wszelkich zasobów, które może nadal posiadać.

Argument dotyczący environ wywoływanego aplikacji

Parametr environ powinien być obiektem słownikowym. Służy do przekazywania żądań i informacji o serwerze do aplikacji, podobnie jak robi to CGI. W rzeczywistości wszystkie zmienne środowiskowe CGI są poprawne w WSGI i serwer powinien przekazać wszystko, co dotyczy aplikacji.

Chociaż istnieje wiele opcjonalnych kluczy, które można przekazać, kilka jest obowiązkowych. Biorąc jako przykład następujące żądanie GET :

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

Oto klucze, które serwer musi dostarczyć, oraz wartości, które przyjmą:

Klucz Wartość Uwagi
REQUEST_METHOD "GET"
SCRIPT_NAME "" Zależna od konfiguracji serwera
PATH_INFO "/auth"
QUERY_STRING "token=123"
CONTENT_TYPE ""
CONTENT_LENGTH ""
SERVER_NAME "127.0.0.1" Zależna od konfiguracji serwera
SERVER_PORT "8000"
SERVER_PROTOCOL "HTTP/1.1"
HTTP_(...) Nagłówki HTTP dostarczone przez klienta
wsgi.version (1, 0) krotka z wersją WSGI
wsgi.url_scheme "http"
wsgi.input Obiekt podobny do pliku
wsgi.errors Obiekt podobny do pliku
wsgi.multithread False True , jeśli serwer jest wielowątkowy
wsgi.multiprocess False True , jeśli serwer obsługuje wiele procesów
wsgi.run_once False True , jeśli serwer oczekuje, że ten skrypt zostanie uruchomiony tylko raz (np. w środowisku CGI)

Wyjątkiem od tej reguły jest to, że jeśli jeden z tych kluczy miałby być pusty (jak CONTENT_TYPE w powyższej tabeli), to można je pominąć w słowniku i przyjąć, że odpowiadają one pustemu ciągowi.

wsgi.input i wsgi.errors

Większość kluczy environ jest prosta, ale dwa z nich zasługują na nieco więcej wyjaśnienia: wsgi.input , który musi zawierać strumień z treścią żądania od klienta, oraz wsgi.errors , w którym aplikacja zgłasza wszelkie napotkane błędy. Błędy wysyłane z aplikacji do wsgi.errors zazwyczaj trafiają do dziennika błędów serwera.

Te dwa klucze muszą zawierać obiekty plikopodobne; to znaczy obiekty, które zapewniają interfejsy do odczytu lub zapisu jako strumienie, podobnie jak obiekt, który otrzymujemy, gdy otwieramy plik lub gniazdo w Pythonie. Na początku może się to wydawać trudne, ale na szczęście Python daje nam dobre narzędzia do obsługi tego.

Po pierwsze, o jakich strumieniach mówimy? Zgodnie z definicją WSGI, wsgi.input i wsgi.errors muszą obsługiwać obiekty bytes w Pythonie 3 i obiekty str w Pythonie 2. W obu przypadkach, jeśli chcemy użyć bufora w pamięci do przekazywania lub pobierania danych przez WSGI interfejs, możemy użyć klasy io.BytesIO .

Na przykład, jeśli piszemy serwer WSGI, możemy dostarczyć treść żądania do aplikacji w następujący sposób:

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

Po stronie aplikacji, gdybyśmy chcieli zamienić otrzymany strumień w ciąg, chcielibyśmy napisać coś takiego:

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

Strumień wsgi.errors powinien być używany do zgłaszania błędów aplikacji do serwera, a wiersze powinny być zakończone znakiem \n . Serwer WWW powinien zadbać o konwersję na inne zakończenie linii zgodnie z systemem.

Argument start_response aplikacji

Argument start_response musi być wywoływalny z dwoma wymaganymi argumentami, mianowicie status i headers oraz jednym opcjonalnym argumentem exc_info . Musi zostać wywołana przez aplikację, zanim jakakolwiek część treści zostanie odesłana z powrotem na serwer WWW.

W pierwszym przykładzie aplikacji na początku tego artykułu zwróciliśmy treść odpowiedzi jako listę, a zatem nie mamy kontroli nad tym, kiedy lista będzie iterowana. Z tego powodu musieliśmy wywołać start_response przed zwróceniem listy.

W drugim start_response tuż przed przekazaniem pierwszego (i w tym przypadku tylko) fragmentu treści odpowiedzi. W obu przypadkach obowiązuje specyfikacja WSGI.

Po stronie serwera WWW wywołanie start_response nie powinno w rzeczywistości wysyłać nagłówków do klienta, ale opóźniać je do momentu, w którym w treści odpowiedzi pojawi się co najmniej jeden niepusty test bajtowy, który należy odesłać do klienta. Architektura ta pozwala na poprawne raportowanie błędów do ostatniego możliwego momentu wykonania aplikacji.

status Argument start_response

Argument status przekazywany do wywołania zwrotnego start_response musi być ciągiem znaków składającym się z kodu statusu HTTP i opisu, oddzielonych pojedynczą spacją. Prawidłowe przykłady to: '200 OK' lub '404 Not Found' .

headers Argument start_response

Argument headers przekazany do wywołania zwrotnego start_response musi być list tuple Pythona, z każdą krotką utworzoną jako (header_name, header_value) . Zarówno nazwa, jak i wartość każdego nagłówka muszą być łańcuchami (niezależnie od wersji Pythona). Jest to rzadki przykład, w którym typ ma znaczenie, ponieważ jest to rzeczywiście wymagane przez specyfikację WSGI.

Oto poprawny przykład tego, jak może wyglądać argument header :

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

W nagłówkach HTTP nie jest rozróżniana wielkość liter, a jeśli piszemy serwer sieciowy zgodny z WSGI, należy wziąć to pod uwagę podczas sprawdzania tych nagłówków. Również lista nagłówków dostarczana przez aplikację nie powinna być wyczerpująca. Obowiązkiem serwera jest upewnienie się, że wszystkie wymagane nagłówki HTTP istnieją przed wysłaniem odpowiedzi z powrotem do klienta, wypełniając wszelkie nagłówki nie dostarczone przez aplikację.

exc_info Argument start_response

Wywołanie zwrotne start_response powinno obsługiwać trzeci argument exc_info , używany do obsługi błędów. Prawidłowe użycie i implementacja tego argumentu ma ogromne znaczenie dla produkcyjnych serwerów WWW i aplikacji, ale wykracza poza zakres tego artykułu.

Więcej informacji na ten temat można znaleźć w specyfikacji WSGI, tutaj.

Wartość start_responsewrite wywołania zwrotnego

Dla celów kompatybilności wstecznej, serwery WWW implementujące WSGI powinny zwracać wywoływany write . To wywołanie zwrotne powinno umożliwić aplikacji zapisywanie danych odpowiedzi bezpośrednio z powrotem do klienta, zamiast przekazywania ich do serwera za pośrednictwem iteratora.

Mimo swojej obecności jest to przestarzały interfejs i nowe aplikacje powinny powstrzymać się od jego używania.

Generowanie ciała odpowiedzi

Aplikacje implementujące WSGI powinny generować treść odpowiedzi, zwracając obiekt iterowalny. W przypadku większości aplikacji korpus odpowiedzi nie jest bardzo duży i łatwo mieści się w pamięci serwera. W takim przypadku najskuteczniejszym sposobem wysyłania jest wszystko na raz, z iterowalnością jednoelementową. W szczególnych przypadkach, gdy załadowanie całego ciała do pamięci jest niewykonalne, aplikacja może zwrócić je część po części przez ten iterowalny interfejs.

Istnieje tylko niewielka różnica między WSGI Pythona 2 i Pythona 3: w Pythonie 3 treść odpowiedzi jest reprezentowana przez obiekty bytes ; w Pythonie 2 poprawnym typem jest str .

Konwersja ciągów znaków UTF-8 na bytes lub str jest łatwym zadaniem:

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

Jeśli chcesz dowiedzieć się więcej o obsłudze Unicode i bytestring w Pythonie 2, na YouTube znajdziesz fajny samouczek.

Serwery WWW implementujące WSGI powinny również obsługiwać wywołanie zwrotne write w celu zapewnienia kompatybilności wstecznej, jak opisano powyżej.

Testowanie aplikacji bez serwera WWW

Rozumiejąc ten prosty interfejs, możemy łatwo tworzyć skrypty do testowania naszych aplikacji bez konieczności uruchamiania serwera.

Weźmy na przykład ten mały skrypt:

 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}

W ten sposób możemy na przykład zainicjować niektóre dane testowe i symulować moduły w naszej aplikacji i wykonać wywołania GET w celu przetestowania, czy odpowiednio zareaguje. Widzimy, że nie jest to rzeczywisty serwer WWW, ale interfejs z naszą aplikacją w porównywalny sposób, dostarczając aplikacji wywołanie zwrotne start_response i słownik z naszymi zmiennymi środowiskowymi. Na końcu żądania zużywa iterator treści odpowiedzi i zwraca ciąg z całą jego zawartością. Podobne metody (lub ogólną) można utworzyć dla różnych typów żądań HTTP.

Zakończyć

WSGI jest krytyczną częścią prawie każdego frameworka WWW w Pythonie.

W tym artykule nie podeszliśmy do tego, jak WSGI radzi sobie z przesyłaniem plików, ponieważ można to uznać za bardziej „zaawansowaną” funkcję, nieodpowiednią do artykułu wprowadzającego. Jeśli chcesz dowiedzieć się więcej na ten temat, zajrzyj do sekcji PEP-3333 dotyczącej obsługi plików.

Mam nadzieję, że ten artykuł pomoże lepiej zrozumieć, w jaki sposób Python komunikuje się z serwerami sieciowymi, i pozwoli programistom korzystać z tego interfejsu w ciekawy i kreatywny sposób.

Podziękowanie

Chciałbym podziękować mojemu redaktorowi Nickowi McCrea za pomoc przy tym artykule. Dzięki jego pracy oryginalny tekst stał się znacznie jaśniejszy, a kilka błędów nie pozostało niepoprawnych.