Το 1993, ο ιστός ήταν ακόμη στα σπάργανα, με περίπου 14 εκατομμύρια χρήστες και 100 ιστότοποι . Οι σελίδες ήταν στατικές αλλά υπήρχε ήδη ανάγκη δημιουργίας δυναμικού περιεχομένου, όπως ενημερωμένων ειδήσεων και δεδομένων. Απαντώντας σε αυτό, ο Rob McCool και άλλοι συνεισφέροντες εφάρμοσαν το Common Gateway Interface (CGI) στο Εθνικό Κέντρο Υπερυπολογιστικών Εφαρμογών (NCSA) Διακομιστής ιστού HTTPd (ο πρόδρομος του Apache). Αυτός ήταν ο πρώτος διακομιστής ιστού που μπορούσε να εξυπηρετήσει περιεχόμενο που δημιουργήθηκε από μια ξεχωριστή εφαρμογή.
Από τότε, ο αριθμός των χρηστών στο Διαδίκτυο έχει εκραγεί και δυναμικοί ιστότοποι έχουν γίνει πανταχού παρόντες. Κατά την πρώτη εκμάθηση μιας νέας γλώσσας ή ακόμη και την πρώτη εκμάθηση κωδικών, οι προγραμματιστές, σύντομα, θέλουν να μάθουν πώς να συνδέσουν τον κώδικά τους στον ιστό.
Από τη δημιουργία του CGI, πολλά έχουν αλλάξει. Η προσέγγιση CGI έγινε ανέφικτη, καθώς απαιτούσε τη δημιουργία μιας νέας διαδικασίας σε κάθε αίτημα, σπατάλη μνήμης και CPU. Μερικές άλλες προσεγγίσεις χαμηλού επιπέδου εμφανίστηκαν, όπως το FastCGI] (http://www.fastcgi.com/) (1996) και mod_python (2000), παρέχοντας διαφορετικές διεπαφές μεταξύ των πλαισίων Ιστού Python και του διακομιστή Ιστού. Καθώς πολλαπλασιάστηκαν διαφορετικές προσεγγίσεις, η επιλογή του πλαισίου του προγραμματιστή κατέληξε να περιορίζει τις επιλογές των διακομιστών ιστού και το αντίστροφο.
Για να αντιμετωπιστεί αυτό το πρόβλημα, το 2003 ο Phillip J. Eby πρότεινε PEP-0333 , το Python Web Server Gateway Interface (WSGI). Η ιδέα ήταν να παρέχουμε ένα υψηλού επιπέδου, καθολική διεπαφή μεταξύ εφαρμογών Python και διακομιστών ιστού.
Το 2003, PEP-3333 ενημέρωσε τη διεπαφή WSGI για να προσθέσει υποστήριξη Python 3. Σήμερα, σχεδόν όλα τα πλαίσια της Python χρησιμοποιούν το WSGI ως μέσο, αν όχι το μόνο μέσο, για επικοινωνία με τους διακομιστές τους. Ετσι Τζάνγκο , Φλάσκα και πολλά άλλα δημοφιλή πλαίσια το κάνουν.
Αυτό το άρθρο σκοπεύει να δώσει στον αναγνώστη μια ματιά στον τρόπο λειτουργίας του WSGI και να επιτρέψει στον αναγνώστη να δημιουργήσει μια απλή εφαρμογή ή διακομιστή WSGI. Ωστόσο, δεν προορίζεται να είναι εξαντλητικός, και οι προγραμματιστές που σκοπεύουν να εφαρμόσουν διακομιστές ή εφαρμογές έτοιμες για παραγωγή θα πρέπει να ρίξουν μια πιο προσεκτική ματιά στο Προδιαγραφή WSGI .
Το WSGI καθορίζει απλούς κανόνες στους οποίους πρέπει να συμμορφώνεται ο διακομιστής και η εφαρμογή. Ας ξεκινήσουμε εξετάζοντας αυτό το συνολικό μοτίβο.
Στο Python 3.5, οι διεπαφές εφαρμογής έχουν ως εξής:
def application(environ, start_response): body = b'Hello world!
' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]
Στο Python 2.7, αυτή η διεπαφή δεν θα ήταν πολύ διαφορετική. η μόνη αλλαγή θα ήταν ότι το σώμα αντιπροσωπεύεται από ένα str
αντικείμενο, αντί για bytes
ένας.
Αν και έχουμε χρησιμοποιήσει μια συνάρτηση σε αυτήν την περίπτωση, οποιαδήποτε καλεί θα κάνω. Οι κανόνες για το αντικείμενο εφαρμογής εδώ είναι:
environ
και start_response
Παράμετροι. start_response
επιστροφή κλήσης πριν από την αποστολή του σώματος. Ένα άλλο παράδειγμα ενός αντικειμένου που ικανοποιεί αυτούς τους κανόνες και θα παράγει το ίδιο αποτέλεσμα είναι:
class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!
' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body
Ένας διακομιστής WSGI ενδέχεται να διασυνδέεται με αυτήν την εφαρμογή ως εξής ::
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()
Όπως ίσως έχετε παρατηρήσει, το start_response
callable επέστρεψε ένα write
κλήση που μπορεί να χρησιμοποιήσει η εφαρμογή για την αποστολή δεδομένων πίσω στον πελάτη, αλλά αυτό δεν χρησιμοποιήθηκε από το παράδειγμα κώδικα εφαρμογής μας. Αυτό write
η διεπαφή έχει καταργηθεί και μπορούμε να την αγνοήσουμε για τώρα Θα συζητηθεί σύντομα αργότερα στο άρθρο.
Μια άλλη ιδιαιτερότητα των ευθυνών του διακομιστή είναι να καλέσετε το προαιρετικό close
μέθοδος στο επαναληπτικό απόκρισης, εάν υπάρχει. Όπως επισημαίνεται στο άρθρο του Graham Dumpleton εδώ , είναι ένα χαρακτηριστικό του WSGI που συχνά παραβλέπεται. Καλώντας αυτήν τη μέθοδο, αν υπαρχει , επιτρέπει στην εφαρμογή να αποδεσμεύει τυχόν πόρους που ενδέχεται να διατηρεί.
environ
ΔιαφωνίαΤο environ
η παράμετρος πρέπει να είναι αντικείμενο λεξικού. Χρησιμοποιείται για τη μετάδοση πληροφοριών αιτήματος και διακομιστή στην εφαρμογή, όπως και το CGI. Στην πραγματικότητα, όλες οι μεταβλητές περιβάλλοντος CGI είναι έγκυρες στο WSGI και ο διακομιστής πρέπει να περάσει όλα όσα ισχύουν για την εφαρμογή.
Ενώ υπάρχουν πολλά προαιρετικά κλειδιά που μπορούν να περάσουν, πολλά είναι υποχρεωτικά. Λαμβάνοντας ως παράδειγμα τα ακόλουθα GET
αίτηση:
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'
Αυτά είναι τα κλειδιά που ο διακομιστής πρέπει παρέχουν και τις τιμές που θα λαμβάνουν:
Κλειδί | αξία | Σχόλια |
---|---|---|
REQUEST_METHOD | 'GET' | |
SCRIPT_NAME | '' | εξαρτάται από τη ρύθμιση του διακομιστή |
PATH_INFO | '/auth' | |
QUERY_STRING | 'token=123' | |
CONTENT_TYPE | '' | |
CONTENT_LENGTH | '' | |
SERVER_NAME | '127.0.0.1' | εξαρτάται από τη ρύθμιση του διακομιστή |
SERVER_PORT | '8000' | |
SERVER_PROTOCOL | 'HTTP/1.1' | |
HTTP_(...) | Παρέχονται από τον πελάτη κεφαλίδες HTTP | |
wsgi.version | (1, 0) | πλειάδα με έκδοση WSGI |
wsgi.url_scheme | 'http' | |
wsgi.input | Αντικείμενο που μοιάζει με αρχείο | |
wsgi.errors | Αντικείμενο που μοιάζει με αρχείο | |
wsgi.multithread | False | True εάν ο διακομιστής είναι πολλαπλών νημάτων |
wsgi.multiprocess | False | True εάν ο διακομιστής εκτελεί πολλές διαδικασίες |
wsgi.run_once | False | True εάν ο διακομιστής αναμένει ότι αυτό το σενάριο θα εκτελεστεί μόνο μία φορά (π.χ. σε περιβάλλον CGI) |
Η εξαίρεση σε αυτόν τον κανόνα είναι ότι εάν ένα από αυτά τα πλήκτρα ήταν κενό (όπως CONTENT_TYPE
στον παραπάνω πίνακα), τότε μπορούν να παραλειφθούν από το λεξικό και θα υποτεθεί ότι αντιστοιχούν στην κενή συμβολοσειρά.
wsgi.input
και wsgi.errors
Τα περισσότερα environ
Τα πλήκτρα είναι απλά, αλλά δύο από αυτά αξίζουν λίγο περισσότερη διευκρίνιση: wsgi.input
, η οποία πρέπει να περιέχει μια ροή με το σώμα αιτήματος από τον πελάτη και wsgi.errors
, όπου η εφαρμογή αναφέρει τυχόν σφάλματα που συναντά. Σφάλματα που στάλθηκαν από την εφαρμογή στο wsgi.errors
συνήθως αποστέλλεται στο αρχείο καταγραφής σφαλμάτων διακομιστή.
Αυτά τα δύο κλειδιά πρέπει να περιέχουν αντικείμενα που μοιάζουν με αρχεία. δηλαδή, αντικείμενα που παρέχουν διασυνδέσεις για ανάγνωση ή εγγραφή σε ροές, όπως και το αντικείμενο που λαμβάνουμε όταν ανοίγουμε ένα αρχείο ή μια υποδοχή στο Python. Αυτό μπορεί να φαίνεται δύσκολο στην αρχή, αλλά ευτυχώς, η Python μας δίνει καλά εργαλεία για να το χειριστούμε.
Πρώτον, για τι είδους ροές μιλάμε; Σύμφωνα με τον ορισμό WSGI, wsgi.input
και wsgi.errors
πρέπει να χειριστεί bytes
αντικείμενα στο Python 3 και str
αντικείμενα στο Python 2. Σε κάθε περίπτωση, εάν θέλουμε να χρησιμοποιήσουμε ένα buffer στη μνήμη για να περάσουμε ή να λάβουμε δεδομένα μέσω της διεπαφής WSGI, μπορούμε να χρησιμοποιήσουμε την κλάση io.BytesIO
.
Για παράδειγμα, εάν γράφουμε διακομιστή WSGI, θα μπορούσαμε να παρέχουμε το σώμα αιτήματος στην εφαρμογή όπως αυτό:
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)
Από την πλευρά της εφαρμογής, εάν θέλαμε να μετατρέψουμε μια είσοδο ροής που λάβαμε σε συμβολοσειρά, θα θέλαμε να γράψουμε κάτι σαν αυτό:
readstr = environ['wsgi.input'].read() # returns str object
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object
Το wsgi.errors
Η ροή πρέπει να χρησιμοποιείται για την αναφορά σφαλμάτων εφαρμογής στο διακομιστή και οι γραμμές πρέπει να τερματίζονται με
. Ο διακομιστής Ιστού πρέπει να φροντίσει να μετατρέψει σε διαφορετική γραμμή που τελειώνει σύμφωνα με το σύστημα.
start_response
ΔιαφωνίαΤο start_response
Το όρισμα πρέπει να είναι callable με δύο απαιτούμενα ορίσματα, δηλαδή status
και headers
, και ένα προαιρετικό όρισμα, exc_info
. Πρέπει να καλείται από την εφαρμογή προτού αποσταλεί οποιοδήποτε μέρος του σώματος στον διακομιστή ιστού.
Στο πρώτο παράδειγμα εφαρμογής στην αρχή αυτού του άρθρου, επιστρέψαμε το κύριο μέρος της απάντησης ως λίστα και, ως εκ τούτου, δεν έχουμε κανέναν έλεγχο για το πότε θα επαναληφθεί η λίστα. Εξαιτίας αυτού, έπρεπε να καλέσουμε start_response
πριν επιστρέψετε στη λίστα.
Στο δεύτερο, καλέσαμε start_response
λίγο πριν αποδώσετε το πρώτο (και, σε αυτήν την περίπτωση, μόνο) κομμάτι του σώματος απόκρισης. Και οι δύο τρόποι είναι έγκυροι σύμφωνα με τις προδιαγραφές WSGI.
Από την πλευρά του διακομιστή ιστού, η κλήση του start_response
δεν πρέπει να στείλετε τις κεφαλίδες στον πελάτη, αλλά να το καθυστερήσετε έως ότου υπάρχει τουλάχιστον ένα μη κενό κενό στο σώμα απόκρισης για αποστολή στον πελάτη. Αυτή η αρχιτεκτονική επιτρέπει τη σωστή αναφορά σφαλμάτων μέχρι την τελευταία δυνατή στιγμή της εκτέλεσης της εφαρμογής.
σύναψη επιχειρηματικού σχεδίου
status
Το επιχείρημα της start_response
Το status
το όρισμα πέρασε στο start_response
Η επιστροφή κλήσης πρέπει να είναι μια συμβολοσειρά που αποτελείται από έναν κωδικό κατάστασης HTTP και μια περιγραφή, διαχωρισμένη με ένα κενό διάστημα. Τα έγκυρα παραδείγματα είναι: '200 OK'
, ή '404 Not Found'
.
headers
Το επιχείρημα της start_response
Το headers
το όρισμα πέρασε στο start_response
Η επανάκληση πρέπει να είναι Python list
από tuple
s, με κάθε πλειάδα να συντίθεται ως (header_name, header_value)
. Τόσο το όνομα όσο και η τιμή κάθε κεφαλίδας πρέπει να είναι συμβολοσειρές (ανεξάρτητα από την έκδοση Python). Αυτό είναι ένα σπάνιο παράδειγμα στο οποίο έχει σημασία ο τύπος, καθώς αυτό απαιτείται πράγματι από την προδιαγραφή WSGI.
Εδώ είναι ένα έγκυρο παράδειγμα του τι a header
το επιχείρημα μπορεί να μοιάζει με:
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]
Οι κεφαλίδες HTTP δεν έχουν διάκριση πεζών-κεφαλαίων και αν γράφουμε έναν διακομιστή ιστού συμβατό με WSGI, αυτό είναι κάτι που πρέπει να λάβετε υπόψη κατά τον έλεγχο αυτών των κεφαλίδων. Επίσης, η λίστα των κεφαλίδων που παρέχεται από την εφαρμογή δεν πρέπει να είναι εξαντλητική. Είναι ευθύνη του διακομιστή να διασφαλίσει ότι υπάρχουν όλες οι απαιτούμενες κεφαλίδες HTTP πριν αποστείλετε την απάντηση πίσω στον πελάτη, συμπληρώνοντας τυχόν κεφαλίδες που δεν παρέχονται από την εφαρμογή.
exc_info
Το επιχείρημα της start_response
Το start_response
Η επιστροφή κλήσης πρέπει να υποστηρίζει ένα τρίτο όρισμα exc_info
, που χρησιμοποιείται για τον χειρισμό σφαλμάτων. Η σωστή χρήση και εφαρμογή αυτού του επιχειρήματος είναι υψίστης σημασίας για τους διακομιστές και τις εφαρμογές web παραγωγής, αλλά δεν εμπίπτει στο πεδίο εφαρμογής αυτού του άρθρου.
Περισσότερες πληροφορίες σχετικά με αυτό μπορούν να ληφθούν στην προδιαγραφή WSGI, εδώ .
start_response
Αξία επιστροφής - Η write
Επιστροφή κλήσηςΓια σκοπούς συμβατότητας προς τα πίσω, οι διακομιστές ιστού που εφαρμόζουν WSGI θα πρέπει να επιστρέφουν ένα write
καλεί. Αυτή η επιστροφή θα πρέπει να επιτρέπει στην εφαρμογή να γράφει δεδομένα απόκρισης σώματος απευθείας στον πελάτη, αντί να τα αποδίδει στο διακομιστή μέσω επαναληπτικού.
Παρά την παρουσία του, αυτή είναι μια καταργημένη διεπαφή και οι νέες εφαρμογές πρέπει να απέχουν από τη χρήση της.
Οι εφαρμογές που εφαρμόζουν το WSGI θα πρέπει να δημιουργήσουν το σώμα απόκρισης επιστρέφοντας ένα επαναλαμβανόμενο αντικείμενο. Για τις περισσότερες εφαρμογές, το σώμα απόκρισης δεν είναι πολύ μεγάλο και χωράει εύκολα στη μνήμη του διακομιστή. Σε αυτήν την περίπτωση, ο πιο αποτελεσματικός τρόπος αποστολής είναι όλα ταυτόχρονα, με ένα στοιχείο επαναλαμβανόμενο. Σε ειδικές περιπτώσεις, όπου η φόρτωση ολόκληρου του σώματος στη μνήμη δεν είναι εφικτή, η εφαρμογή μπορεί να το επιστρέψει εν μέρει μέσω αυτής της επαναλαμβανόμενης διεπαφής.
Υπάρχει μόνο μια μικρή διαφορά εδώ μεταξύ του Python 2 και του WSGI του Python 3: στο Python 3, το σώμα απόκρισης αντιπροσωπεύεται από bytes
αντικείμενα στο Python 2, ο σωστός τύπος για αυτό είναι str
.
Μετατροπή συμβολοσειρών UTF-8 σε bytes
ή str
είναι μια εύκολη εργασία:
body = 'unicode stuff'.encode('utf-8')
body = u'unicode stuff'.encode('utf-8')
Αν θέλετε να μάθετε περισσότερα σχετικά με το unicode του Python 2 και τον χειρισμό bytesting, υπάρχει ένα ωραίο σεμινάριο για Youtube .
Οι διακομιστές Ιστού που εφαρμόζουν WSGI θα πρέπει επίσης να υποστηρίζουν το write
callback για συμβατότητα προς τα πίσω, όπως περιγράφεται παραπάνω.
Με την κατανόηση αυτής της απλής διεπαφής, μπορούμε εύκολα να δημιουργήσουμε σενάρια για να δοκιμάσουμε τις εφαρμογές μας χωρίς να χρειαστεί να ξεκινήσουμε έναν διακομιστή.
Πάρτε αυτό το μικρό σενάριο, για παράδειγμα:
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}
Με αυτόν τον τρόπο, θα μπορούσαμε, για παράδειγμα, να αρχίσουμε ορισμένα δεδομένα δοκιμών και πλαστά στοιχεία στην εφαρμογή μας και να κάνουμε GET
καλεί για να ελέγξει εάν ανταποκρίνεται ανάλογα. Μπορούμε να δούμε ότι δεν είναι πραγματικός διακομιστής ιστού, αλλά διασυνδέεται με την εφαρμογή μας με συγκρίσιμο τρόπο παρέχοντας στην εφαρμογή ένα start_response
επανάκληση και ένα λεξικό με τις μεταβλητές περιβάλλοντος. Στο τέλος του αιτήματος, καταναλώνει τον επαναληπτικό του σώματος απόκρισης και επιστρέφει μια συμβολοσειρά με όλο το περιεχόμενό της. Παρόμοιες μέθοδοι (ή γενικές) μπορούν να δημιουργηθούν για διαφορετικούς τύπους αιτημάτων HTTP.
Σε αυτό το άρθρο, δεν έχουμε προσεγγίσει τον τρόπο με τον οποίο το WSGI ασχολείται με τις μεταφορτώσεις αρχείων, καθώς αυτό θα μπορούσε να θεωρηθεί μια πιο «προηγμένη» δυνατότητα, δεν είναι κατάλληλη για ένα εισαγωγικό άρθρο. Αν θέλετε να μάθετε περισσότερα για αυτό, ρίξτε μια ματιά στο Ενότητα PEP-3333 που αναφέρεται στο χειρισμό αρχείων .
Ελπίζω ότι αυτό το άρθρο είναι χρήσιμο για τη δημιουργία μιας καλύτερης κατανόησης του τρόπου με τον οποίο η Python μιλά σε διακομιστές ιστού και επιτρέπει στους προγραμματιστές να χρησιμοποιούν αυτήν τη διεπαφή με ενδιαφέροντες και δημιουργικούς τρόπους.
Θα ήθελα να ευχαριστήσω τον συντάκτη μου Νικ Μακρέα για να με βοηθήσετε με αυτό το άρθρο. Λόγω του έργου του, το αρχικό κείμενο έγινε πολύ πιο σαφές και πολλά λάθη δεν διορθώθηκαν.