WSGI是一个约定,一份协议,并不是什么高深的技术,而且它的约定十分简单,使得服务器程序和应用程序可以非常轻便的结合在一起,任何服务器,任何web应用程序,只要他们遵守了这份约定,就可以一起在服务器上部署,提供服务
python后端开发,服务端程序分为两部分,一个是服务器程序,一个是应用程序,我们用flask,django,tornado框架写出来的都是应用程序,这些应用程序必须通过服务器部署才能被访问使用。
这里说的服务器程序,可不是你理解的nginx,tomcat,他们是无法直接部署我们写的python web 应用程序的。
服务器负责收集用户的请求,初步处理后传递给我们的应用程序,应用程序负责具体的业务逻辑处理,为了开发的便利性,人们把常用的功能封装在一起,这就形成了各种web 框架,例如flask, django。
还有那么一批人,出于对技术的热爱,实现了不同的服务器,比如tornado(既是服务器也是web框架),wsgiref.simple_server(官方实现),gevent中的pywsgi.WSGIServer,gunicorn中的Application等等,他们的底层实现各不相同,为了追求性能,有的服务器用epoll网络模型,有的用协程,还有的用多进程,总之百花齐放。
那么多的web框架,那么多的服务器,他们怎么配合工作呢?这就是WSGI的由来。
The Web Server Gateway Interface, 即 WSGI,WSGI是一个协议,是一份约定,它规定服务器和应用程序之间如何传递数据,各自应该实现什么样的接口,以便彼此间配合工作。
WSGI的约定不能太复杂了,因为这是服务器和应用程序要共同遵守的约定,弄的太复杂了,两边的人开发起来都困难,所以,对于应用程序段,它只规定了3个简单的要求
下面,对这3点逐一进行解释
函数,类,实现了__call__方法的类的对象,都是可调用对象,俗称可callable对象。
def test():
pass
class Test:
pass
print(callable(test), callable(Test))
print 输出两个True,所谓可调用对象,就是对象后面可以加小括号的对象,这是最简单粗暴的理解了。使用flask框架时,我们通常这样写
app = Flask(__name__)
app就是一个可调用对象,Flask类实现了__call__方法,当我们执行下面的代码
app.run(debug=True)
其实就是在执行Flask类里的__call__方法
之所以强调,应用程序是一个可调用对象,是因为服务器在部署我们写的web程序时,本质上就是在调用这个对象,因此它必须是可调用的,只有可调用,服务器才能把environ和start_response传给我们的应用程序,那么这两个参数都用什么用呢?
environ可不简单,它是一个字典,包含了一次请求的几乎所有信息,下面是一个内容示例
{
'SERVER_PROTOCOL': 'HTTP/1.1',
'SERVER_SOFTWARE': 'WSGIServer/0.2',
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/book/python',
'QUERY_STRING': 'name=flask',
'REMOTE_ADDR': '127.0.0.1',
'CONTENT_TYPE': 'text/plain',
'HTTP_HOST': 'localhost: 8800',
'HTTP_CONNECTION': 'keep-alive',
'HTTP_CACHE_CONTROL': 'max-age=0',
'HTTP_UPGRADE_INSECURE_REQUESTS': '1',
'HTTP_USER_AGENT': 'Mozilla/5.0(Macintosh;IntelMacOSX10_14_2)AppleWebKit/537.36(KHTML,
likeGecko)Chrome/75.0.3770.142Safari/537.36',
'HTTP_ACCEPT': 'text/html,
application/xhtml+xml,
application/xml;q=0.9,
image/webp,
image/apng,
*/*;q=0.8,
application/signed-exchange;v=b3',
'HTTP_ACCEPT_ENCODING': 'gzip,
deflate,
br',
'HTTP_ACCEPT_LANGUAGE': 'zh-CN,
zh;q=0.9',
'HTTP_COOKIE': '_ga=GA1.1.1246835462.1564651565;token=uQITFBiKxFOfLWDHZxHQbdIDJVJLGRdR;email=admin;session_uuid=2c563845-4f93-4832-9e3d-4cd14c179fd2;wordpress_test_cookie=WP+Cookie+check;wordpress_logged_in_70490311fe7c84acda8886406a6d884b=kwsy%7C1568971347%7C8F3nK8fRBa6x2ePaFjvSYrmB3QMGm3I32Gyh3KGJp2t%7C322f03c8e5e825a25db050d60ae1e6430ee5417b95530fb3a10afdf658cbcd46;wp-settings-1=mfold%3Do;wp-settings-time-1=1567761754'
实际的内容比这要多出很多,为了突出重点,减少文章篇幅,我删除了一部分,剩下的这些,如果你对http协议有所了解的,很容易就能看出environ的作用。为了得到这份environ,我写了一个简单的web 程序
from wsgiref.simple_server import make_server
def handle_request(env, start_response):
print(env)
start_response("200 OK",[("Content-Type","text/html")])
body = "<h1>Hello World!</h1>"
return [body.encode("utf-8")]
if __name__ == "__main__":
httpd = make_server("",8800,handle_request)
print("Serving http on port 8800")
httpd.serve_forever()
启动程序后,在浏览器里输入http://localhost:8800/book/python?name=flask, 即可得到上述内容。现在,我们的应用程序已经过了本次请求的全部信息,包括本次请求的资源地址(PATH_INFO),请求的方法(REQUEST_METHOD),请求的参数(QUERY_STRING),客户端的IP地址(REMOTE_ADDR),请求的cookie(HTTP_COOKIE)。
start_response是一个方法,以上面的代码为例,其方法定义和实现为
def start_response(self, status, headers,exc_info=None):
"""'start_response()' callable as specified by PEP 3333"""
if exc_info:
try:
if self.headers_sent:
# Re-raise original exception if headers sent
raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
finally:
exc_info = None # avoid dangling circular ref
elif self.headers is not None:
raise AssertionError("Headers already set!")
self.status = status
self.headers = self.headers_class(headers)
它最重要的功能是设置response的http code和headers。熟悉http协议的人都知道,服务器处理完请求后,要将数据发送给客户端,这个就教做response,根据http协议,返回的response要标识http状态码,还要设置headers,你接触过的任何一个框架都会这样做处理的,只是这些框架封装的很好,不需要你自己去做处理而已。
要明确一点,environ和start_response是服务器传给我们的,前者是一个字典,存储了请求的信息,后者是一个方法,用于我们在应用程序里设置响应头,那么如何接收这两个参数呢,前面已经说过,应用程序是一个可调用对象,它可以是函数,也可以是类,也可以是一个类的对象,那么方法就有三种
先说第一种,我们的应用程序提供的可调用对象是一个函数,定义如下
def application(environ, start_response):
pass
application(environ, start_response) # 调用
第二种,应用程序提供的可调用对象是一个类,定义如下
class Application:
def __init__(self, environ, start_response):
pass
Application(environ, start_response) # 调用
第三种,应用程序提供的可调用对象是类的对象,该类实现了__call__方法
class Application:
def __call__(self, environ, start_response):
pass
app = Application()
app(environ, start_response) # 调用
为啥非得返回一个可迭代对象呢?在上面的例子中,我返回的是一个列表,列表是可迭代对象,为啥不可以直接返回一个字符串呢?
这样做,是为了更好的提供web服务,比如服务端要返回给客户端一个2个G的文件数据,如果应用程序是一次性加载到内存中进行返回,那么就会占用非常多的内存,要知道,这可是web服务啊,吃不消的。如果可调用对象返回的是可迭代对象,情形就不一样了,同样是返回这个文件,我们可以使用生成器,一次只生成一小块固定大小的文件数据,这样占用内存就很小,我们可能会写一个生成器,类似下面的代码
def send_file(file_path, BLOCK_SIZE=1000):
with open(file_path) as f:
block = f.read(BLOCK_SIZE)
while block:
yield block
block = f.read(BLOCK_SIZE)
我们的可调用对象,则可以这样来处理
def application(environ, start_response):
size = os.path.getsize('big_file')
headers = [
("Content-length", str(size)),
]
start_response("200 OK", headers)
return send_file('big_file')
在服务器端,则是这样来处理我们返回的可迭代数据的
def finish_response(self):
try:
if not self.result_is_file() or not self.sendfile():
for data in self.result:
self.write(data)
self.finish_content()
finally:
self.close()
遍历self.result,这就是我们返回的结果。
对服务器的约定,就更为简单了,它只要求服务器提供一个start_response方法,当请求到来时,将environ整理好一并传给应用程序,还是以官方实现的wsgiref.simple_server为例,其调用应用程序的过程如下,代码有删减
def run(self, application):
self.setup_environ()
self.result = application(self.environ, self.start_response)
self.finish_response()
QQ交流群: 211426309