An in depth understanding of Python WSGI

  • 2020-12-05 17:18:13
  • OfStack

preface

This paper mainly introduces Python WSGI related contents, mainly from the following websites:

What is WSGI? WSGI Tutorial An Introduction to the Python Web Server Gateway Interface (WSGI)

You can view it as a simple and crude translation.

What is a WSGI

The full name of WSGI is Web Server Gateway Interface, which is a specification that describes how web server interacts with web application and how web application handles requests. The specification is detailed in PEP 3333. Note that WSGI implements both web server and web application.

The modules/libraries of WSGI are wsgiref(built-in python), ES37en. serving, twisted. web and so on.

Currently, web framework running on WSGI includes Bottle, Flask, Django, etc., specifically, Frameworks that run on WSGI.

All WSGI server does is pass the request received from the client to WSGI application and then pass the return value of WSGI application to the client as a response. WSGI applications can be stack style, the middle part of the stack is called middleware, the two ends of the stack are required to implement application and server.

WSGI tutorial

This part is mainly from WSGI Tutorial.

WSGI application interface

The WSGI application interface should be implemented as 1 callable object, such as a function, method, class, and an instance of the method with ___ 84en__. The callable object can take two arguments:

1 dictionary, which can contain information requested by the client as well as other information, which can be considered the request context, is commonly called environment (often abbreviated to environ and env in encoding); 1 callback function for sending HTTP response status (HTTP status), response header (HTTP headers).

Also, the return value of the callable object is the response body (response body), which is iteratable and contains multiple strings.

The structure of WSGI application is as follows:


def application (environ, start_response):

 response_body = 'Request method: %s' % environ['REQUEST_METHOD']

 # HTTP The response status 
 status = '200 OK'

 # HTTP Response headers, pay attention to formatting 
 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(len(response_body)))
 ]

 #  Hand over the response status and response headers WSGI server
 start_response(status, response_headers)

 #  Return response body 
 return [response_body]

Environment

The following program can return the contents of the environment dictionary to the client (ES108en.py) :


# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

#  The import python The built-in WSGI server
from wsgiref.simple_server import make_server

def application (environ, start_response):

 response_body = [
  '%s: %s' % (key, value) for key, value in sorted(environ.items())
 ]
 response_body = '\n'.join(response_body) #  Since the following will Content-Type Set to text/plain , so `\n` It acts as a line break in the browser 

 status = '200 OK'
 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(len(response_body)))
 ]
 start_response(status, response_headers)

 return [response_body]

#  instantiation WSGI server
httpd = make_server (
 '127.0.0.1', 
 8051, # port
 application # WSGI application , here is 1 A function 
)

# handle_request Functions can only handle 1 The second request, after the console `print 'end'` the 
httpd.handle_request()

print 'end'

The browser (or curl, wget, etc.) access http: / / 127.0.0.1:8051 /, can see environment content.

In addition, after one browser request, ES119en. py is over, and the program outputs the following contents in the terminal:

[

127.0.0.1 - - [09/Sep/2015 23:39:09] "GET / HTTP/1.1" 200 5540
end

]

Iterable responses

If you put the return value of the callable object application above:


return [response_body]

To:


return response_body

This causes the WSGI program to be slower to respond. The reason is that the string response_body is also iteratable, and it only gets 1 byte data per iteration, which means that only 1 byte data is sent to the client every iteration until the sending is complete. Therefore, return [response_body] is recommended.

If the iterable response contains multiple strings, ES150en-Length should be the sum of these string lengths:


# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server

def application(environ, start_response):

 response_body = [
  '%s: %s' % (key, value) for key, value in sorted(environ.items())
 ]
 response_body = '\n'.join(response_body)

 response_body = [
  'The Beggining\n',
  '*' * 30 + '\n',
  response_body,
  '\n' + '*' * 30 ,
  '\nThe End'
 ]

 #  o Content-Length
 content_length = sum([len(s) for s in response_body])

 status = '200 OK'
 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(content_length))
 ]

 start_response(status, response_headers)
 return response_body

