WSGI is a term often referred to in Python Web development, and is defined in Wikipedia as follows.
The Python Web Server Gateway Interface (abbreviated WSGI) is a simple and generic interface between a web server and a web application or framework defined for the Python language. Since WSGI was developed, similar interfaces have appeared in many other languages.
As defined, WSGI is not a server, not an API, not a Python module, but an interface specification that specifies the interaction between a server and a client.
The goal of WSGI is to provide a common API standard between the Web server and Web framework layers, reducing interoperability and forming a uniform invocation. According to this definition, a Web server that satisfies WSGI will pass two fixed parameters into the WSGI APP: a dictionary of environment variables and a callable object that initializes the Response. WSGI APP will process the request and return an iterable object.
WSGI APP
By definition, we can implement a very simple App that satisfies WSGI.
1
2
3
4
5
|
def demo_wsgi_app(environ, start_response):
status = '200 OK'
headers = [('Content-type', 'text/plain')]
start_response(status, headers)
yield "Hello World!"
|
As you can see, the App initializes the request with start_response
and returns the body via yield. In addition to yield, it is possible to return an iterable object directly.
A simple WSGI APP is already included in the standard library wsgiref, which can be found in wsgiref.simple_server, and as you can see, this does the same thing.
1
2
3
4
5
6
7
8
9
10
|
def demo_app(environ,start_response):
from io import StringIO
stdout = StringIO()
print("Hello world!", file=stdout)
print(file=stdout)
h = sorted(environ.items())
for k,v in h:
print(k,'=',repr(v), file=stdout)
start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
return [stdout.getvalue().encode("utf-8")]
|
Run the app as follows.
In Django, you can find get_wsgi_application
in wsgi.py under the default app, and Django creates and returns a WSGIHandle
through this method, which is, in essence, still a WSGI APP, see its __call__
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_middleware()
def __call__(self, environ, start_response):
set_script_prefix(get_script_name(environ))
signals.request_started.send(sender=self.__class__, environ=environ)
request = self.request_class(environ)
response = self.get_response(request)
response._handler_class = self.__class__
status = '%d %s' % (response.status_code, response.reason_phrase)
response_headers = list(response.items())
for c in response.cookies.values():
response_headers.append(('Set-Cookie', c.output(header='')))
start_response(status, response_headers)
if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
response = environ['wsgi.file_wrapper'](response.file_to_stream)
return response
|
WSGI Server
You can basically guess what the WSGI server does from the way the WSGI APP is written, so you can try to implement a rudimentary WSGI server.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
def run_wsgi_app(app, environ):
from io import StringIO
body = StringIO()
def start_response(status, headers):
body.write('Status: {}\r\n'.format(status))
for header in headers:
body.write("{}: {}".format(*header))
return body.write
iterable = app(environ, start_response)
try:
if not body.getvalue():
raise RuntimeError("No exec start_response")
body.write("\r\n{}\r\n".format('\r\n'.join(line for line in iterable)))
finally:
if hasattr(iterable, "close") and callable(iterable.close):
iterable.close()
# θΏιηζ―
return body.getvalue()
|
For real (usable) WSGI servers, such as Gunicorn, there is a class method called handle_request
implemented in different workers (in the gunicorn.worker module), which is the method that calls the WSGI APP and completes the Response assembly. worker implementations differ slightly, but the code is relatively common.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
respiter = self.wsgi(environ, resp.start_response)
try:
if isinstance(respiter, environ['wsgi.file_wrapper']):
resp.write_file(respiter)
else:
for item in respiter:
resp.write(item)
resp.close()
request_time = datetime.now() - request_start
self.log.access(resp, req, environ, request_time)
finally:
if hasattr(respiter, "close"):
respiter.close()
|
This is the code that calls the WSGI APP and writes the Body to the resp through a loop.
Middleware
Because of the way WSGI is defined, multiple WSGI APPs can be written to nest and handle different logic, e.g.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
def first_wsgi_app(environ, start_response):
import logging
logging.info("new request")
rsp = second_wsgi_app(environ, start_response)
logging.info("finish request")
return rsp
def second_wsgi_app(environ, start_response):
if environ.get("HTTP_ROLE") == "ADMIN":
return third_wsgi_app(environ, start_response)
status = '200 OK'
headers = [('Content-type', 'text/plain')]
start_response(status, headers)
yield "Hello User!"
def third_wsgi_app(environ, start_response):
status = '200 OK'
headers = [('Content-type', 'text/plain')]
start_response(status, headers)
yield "Hello Admin!"
|
At this point we pass the first WSGI APP first_wsgi_app
to the Server. during execution, first_wsgi_app
does the logging, second_wsgi_app
does the authentication, and third_wsgi_app
does the actual processing of the request.
This onion structure of the App is affectionately known as Russian nesting doll by Maslen.