WSGI协议介绍(萌新版)

在探讨WSGI具体是什么之前,我们先考虑一个更加生活化的问题:
在邮局出现之前,人们为了联系出门在外的游子,往往依靠熟人捎信的方式来传递信息和物品。在一个偏远的山村里,老刘的儿子在上海卖药材,他要等到碰巧有熟人到上海去的时候才能拜托对方帮忙捎信,如果事情紧急的话免不了专程跑一趟。同样,老王的儿子在京城开当铺,老李的儿子在山东收驴皮,他们与亲人联系也非常不便。有人发现了这个需求,便站了出来,每个月出门一回,负责把附近几个村子的信件和物品送出去并收取少量的酬劳(艾玛,这不就是余秋雨的《信客》么)。信客的出现使人们不再为难以通信而犯愁,此外,为了方便管理信件和物品,他规定乡亲们的书信应该装在什么样的信封里,地址应该怎么怎么写,要是有物品需要寄送,应该怎么怎么包装,等等。时人将这套“接口规范”称之为XMTP(Xinke's Mail and Transport Protocol)。
那么, 现在回到python中来。
假如你已经掌握了python编程的基本技巧并且花了一个下午一字不落地看完了《图解HTTP》这本书,那么理论上来讲,你已经具备了python web应用开发的基本功,做个小网站应该不是什么难事。折腾了一番之后,你的网站就部署上线了。你用磕磕绊绊的代码完成了从监听网络端口到建立连接、解析请求的参数、进行回应、发送Response数据包等等一系列的工作。后来,陆陆续续又做了几个项目之后,你发现,娘的,每次都要在获取请求、解析请求、发送请求这些一成不变的步骤上花费大量时间,不如写个Server模块,把这些功能单独实现,以后再有新的需求,只需要考虑Framework部分即可,这样就可以避免“重复造轮子”了,就像下图描述的那样(图片来自Let’s Build A Web Server. Part 2.)。

这时候,隔壁老王听说你写了个Server程序很牛掰,想要借用一下你的代码。你本着开源精神将Server模块分享给他,可是,老王拿到代码一看就傻眼了——天啊,你的Server部分的数据接口、函数调用方式和他自己写的Framework完全不搭,改动起来难度颇大,还不如自己再写一个Server来得方便,如下图所示(图片来自Let’s Build A Web Server. Part 2.):

作为你们小区最厉害的程序员,你写的东西分享给了别人他们竟然没有办法去用,没有能够帮到老王的你感到很失落。这时候你想,自己写代码的时候,应该整一套接口规范出来,让老王在编写自己的Framework的时候就按照你设计的规范来写,这样写出来的程序才能够互相“契合”,便于调试和使用。OK,这个问题早就有人想到了,WSGI(Python Web Server Gateway Interface)就是一个这样的规范。如果把负责接收请求和发送回应的server看作信客的话,那么WSGI就是需要发送和接收各种消息的应用与这个信客相互“对接”的各项规范。接下来,我们就来详细了解一下这个规范。

WSGI不是一个库,也不是一个服务器程序,他只是一个协议,或者说是一份标准,用来描述Server与Framework之间的通信接口。这样,一些符合WSGI标准的Framework如Flask、Django、web.py等等就可以与同样符合WSGI标准的Server库进行无缝对接。只要你的Framework符合WSGi规范,那么在以后有了效率更高的Server的时候,你可以毫不费力地将代码迁移过去。
WSGI规范的具体内容在PEP-333中给出,PEP-3333是PEP-333的升级版本,主要是进行了一些修正并提升了对python 3 的支持(网上均可找到对应的中文翻译版本,最好能够通读一遍)。搞懂WSGI的具体内容对于以后我们自己编写Web框架来说非常重要,下面就以PEP-333为例来介绍一下其具体内容。
WSGI描述了Server与Framework之间通信的规范,简单来说,WSGI规范了以下几项内容:
  • WSGI协议主要包括server和application两部分,server负责接受客户端请求并进行解析,然后将其传入application,客户端处理请求并将响应头和正文返回服务器(严格说来,还有一个模块叫做中间件middleware,但中间件也同样使用上述两种接口进行通讯)
  • 从application的角度来说,它应当是一个可调用的对象(实现了__call__ 函数的方法或者类),它接受两个参数:environ和start_response,其主要作用就是根据server传入的environ字典来生成一个“可迭代的”http报文并返回给server
  • 从server的角度来说,其主要工作是解析http请求,生成一个environ字典并将其传递给可调用的application对象;另外,server还要实现一个start_response函数,其作用是生成响应头,start_response作为参数传入application中并被其调用
怎么办,感觉好复杂,完全看不懂。。。
不要紧,PEP-333中给出了两个小例子,让我们一起来看看。
对于一个实现了WSGI的服务器来说,它应该是这样的:
import os, sys#application是一个可调用的对象,作为参数传入服务器函数def run_with_cgi(application): #wsgi参数设置,解析请求并将其传入environ字典(这里并没有给出解析请求的代码) 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' headers_set = [] headers_sent = [] def write(data): 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 sys.stdout.write('Status: %s\r\n' % status) for header in response_headers: sys.stdout.write('%s: %s\r\n' % header) sys.stdout.write('\r\n') sys.stdout.write(data) sys.stdout.flush() #start_response的作用是生成响应头 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[0], exc_info[1], 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生成的http报文,application返回一个可迭代对象 result = application(environ, start_response) try: for data in result: if data: # don't send headers until body appears #如果获取到data,就调用write函数输出header和data write(data) if not headers_sent: write('') # send headers now if body was empty finally: if hasattr(result, 'close'): result.close()
根据上述代码,服务器中的数据流是这样的:

由于大多数初学者的目的是为了写一个web框架出来,所以对于server的具体技术细节可以不要深究,但一定要把server与application之间相互调用和传递参数的过程理清楚。
application的例子有两个,分别以函数和类进行说明。函数本身就可被调用,而类要在其内部实现一个__iter__(self)或__call__(self)方法才能成为可被调用的对象(请看代码中英文注释,或参考这篇文章:Python Web开发最难懂的WSGI协议,到底包含哪些内容?):
def simple_app(environ, start_response): """Simplest possible application object""" status = '200 OK' #构建响应头 response_headers = [('Content-type', 'text/plain')] #调用作为参数传入的start_response,其已经在server中定义 start_response(status, response_headers) #返回可迭代的响应体(即使只有一行,也要写成列表的形式) return ['Hello world!\n']class AppClass: """Produce the same output, but using a class (Note: 'AppClass' is the "application" here, so calling it returns an instance of 'AppClass', which is then the iterable return value of the "application callable" as required by the spec. If we wanted to use *instances* of 'AppClass' as application objects instead, we would have to implement a '__call__' method, which would be invoked to execute the application, and we would need to create an instance for use by the server or gateway. """ def __init__(self, environ, start_response): self.environ = environ self.start = start_response def __iter__(self): status = '200 OK' response_headers = [('Content-type', 'text/plain')] self.start(status, response_headers) #__iter__和yield结合,实现了生成器函数, #result = AppClass(environ, start_response)将返回一个可迭代的实例 yield "Hello world!\n"
对于新手来说,将application的各项规范理解透彻才是重点。WSGI的精华在于application的两个参数以及其如何返回response header和response body。把这个搞清楚,今天的目的就达到了。对于application的实现,重点只有三条:
  1. 接受environ 和start_response两个参数
  2. 内部调用 start_respons生成header
  3. 返回一个可迭代的响应体
当然,上面的代码只是个小例子,除了可以返回“Hello World!”之外,没有任何功能。你可以将上述代码块中的run_with_cgi部分的代码和application代码块中的simple_app()函数的代码复制到一个名为server.py的文件中,然后在该文件夹下进入python命令行,查看运行结果,以加深对于WSGI的理解。
因为上述run_with_cgi()并未实现监听http请求的功能,我们只能以一个简单粗暴的方式来获取response:
>>>run_with_cgi(simple_app)
操作无误的话,你将会看到如下的输出:
Python 2.7.12 (default, Nov 20 2017, 18:23:56) [GCC 5.4.0 20160609] on linux2Type "help", "copyright", "credits" or "license" for more information.>>> from server import *>>> run_with_cgi(simple_app)Status: 200 OKContent-type: text/plainHello world!
展开阅读全文

没有更多推荐了,返回首页