Django 深入理解WSGI协议
起步#
惭愧啊,惭愧啊,距离上一篇这个系列的文章已经是半年前的了,随着 Django2.0 的发布,感觉之前分析的 1.10.5
版本似乎有点老了,我看了一下,好在和我前面文章分析的内容差异不大,基本上也是可以就着前面的分析内容来品尝最新的 django 代码。
那我接下来阅读的版本就从当前能获取的 2.0.6
来分析了。不过呢,本章要将的内容,可能和 django 代码本身没太多关系。本章来理解一下 WSGI 协议,django 就是遵守这个协议的web开发框架,本章重点是协议方面的说明,顶多会讲讲django里相应的 wsgi 的代码,而不对 django 代码做分析。
什么是 WSGI#
WSGI (Web Server Gateway Interface)是用来指定 Web 服务器与 Python Web 应用程序或框架之间标准接口,以促进跨各种Web服务器的Web应用程序可移植性。
在这个规范出来之前,Python 拥有各种各样的 Web 应用程序框架,这也就产生了一个问题,开发者选择Web 框架会限制他们选择web 服务器,反之亦然。
因此,python就提出了一个简单而通用的 Web 服务器与 Web 应用程序之间的接口:Python Web服务器网关接口(WSGI)。
WSGI 的目标是促进现有服务器和应用程序的轻松互联,而不是创建新的Web框架。
调用方式#
WSGI 协议要面对两个端:一个是服务器或者说是网关端,另一个是应用程序或者说框架端。就需要处理一个问题,是谁调用了另一方。
在协议中规定了调用方式:服务器端调用应用程序端提供的 可调用
对象。
也就是说,web 应用程序需要提供一个可调用对象给web服务器调用,这个可调用的对象可以是 函数,方法,类或者带有 __call__
方法的实例。
可调用对象的构成#
这个可调用对象的构成也很简单,它接收 两个参数,该对象必须允许能够调用多次,如下面的示例:
def simple_app(environ, start_response):
"""最简单的应用程序对象"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['Hello world!\n']
这样就是一个满足 WSGI 协议的web程序应用了,是不是很简单。对应的django里,可以从 wsgi.py
中看到 application = get_wsgi_application()
这个函数展开基本和我们实例的最简单的应用程序对象结构一样了:
class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
def __call__(self, environ, start_response):
request = self.request_class(environ)
response = self.get_response(request)
status = '%d %s' % (response.status_code, response.reason_phrase)
response_headers = list(response.items())
start_response(status, response_headers)
return response
服务器端#
服务器的作用是接收每一个 HTTP 请求,应用程序对象调用时需要传入 environ
和 start_response
,因此这两个参数需要由服务器端来整理并提供给应用程序使用。
environ
是一个字典,以一个简单的 CGI 网关为例,它的值可以这么设置:
import os
environ = dict(os.environ.items())
environ['wsgi.input'] = sys.stdin
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'
start_response
则是一个函数,原型是 start_response(status, response_headers, exc_info=None)
并且这个函数要返回一个可调用的 write(body_data)
对象。例如:
def unicode_to_wsgi(u):
# Convert an environment variable to a WSGI "bytes-as-unicode" string
return u.encode(enc, esc).decode('iso-8859-1')
def wsgi_to_bytes(s):
return s.encode('iso-8859-1')
headers_set = [] # 待发送的响应的header信息
headers_sent = [] # 已发送的响应的header信息
def write(data):
out = sys.stdout.buffer
if not headers_set:
raise AssertionError("write() before start_response()")
elif not headers_sent:
# Before the first output, send the stored headers
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:
# Re-raise original exception if headers sent
raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None # avoid dangling circular ref
elif headers_set:
raise AssertionError("Headers already set!")
headers_set[:] = [status, response_headers]
return write
这样其实一个满足 WSGI 协议的 web服务器端 就基本完成了,现在需要整合一下,由于需要涉及到请求包的分析过程,我们就直接用标准库 wsgiref.simple_server
中的 WSGIServer
作为web服务器。
整合一下:
import sys
import os
from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
def demo_app(environ,start_response):
"""
示例的 app
"""
stdout = "Hello world!"
h = sorted(environ.items())
for k,v in h:
stdout += k + '=' + repr(v) + "\r\n"
print(start_response)
start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
return [stdout.encode("utf-8")]
enc, esc = sys.getfilesystemencoding(), 'surrogateescape'
def unicode_to_wsgi(u):
# Convert an environment variable to a WSGI "bytes-as-unicode" string
return u.encode(enc, esc).decode('iso-8859-1')
def wsgi_to_bytes(s):
return s.encode('iso-8859-1')
def run_with_cgi(request, client_address, server):
environ = {k: unicode_to_wsgi(v) for k, v in os.environ.items()}
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:
# Before the first output, send the stored headers
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:
# Re-raise original exception if headers sent
raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None # avoid dangling circular ref
elif headers_set:
raise AssertionError("Headers already set!")
headers_set[:] = [status, response_headers]
return write
application = server.get_app()
result = application(environ, start_response)
try:
for data in result:
if data: # don't send headers until body appears
write(data)
if not headers_sent:
write('') # send headers now if body was empty
finally:
if hasattr(result, 'close'):
result.close()
server = WSGIServer(('', 8000), run_with_cgi)
server.set_app(demo_app)
server.serve_forever()
运行这个程序,然后用浏览器访问本地 8000 端口,就能看到终端输出了 environ
。
中间件#
一个中间件扮演了与某些 application
相关的角色,同时,中间件也可以是某些服务器的应用程序。
中间件拥有如下功能:
- 适当修改
environ
后,根据目标 URL 将请求分配到不同的应用程序对象; - 允许多个
application
在同一个进程中并行; - 通过网络转发请求和响应来负载平衡和远程处理;
- 执行内容后处理,例如应用XSL样式表。
一般来说,中间件对于 "server/gateway" 和 "application/framework" 都是透明的,并且不需要特别的支持。如果用户将中间件集成到 application
中,那中间件提供给服务器调用,此时中间件就像 application
一样了;反过来,如果配置的中间件是调用 application
的调用方,那它就像服务器一样了。
因此,中间件包装的 "应用程序" 实际上也可能是另一个包装着应用程序的中间件。
大多数情况下,中间件必须符合 WSGI
服务器和应用程序端的限制和要求,django 中的中间件都是符合这些要求的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架