httpd = make_server('localhost', 8051, application)
httpd.handle_request()

print 'end'

Resolve the GET request

Run ES158en.py and access http://localhost:8051/? in a browser. age=10 & hobbies=software & hobbies=tunning, can be found in the content of the response:


QUERY_STRING: age=10&hobbies=software&hobbies=tunning
REQUEST_METHOD: GET

The cgi.es172EN_ES173en () function can easily handle QUERY_STRING and requires cgi.escape () to handle special characters to prevent script injection. Here is an example:


# ! /usr/bin/env python
# -*- coding: utf-8 -*- 
from cgi import parse_qs, escape

QUERY_STRING = 'age=10&hobbies=software&hobbies=tunning'
d = parse_qs(QUERY_STRING)
print d.get('age', [''])[0] # [''] Is the default value if at QUERY_STRING Didn't find age Returns the default value 
print d.get('hobbies', [])
print d.get('name', ['unknown'])

print 10 * '*'
print escape('<script>alert(123);</script>')

The output is as follows:

[

10
['software', 'tunning']
['unknown']
**********
& lt;script & gt;alert(123); & lt;/script & gt;

]

We can then write a basic dynamic web page that handles GET requests:


# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server
from cgi import parse_qs, escape

# html In the form the method is get . action Is the current page 
html = """
<html>
<body>
 <form method="get" action="">
  <p>
   Age: <input type="text" name="age" value="%(age)s">
  </p>
  <p>
   Hobbies:
   <input
    name="hobbies" type="checkbox" value="software"
    %(checked-software)s
   > Software
   <input
    name="hobbies" type="checkbox" value="tunning"
    %(checked-tunning)s
   > Auto Tunning
  </p>
  <p>
   <input type="submit" value="Submit">
  </p>
 </form>
 <p>
  Age: %(age)s<br>
  Hobbies: %(hobbies)s
 </p>
</body>
</html>
"""

def application (environ, start_response):

 #  parsing QUERY_STRING
 d = parse_qs(environ['QUERY_STRING'])

 age = d.get('age', [''])[0] #  return age The value of the corresponding 
 hobbies = d.get('hobbies', []) #  In order to list Form returns all hobbies

 #  Prevent script injection 
 age = escape(age)
 hobbies = [escape(hobby) for hobby in hobbies]

 response_body = html % { 
  'checked-software': ('', 'checked')['software' in hobbies],
  'checked-tunning': ('', 'checked')['tunning' in hobbies],
  'age': age or 'Empty',
  'hobbies': ', '.join(hobbies or ['No Hobbies?'])
 }

 status = '200 OK'

 #  This time the content type is text/html
 response_headers = [
  ('Content-Type', 'text/html'),
  ('Content-Length', str(len(response_body)))
 ]

 start_response(status, response_headers)
 return [response_body]

httpd = make_server('localhost', 8051, application)

#  To be able to 1 Direct processing request 
httpd.serve_forever()

print 'end'

Start the program and access http://localhost:8051/, http://localhost:8051/? in the browser; age=10 & hobbies=software & hobbies=tunning Feel 1 ~

This program will run 1 straight, you can use the shortcut Ctrl-C to terminate it.

This code involves two tricks that I personally haven't used before:


>>> "Age: %(age)s" % {'age':12}
'Age: 12'
>>> 
>>> hobbies = ['software']
>>> ('', 'checked')['software' in hobbies]
'checked'
>>> ('', 'checked')['tunning' in hobbies]
''

Resolve the POST request

For POST requests, the query string (query string) is placed in the body of the HTTP request (request body), not in URL. The request body is in the value corresponding to the key wsgi.input in the environment dictionary variable, which is a variable similar to file, and the value is 1. The PEP 3333 notes that the CONTENT_LENGTH field in the request header indicates the size of the body, but may be empty or non-existent, so use try/except to read the body of the request.

Here is a dynamic site that can handle POST requests:


# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server
from cgi import parse_qs, escape

# html In the form the method is post
html = """
<html>
<body>
 <form method="post" action="">
  <p>
   Age: <input type="text" name="age" value="%(age)s">
  </p>
  <p>
   Hobbies:
   <input
    name="hobbies" type="checkbox" value="software"
    %(checked-software)s
   > Software
   <input
    name="hobbies" type="checkbox" value="tunning"
    %(checked-tunning)s
   > Auto Tunning
  </p>
  <p>
   <input type="submit" value="Submit">
  </p>
 </form>
 <p>
  Age: %(age)s<br>
  Hobbies: %(hobbies)s
 </p>
</body>
</html>
"""

def application(environ, start_response):

 # CONTENT_LENGTH  It might be empty, or it might not be 
 try:
  request_body_size = int(environ.get('CONTENT_LENGTH', 0))
 except (ValueError):
  request_body_size = 0

 request_body = environ['wsgi.input'].read(request_body_size)
 d = parse_qs(request_body)

 #  To get the data 
 age = d.get('age', [''])[0] 
 hobbies = d.get('hobbies', []) 

 #  Escape to prevent script injection 
 age = escape(age)
 hobbies = [escape(hobby) for hobby in hobbies]

 response_body = html % { 
  'checked-software': ('', 'checked')['software' in hobbies],
  'checked-tunning': ('', 'checked')['tunning' in hobbies],
  'age': age or 'Empty',
  'hobbies': ', '.join(hobbies or ['No Hobbies?'])
 }

 status = '200 OK'

 response_headers = [
  ('Content-Type', 'text/html'),
  ('Content-Length', str(len(response_body)))
 ]

 start_response(status, response_headers)
 return [response_body]

httpd = make_server('localhost', 8051, application)

httpd.serve_forever()

print 'end' 

Introduction to Python WSGI

An Introduction to Python Web Server Gateway Interface (WSGI).

Web server

WSGI server is an web server. The logic for processing an HTTP request is as follows:


# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

#  The import python The built-in WSGI server
from wsgiref.simple_server import make_server

def application (environ, start_response):

 response_body = [
  '%s: %s' % (key, value) for key, value in sorted(environ.items())
 ]
 response_body = '\n'.join(response_body) #  Since the following will Content-Type Set to text/plain , so `\n` It acts as a line break in the browser 

 status = '200 OK'
 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(len(response_body)))
 ]
 start_response(status, response_headers)

 return [response_body]

#  instantiation WSGI server
httpd = make_server (
 '127.0.0.1', 
 8051, # port
 application # WSGI application , here is 1 A function 
)

# handle_request Functions can only handle 1 The second request, after the console `print 'end'` the 
httpd.handle_request()

print 'end'
0

app is WSGI application and environ is environment above. The callable object app returns 1 iterable value, which is obtained by WSGI server and sent to the client.

Web framework/app

Namely WSGI application.

Middleware (Middleware)

Middleware is located between WSGI server and WSGI application, so

A sample

Middleware is used in this example.


# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

#  The import python The built-in WSGI server
from wsgiref.simple_server import make_server

def application (environ, start_response):

 response_body = [
  '%s: %s' % (key, value) for key, value in sorted(environ.items())
 ]
 response_body = '\n'.join(response_body) #  Since the following will Content-Type Set to text/plain , so `\n` It acts as a line break in the browser 

 status = '200 OK'
 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(len(response_body)))
 ]
 start_response(status, response_headers)

 return [response_body]

#  instantiation WSGI server
httpd = make_server (
 '127.0.0.1', 
 8051, # port
 application # WSGI application , here is 1 A function 
)

# handle_request Functions can only handle 1 The second request, after the console `print 'end'` the 
httpd.handle_request()

print 'end'
1

then

With this basic knowledge, you can build an web framework. If you are interested, you can read 1 Bottle, Flask and other source code.

There is more on WSGI Learn about WSGI.

conclusion


Related articles: