【Python Web 系列】WSGI

前言

对一个 Python Web 开发者来说,PEP333/PEP3333,是必须熟悉的。都什么年代了,我们自然直接看 3333。

PEP 3333

PEP3333,描述了 Web 服务器与 Python Web 应用程序或框架之间的建议标准接口,
以促进跨各种 Web 服务器的应用程序的可移植性,形成类似于Java 的 “servlet” API。

接口说明

WSGI 接口有两个方面:服务器/网关侧,以及应用程序/框架侧。服务器调用应用程序提供的可调用对象。
提供该对象的具体方式取决于服务器或网关,可以通过脚本或配置文件的形式指定。
可调用对象,是指一个函数/方法/具有__call__方法的实例

为了适配 Py3 中的字符串 Unicode,WSGI 定义了两种字符串:

  • Native字符串(str),用于请求/响应标头和元数据
  • Bytestrings(bytes),用于请求响应体

这就要注意了,在 Py3 中,str对应UnicodeTypebytestring对应BytesType。而上述Native字符串,encode(‘latin-1’) 后得到 Bytestrings。
意味着,str 虽然是 Unicode,但也只是支持ISO-8859-1编码部分(\u0000~\u00FF)。

应用程序侧

应用程序/框架,提供给 Server 的可调用对象,接受两个位置参数environ/start_response

  • 位置 1:environ,一个字典,封装了请求的相关信息 + CGI 风格的环境变量
  • 位置 2:start_response,一个可调用对象,传入status, response_header[, exc_info]三个位置参数
    • status 是形如 200 OK 的状态字符串
    • headers 是一个元组列表,形如 [(header_name, header_value)],注意大小写不敏感
    • 可选参数 exc_info,在传递错误信息时使用
    • 客户端侧,不使用start_response()的返回值
  • app(environ,start_response)返回一个能迭代出 bytestrings 的可迭代对象
  • 应用程序必须保证,先调用 start_response,才迭代出 bytestrings

服务器侧

在服务器侧最终的结果,是直接通过app(environ,start_response)得到

  • start_response 必须返回一个可调用对象,接收一个bytestring 对象作为参数
  • 仅对响应内容进行编码传输,不更改内容,且不返回迭代器的任何属性
  • 如果能够调用close(iterable),服务器必须请求结束后调用该方法
  • WSGI服务器、网关和中间件不能拖延任何块的传输,要么立即发送块,要么保证应用程序侧连接不断开

中间件

只要符合 WSGI 协议,就可以接入到整个应用链中。因此,诞生了中间件Middleware。其可以:

  • 从 server 端接收,并改写信息,定位到不同的应用程序
  • 从 app 端接收,改写信息,发送不同的格式内容

环境变量

environ 字典中,不仅含有 CGI 中的部分参数,还包含 WSGI 定义的变量。

CGI 中的变量,注意也必须是 string 类型:

  • REQUEST_METHOD: 请求方法,是个字符串,’GET’, ‘POST’等
  • SCRIPT_NAME: HTTP 请求的 path 中的用于查找到 app 对象的部分,比如 Web 服务器可以根据 path 的一部分来决定请求由哪个virtual host处理
  • PATH_INFO: HTTP 请求的 path 中剩余的部分,也就是 application 要处理的部分
  • QUERY_STRING: HTTP 请求中的查询字符串,URL中?后面的内容
  • CONTENT_TYPE: HTTP headers中的content-type内容
  • CONTENT_LENGTH: HTTP headers中的content-length内容
  • SERVER_NAME和SERVER_PORT: 服务器名和端口,这两个值和前面的SCRIPT_NAME, PATH_INFO拼起来可以得到完整的URL路径
  • SERVER_PROTOCOL: HTTP 协议版本,HTTP/1.0或者HTTP/1.1
  • HTTP_*: 和请求中的headers对应
    示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    "SERVER_NAME": "DESKTOP",
    "SERVER_PORT": "8080",
    "CONTENT_LENGTH": "",
    "SCRIPT_NAME": "",
    "SERVER_PROTOCOL": "HTTP/1.1",
    "SERVER_SOFTWARE": "WSGIServer/0.2",
    "REQUEST_METHOD": "GET",
    "QUERY_STRING": "",
    "CONTENT_TYPE": "text/plain",

    "HTTP_HOST": "127.0.0.1:8080",
    "HTTP_CONNECTION": "keep-alive",
    "HTTP_CACHE_CONTROL": "max-age=0",
    "HTTP_USER_AGENT": "Mozilla/5.0 ,
    "HTTP_UPGRADE_INSECURE_REQUESTS": "1",
    "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.8",
    "HTTP_DNT": "1",
    "HTTP_ACCEPT_ENCODING": "gzip, deflate, br",
    "HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.9,ca;q=0.8",
    "HTTP_COOKIE":"csrftoken=w6V4gyp0o9doePer5oDrlDCwtxeaWxq",

WSGI 变量:

  • wsgi.version:表示WSGI版本,一个元组(1, 0),表示版本1.0
  • wsgi.url_scheme:http或者https
  • wsgi.input:一个类文件的输入流,application可以通过这个获取HTTP request body
  • wsgi.errors:一个输出流,当应用程序出错时,可以将错误信息写入这里
  • wsgi.multithread:当application对象可能被多个线程同时调用时,这个值需要为True
  • wsgi.multiprocess:当application对象可能被多个进程同时调用时,这个值需要为True
  • wsgi.run_once:当server期望application对象在进程的生命周期内只被调用一次时,该值为True
    示例:
    1
    2
    3
    4
    5
    6
    7
    8
    "wsgi.input": "<_io.BufferedReader name=448>",
    "wsgi.errors": "<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>",
    "wsgi.version": "(1, 0)",
    "wsgi.run_once": "False",
    "wsgi.url_scheme": "http",
    "wsgi.multithread": "True",
    "wsgi.multiprocess": "False",
    "wsgi.file_wrapper": "<class 'wsgiref.util.FileWrapper'>"

Demo

SimpleApp

1
2
3
4
5
def simple_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/html; charset=utf-8')]
start_response(status, response_headers)
return [b"<h1>Hello,world! I'm Lx.</h1>"]

SimpleServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import socket
from io import StringIO
import sys
import datetime
import os

class SimpleServer(object):
def __init__(self, address, application):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(address)
self.socket.listen(1)
self.host = address[0]
self.port = address[1]
self.application = application
self.headers_set = []

def serve_forever(self):
while True:
self.connection, client_address = self.socket.accept()
self.handle_request()

def handle_request(self):
request_data = self.connection.recv(1024).decode()
self.get_url_parameter(request_data)
result = self.application(self.get_environ(), self.start_response)
self.finish_response(result)
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}]",
f"{self.request_headline} {self.headers_set[0]}")

def get_url_parameter(self, reqest_data):
request_lines = reqest_data.splitlines()
self.request_headline = request_lines[0]
self.request_dict = {'Path': request_lines[0]}
for itm in request_lines[1:]:
if ':' in itm:
self.request_dict[itm.split(':')[0]] = itm.split(':')[1]
self.request_method, self.path, self.request_version = \
self.request_dict.get('Path').split()

def get_environ(self):
env = {
'REQUEST_METHOD': self.request_method,
'PATH_INFO': self.path,
'SERVER_NAME': self.host,
'SERVER_PORT': str(self.port),
'USER_AGENT': self.request_dict.get('User-Agent')
}
environ = dict(os.environ.items())
environ['wsgi.version'] = "(1, 0)",
environ['wsgi.input'] = StringIO(),
environ['wsgi.errors'] = sys.stderr
environ['wsgi.version'] = (1, 0)
environ['wsgi.multithread'] = False
environ['wsgi.multiprocess'] = True
environ['wsgi.run_once'] = True
if environ.get('HTTPS', 'off') in ('on', '1'):
environ['wsgi.url_scheme'] = 'https'
else:
environ['wsgi.url_scheme'] = 'http'
environ.update(env)
return environ

def start_response(self, status, response_headers, exc_info=None):
headers = [
('Date', time.strftime('%a, %d %b %Y %H:%M:%S GMT')),
('Server', 'SimpleServer'),
]
self.headers_set[:] = [status, response_headers + headers]

def finish_response(self, app_data):
try:
response = 'HTTP/1.1 {}\r\n'.format(self.headers_set[0])
for header in self.headers_set[1]:
response += '{0}: {1}\r\n'.format(*header)
response += '\r\n'
response = response.encode()
if isinstance(app_data, bytes):
response += app_data
else:
for info in app_data:
response += info
self.connection.sendall(response)
finally:
self.connection.close()

SimpleServerApp

1
2
3
4
def SimpleServerApp():
httpd = SimpleServer(('0.0.0.0', 8080), simple_app)
print('WSGI Server Start Serving...http://127.0.0.1:8080')
httpd.serve_forever()

Middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import os
from werkzeug.wrappers import Request, Response
from werkzeug.wsgi import SharedDataMiddleware

class Shortly:
def dispatch_request(self, request):
response = Response()
response.headers['Content-Type'] = 'text/html'
if request.path == '/':
response.data = '<h1>Hello World!</h1>'
else:
response.data = "Path>>>%s" % request.path
return response

def wsgi_app(self, environ, start_response):
request = Request(environ)
response = self.dispatch_request(request)
return response(environ, start_response)

def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)

def create_app(with_static=True):
app = Shortly()
if with_static:
app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
'/static': os.path.join(os.path.dirname(__file__), 'static')
})
return app