Django in Action: WSGI

Django in Action: WSGI

WSGI: Web Server Gateway Interface, Web 服务器网关接口。它是 Python 中定义的一个网关协议

先看看一个简单的 Web Server:

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
import socket

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
body = '''Hello, World! <h1> from QCF </h1>'''
response_params = {
"HTTP/1.1 200 OK",
"Date: Sat, 06 March 2021 22:01:00 GMT",
"Content-Type: text/html; charset=utf-8",
"Content-Length: {}\r\n".format(len(body.encode())),
body,
}
response = '\r\n'.join(response_params)

def handle_connection(conn, addr):
request = b""
while EOL1 not in request and EOL2 not in request:
request += conn.recv(1024)
print(request)
conn.send(response.encode())
conn.close()

def main():
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口立即释放
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(("127.0.0.1", 10001))
serversocket.listen(5)
print('http://127.0.0.1:10001')

try:
while True:
conn, addr = serversocket.accept()
print(addr)
handle_connection(conn, addr)
finally:
serversocket.close()

if __name__ == "__main__":
main()

测试时使用 Firfox 浏览器。

程序思路很简单,就是监听特定端口,接收客户端请求,构造 http response 后返回:

Content-Type: text/html

Content-Type: text/plain

Web Server 的模型类似:

sequenceDiagram Browser->>Web Server: Http Request Web Server->>Web Server: Generate a HTML page Web Server->>Browser: Http Response with HTML page as body Browser->>Browser: Get the HTML page and display

从代码中可以发现,接收 http request,生成 http response 是个苦力活,而生成 html 页面更加容易,因此就把这些底层内容教给服务器程序去干,并通过 WSGI 协议向我们提供服务。

WSGI 就是 Python 定义的一个协议,只要我们按照这个协议编写 Web App,就能通过 WSGI 处理 HTTP 请求和响应。

WSGI 协议包括两部分:Web Application 和 Gateway。Gateway (Web Server) 接收请求后,将数据以规定格式传递给 Web Application,Web Application 处理完后,设置对应的 Status 和 Header,并将数据返回给 Gateway,而 Gateway 将返回的数据进行封装,最终返回一个完整的 Http Response。

以下是一个简单的符合 WSGI 协议的 Web Application:

1
2
3
4
5
def simple_app(environ, start_response):
status = "200 OK"
response_headers = [("Content-Type", "text/plain")]
start_response(status, response_headers)
return [b"Hello World! -by QCF \n"]

它接收两个参数:

  • environ:系统环境信息,包含所有HTTP请求信息的 dict 对象;
  • start_response:发送 HTTP 响应的函数。

Web Server 接收到请求后,会将请求信息写入系统环境变量,供 application 调用。

environ 和 start_response 是我们无法提供的,这个函数得交给 WSGI 服务器调用。而我们所使用的 Web 框架是对 WSGI 的进一步封装,让我们只专注于业务逻辑,进一步简化 Web 开发。

以下通过 wsgiref 启动一个简单的 wsgi 服务器:

1
2
3
4
5
6
from wsgiref.simple_server import make_server
from app import simple_app

httpd = make_server('127.0.0.1', 10001, simple_app)
print('Listening on 127.0.0.1:10001...')
httpd.serve_forever()

访问 localhost:10001

以上代码并没有揭示应用运行的内在逻辑,但我们可以通过以下代码一窥究竟:

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
import os
import sys

from app import simple_app

def wsgi_to_bytes(s):
return s.encode()

def run_with_cgi(application):
# 获取系统环境信息
environ = dict(os.environ.items())
# 将信息写入 environ
environ["wsgi.input"] = sys.stdin.buffer
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"

headers_set = []
headers_sent = []

def write(data):
out = sys.stdout.buffer

if not headers_set:
raise AssertionError("write() before start_response()")

elif not headers_sent:
# 输出第一行数据前,先发送响应头
status, response_headers = headers_sent[:] = headers_set
out.write(wsgi_to_bytes("Status: %s\r\n" % status))
for header in response_headers:
out.write(wsgi_to_bytes("%s: %s\r\n" % header))
out.write(wsgi_to_bytes("\r\n"))
out.write(data)
out.flush()

def start_response(status, response_headers, exc_info = None):
if exc_info:
try:
# 若已经发送了响应头,则重新抛出异常信息
if headers_sent:
raise (exc_info[0], exc_info[1], exc_info[2])
finally:
exc_info = None
elif headers_set:
raise AssertionError("Headers already set!")

headers_set[:] = [status, response_headers]
return write

result = application(environ, start_response)

try:
for data in result:
# 如果没有 body 数据,则不发送响应头
if data:
write(data)
# 若 body 为空,则发送响应头
if not headers_sent:
write('')
finally:
if hasattr(result, "close"):
result.close()

if __name__ == "__main__":
run_with_cgi(simple_app)

代码的具体细节可以以后再研究,其主要运行过程可以用以下代码归纳(出自标准模块 wsgiref.handlers):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BaseHandler:
"""Manage the invocation of a WSGI application"""

...

def run(self, application):
"""Invoke the application"""
# Note to self: don't move the close()! Asynchronous servers shouldn't
# call close() from finish_response(), so if you close() anywhere but
# the double-error branch here, you'll break asynchronous servers by
# prematurely closing. Async servers must return from 'run()' without
# closing if there might still be output to iterate over.
try:
self.setup_environ()
self.result = application(self.environ, self.start_response)
self.finish_response()
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
...
...

代码文件:

后记:写的还是很乱,没突出重点。