如何创建部署WSGI使用什么字段类型创建新的字段的Python应用

使用上一步创建的超级鼡户登录你会看到管理站点的首页,如下图所示:

这里的GroupUser模型是Django权限框架的一部分位于django.contrib.auth中。如果你点击Users会看到你之前创建的用户。你的blog应用的Post模型与User模型关联在一起记住,这种关系由author字段定义

1.4.3 添加模型到管理站点

让我们添加blog模型到管理站点。编辑blog应用的admin.py文件如下所示:

现在,在浏览器中重新载入管理站点你会看到Post模型,如下图所示:

这很容易吧当你在Django管理站点注册模型时,你会得到一个用户友好的界面该界面通过内省你的模型产生,允许你非常方便的排列编辑,创建和删除对象

点击Post右边的Add链接來添加一篇新的帖子。你会看到Django为模型动态生成的表单如下图所示:

Django为每种字段使用什么字段类型创建新的字段使用不同的表单控件。即使是复杂的字段(比如DateTimeField)也会使用类似JavaScript的日期选择器显示一个简单的界面。

填写表单后点击Save按钮。你会被重定向到帖子列表页面其中显示一条成功消息和刚刚创建的帖子,如下图所示:

1.4.4 自定义模型显示方式

现在我们看下如何自定义管理站点。編辑blog应用的admin.py文件修改为:

我们告诉Django管理站点,使用从ModelAdmin继承的自定义类注册模型到管理站点在这个类中,我们可以包括如何在管理站点Φ显示模型的信息以及如何与它们交互。list_display属性允许你设置想在管理对象列表页中显示的模型字段

让我们使用更多选项自定义管理模型,如下所示:

回到浏览器重新载入帖子类别页,如下图所示:

你可以看到帖子列表页中显示的字段就是在list_display属性中指定的字段。现在帖子列表页包括一个右边栏,可以通过list_filter属性中包括的字段来过滤结果页面上出现了一个搜索栏。这是因为我们使用search_fields属性定义了可搜索的芓段列表在搜索栏下面,有一个通过日期进行快速导航的栏这是通过定义date_hierarchy属性定义的。你还可以看到帖子默认按StatusPublish列排序。这是因為使用ordering属性指定了默认排序

post链接,你会看到有些不同了当你为新帖子输入标题时,会自动填写slug字段我们通过prepopulated_fields属性已经告诉了Django,用title字段的输入预填充slug字段同样,author字段显示为搜索控件当你有成千上万的用户时,比下拉框更人性化如下图所示:

通过几行代码,我们已經在管理站点中自定义了模型的显示方式还有很多自定义和扩展Django管理站点的方式。本书后面的章节会涉及这个特性

现在,你已经有了一个功能完整的管理站点来管理博客的内容是时候学习如何从数据库中检索对象,并与之交互了Django自带一个强大的数据库抽象API,可以很容易的创建检索,更新和删除对象Django的ORM(Object-relational Mapper)兼容MySQL,PostgreSQLSQLite和Oracle。记住你可以在项目的settings.py文件中编辑DATABASES设置,来定义项目的数据库Django鈳以同时使用多个数据库,你可以用任何你喜欢的方式甚至编写数据库路由来处理数据。

一旦创建了数据模型Django就提供了一个自由的API来與之交互。你可以在找到数据模型的官方文档

打开终端,运行以下命令来打开Python终端:

译者注:书中的代码不是创建一个Post实例洏是直接使用create()在数据库中创建对象。这个地方应该是作者的笔误

让我们分析这段代码做了什么。首先我们检索usernameadmin的用户对象:

get()方法允許你冲数据库中检索单个对象。注意该方法期望一个匹配查询的结果。如果数据库没有返回结果该方法会抛出DoesNotExist异常;如果数据库返回哆个结果,将会抛出MultipleObjectsReturned异常这两个异常都是执行查询的模型类的属性。

然后我们使用titleslugbody创建了一个Post实例并设置之前返回的user作为帖子嘚作者:

该对象在内存中,而不会存储在数据库中

最后,我们使用save()方法保存Post对象到数据库中:

这个操作会在底层执行一个INSERT语句我们已經知道如何先在内存创建一个对象,然后存储到数据库中但也可以使用create()方法直接在数据库中创建对象:

现在,修改帖子的标题并再次保存对象:

直到调用save()方法,你对对象的修改才会存到数据库中

Django的ORM是基于QuerySet的。一个QuerySet是来自数据库的对象集合它可以有數个过滤器来限制结果。你已经知道如何使用get()方法从数据库检索单个对象正如你所看到的,我们使用Post.objects.get()访问该方法每个Django模型最少有一个管理器(manager),默认管理器叫做objects你通过使用模型管理器获得一个QuerySet对象。要从表中检索所有对象只需要在默认的objects管理器上使用all()方法,比如:

这是如何创建一个返回数据库中所有对象的QuerySet注意,该QuerySet还没有执行Django的QuerySet是懒惰的;只有当强制它们执行时才会执行。这种行为让QuerySet变得很高效如果没有没有把QuerySet赋值给变量,而是直接在Python终端输写QuerySet的SQL语句会执行,因为我们强制它输出结果:

你可以使用管理器的filter()方法過滤一个QuerySet例如,我们使用下面的QuerySet检索所有2015年发布的帖子:

你也可以过滤多个字段例如,我们可以检索2015年发布的作者的usernameamdin的帖子:

这等价于链接多个过滤器,来创建QuerySet

通过两个下划线(publish__year)我们使用字段查找方法构造了查询,但我们也可以使用两个下划线访问相关模型的芓段(author__username)

你可以使用管理器的exclude()方法从QuerySet中排除某些结果。例如我们可以检索所有2017年发布的,标题不是以Why开头的帖子:

你可以使鼡管理器的order_by()方法对不同字段进行排序例如,你可以检索所有对象根据它们的标题排序:

默认是升序排列。通过负号前缀指定降序排列比如:

如果想要删除对象,可以这样操作:

注意删除对象会删除所有依赖关系。

你可以连接任意多个过滤器到QuerySetQuerySet执行之前,不会涉及到数据库QuerySet只在以下几种情况被执行:

  • 当你对它们进行pickle或缓存
  • 当你显示对它们调用list()
  • 当你在语句中测试,比如bool()orand戓者if

1.5.6 创建模型管理器

正如我们之前提到的objects是每个模型的默认管理器,它检索数据库中的所有对象但我们也可以为模型洎定义管理器。接下来我们会创建一个自定义管理器,用于检索所有状态为published的帖子

编辑blog应用中的models.py文件,添加自定义管理器:

get_queryset()是返回被執行的QuerySet的方法我们使用它在最终的QuerySet中包含了自定义的过滤器。我们已经自定义了管理器并添加到Post模型中;现在可以用它来执行查询。唎如我们可以检索所有标题以Who开头,并且已经发布的帖子:

1.6 构建列表和详情视图

现在你已经了解了如何使用ORM,可鉯随时构建博客应用的视图了一个Django视图就是一个Python函数,它接收一个web请求并返回一个web响应。视图中的所有逻辑返回期望的响应

首先,峩们会创建应用视图然后定义每个视图的URL模式,最后创建HTML模板渲染视图产生的数据每个视图渲染一个的模板,同时把变量传递给模板并返回一个具有渲染输出的HTTP响应。

1.6.1 创建列表和详情视图

让我们从创建显示所有帖子的列表视图开始编辑blog应用的views.py文件,如下所示:

你刚创建了第一个Django视图post_list视图接收request对象作为唯一的参数。记住该参数是所有视图都必需的。在这个视图中我们使用之湔创建的published管理器检索所有状态为published的帖子。

最后我们使用Django提供的快捷方法render(),渲染指定模板的帖子列表该函数接收request对象作为参数,通过模板路径和变量来渲染指定的模板它返回一个带有渲染后文本(通常是HTML代码)的HttpResponse对象。render()快捷方法考虑了请求上下文因此由模板上下文处悝器(template context processor)设置的任何变量都可以由给定的模板访问。模板上下文处理器是可调用的它们把变量设置到上下文中。你将会在第三章中学习洳何使用它们

让我们创建第二个视图,用于显示单个帖子添加以下函数到views.py文件中:

这是帖子的详情视图。该视图接收yearmonthdaypost作为参数用于检索指定别名和日期的已发布的帖子。注意当我们创建Post模型时,添加了unique_for_date参数到slug字段这就确保了指定日期和别名时,只会检索到┅个帖子在详情视图中,我们使用get_object_or_404()快捷方法检索期望的帖子该函数检索匹配给定参数的对象,如果没有找到对象就会引发HTTP 404(Not found)异常。最后我们使用模板,调用render()快捷方法渲染检索出来的帖子

一个URL模式由一个Python正则表达式,一个视图和一个项目范围内的洺字组成Django遍历每个URL模式,并在匹配到第一个请求的URL时停止然后,Django导入匹配URL模式的视图传递HttpRequest类实例和关键字或位置参数,并执行视图

如果你以前没有使用过正则表达式,可以在了解

blog应用的目录下新建一个urls.py文件,添加以下代码:

第一个URL模式不带任何参数映射到post_list视圖。第二个模式带以下四个参数映射到post_detail视图。让我们看看URL模式的正则表达式:

  • year:需要四个数字
  • month:需要两个数字在前面补零。
  • day:需要两個数字在前面补零。
  • post:可以由单词和连字符组成

最好为每个应用创建一个urls.py文件,这可以让应用在其它项目中复用

现在你需要在项目嘚主URL模式中包含blog应用的URL模式。编辑项目目录中的urls.py文件如下所示:

这样你就可以让Django包括URL模式,该模式在blog/路径下的urls.py文件中定义你指定它们嘚命名空间为blog,这样你可以很容易的引用该URLs组

URL,为Post对象构建标准的URLDjango的惯例是在模型中添加get_absolute_url()方法,该方法返回对象的标准URL對于这个方法,我们会使用reverse()方法它允许你通过它们的名字,以及传递参数构造URLs编辑models.py文件,添加以下代码:

注意我们使用strftime()函数构造使鼡零开头的月份和日期。我们将会在模板中使用get_absolute_url()方法

1.7 为视图创建模板

我们已经为应用创建了视图和URL模式。现在该添加模板来显示用户界面友好的帖子了

在你的blog应用目录中创建以下目录和文件:

这就是模板的文件结构。base.html文件将会包括网站的主HTML结构它把内嫆分为主内容区域和一个侧边栏。list.htmldetail.html文件继承自base.html文件分别用于渲染帖子列表视图和详情视图。

Django有一个强大的模板语言允许你指定如何顯示数据。它基于模板标签——{% tag %}模板变量——{{ variable }},和可作用于变量的模板过滤器——{{ variable|filter }}你可以在查看所有内置模板标签和过滤器。

让我们編辑base.html文件添加以下代码:

%}模板过滤器。通过该模板过滤器你可以包括静态文件(比如blog.css,在blog应用的static/目录下可以找到这个例子的代码)拷贝这个目录到你项目的相同位置,来使用这些静态文件

你可以看到,有两个{% block %}标签它们告诉Django,我们希望在这个区域定义一个块从这個模板继承的模板,可以用内容填充这些块我们定义了一个title块和一个content块。

%}模板标签告诉Django从blog/base.html模板继承接着,我们填充基类模板的titlecontent块峩们迭代帖子,并显示它们的标题日期,作者和正文其中包括一个标题链接到帖子的标准URL。在帖子的正文中我们使用了两个模板过濾器:truncatewords从内容中截取指定的单词数,linebreaks把输出转换为HTML换行符你可以连接任意多个模板过滤器;每个过滤器作用于上一个过滤器产生的输出。

接着让我们编辑post/detail.html文件,添加以下代码:

返回浏览器点击某条帖子的标题跳转到详情视图,如下图所示:

当你开始往博客中添加内容你会发现需要把帖子分页。Django内置了一个分页类可以很容易的管理分页内容。

  1. 用每页想要显示的对象数量初始化Paginator
  2. 获得GET中的page參数,表示当前页码
  3. 调用Paginator类的page()方法,获得想要显示页的对象
  4. 如果page参数不是整数,则检索第一页的结果如果这个参数大于最大页码,則检索最后一页
  5. 把页码和检索出的对象传递给模板。

现在我们需要创建显示页码的模板,让它可以在任何使用分页的模板中使用在blog應用的templates目录中,创建pagination.html文件并添加以下代码:

这个分页模板需要一个Page对象,用于渲染上一个和下一个链接并显示当前页和总页数。让我們回到blog/post/list.html模板将pagination.html模板包括在{% content %}块的底部,如下所示:

因为我们传递给模板的Page对象叫做posts所以我们把分页模板包含在帖子列表模板中,并指定參数进行正确的渲染通过这种方法,你可以在不同模型的分页视图中重用分页模板

在浏览器中打开http://127.0.0.1:8000/blog/,你会在帖子列表底部看到分页並且可以通过页码导航:

1.9 使用基于类的视图

视图接收一个web请求,返回一个web响应并且可以被调用,所以可以把视图定义為类方法Django为此提供了基础视图类。它们都是继承自View类可以处理HTTP方法调度和其它功能。这是创建视图的一个替代方法

我们使用Django提供的通用ListView,把post_list视图修改为基于类的视图这个基础视图允许你列出任何使用什么字段类型创建新的字段的对象。

编辑blog应用的views.py文件添加以下代碼:

这个基于类的视图与之前的post_list视图类似,它做了以下操作:

  • 对结果进行分页每页显示三个对象。

为了保证分页正常工作我们需要传遞正确的page对象给模板。Django的ListView使用page_obj变量传递选中页因此你需要编辑list.html模板,使用正确的变量包括页码:

在浏览器打开http://127.0.0.1:8000/blog/检查是不是跟之前使用嘚post_list视图一致。这是一个基于类视图的简单示例使用了Django提供的通过类。你会在第十章和后续章节学习更多基于类的视图

在这章中,通过创建一个基本的博客应用我们学习了Django框架的基础知识。你设计了数据模型并进行了数据库迁移。你创建了视图模板和博客的URLs,鉯及对象分页

在下一章,你将会学习如何完善博客应用包括评论系统,标签功能并且允许用户通过e-mail分享帖子。

}

Interface即Web网关接口。其实它并不是OSI七層协议中的协议它就是一个接口而已,即函数而WSGI规定了该接口的输入,输出等其中输入是指Python应用程序服务器调用接口时提供的实参包含哪些数据,即接口参数environ包含哪些数据;输出是指接口必须返回哪些参数给接口调用者(即Python应用程序服务器)说白了,WSGI就是接口的规范而已
environ -- WSGI规定它是一个字典对象,要包含CGI环境变量和一些WSGI需要的变量这是由Web应用程序服务器提供的

Python基础学完后,免不了偠深入到Python的主流Web框架(Python科学计算那部分暂时用不到可以先不管)在学习Flask这些框架的过程中发现它们的底层都是WSGI协议,故决定先啃下WSGI鉴於目前网上几乎没有(完整的)WSGI中文版,于是干脆自己翻译这样也有助于加深自己的理解,也能够帮助到一些初学者

###摘要 这份规范规定了一种在web服务器与web应用程序/框架之间推荐的标准接口,以确保web应用程序在不同的web服务器之间具有可移植性

相比之下,虽然java也拥有眾多web框架但是java的 servlet API 使得使用任何框架编写出来的应用程序都可以在所有支持 servlet API 的web服务器上运行。

FastCGI等))的使用和普及将人们从web框架的选择和web垺务器的选择中分离开来,使他们能偶任意选择适合自己的组合而web服务器和web框架的开发者们也能够把精力集中到各自的领域。

基于此這份PEP建议在web服务器和web应用程序/web框架之间建立一种简单通用的接口规范,即Python Web服务器网关接口(简称WSGI)

但是光有这么一份规范,对于改变web服務器和web应用程序/框架的现状还是不够的只有当那些web服务器和web框架的作者/维护者们真正地实现了WSGI,这份WSGI规范才能起到它该起的作用

然而,由于目前还没有任何框架或服务器实现了WSGI而那些转向支持WSGI的框架作者们也不会得到任何直接的奖励或者好处,因此我们的这份WSGI必须偠拟定地足够容易实现,这样才能降低框架作者们在实现接口这件事上的初始投资成本

由此可见,服务器和框架两边接口实现的简单性对于提高WSGI的实用性来说,绝对是非常重要的同时,这一点也是任何设计决策的首要依据

然而需要注意的是,框架作者实现框架时的簡单性和web应用程序开发者使用框架时的易用性是两码事WSGI为框架作者们提出了一套只包含必需、最基本元素的接口,因为像响应对象以及 cookie 處理等这些花哨的高级功能只会妨碍现有的框架对这些问题的处理再说一次,WSGI的目标是使现有的web服务器和web框架之间更加方便地互联互通而不是想重新创建一套新的web框架。

同时也要注意到我们的这个目标也限制了WSGI不会用到任何当前版本的Python里没有的东西。因此这一份规范中不会推荐或要求任何新的Python标准模块,WSGI中规定的所有东西都不需要2.2.2以上版本的Python支持(当然,在未来版本的Python标准库中倘若Python自带的标准庫中的Web服务器能够包含对我们这份接口的支持,那将会是一个很不错的主意)

除了要让现有的以及将要出现的框架和服务器容易实现之外,也应该让创建诸如请求预处理器(request preprocessors)、响应处理器(response postprocessors)及其他基于WSGI的中间件组件这一类事情变得简单易操作这里说的中间件组件,它们是这樣一种东西:对服务器来说它们是应用程序而对中间件包含的应用程序来说,它们又可以被看作是服务器

如果中间件既简单又鲁棒,並且WSGI可以广泛地应用在服务器和框架中那么就有可能出现全新的Python web框架:一个由若干个WSGI中间件组件组成的松耦合的框架。事实上现有框架的作者们甚至可能会选择去重构他们框架中已有的服务,使它们变得更像是一些配合WSGI使用的库而不是一个完整的框架这样一来,web应用程序开发者们这就可以为他们想实现的特定功能选择最佳组合的组件而不用再局限于某一个特定框架并忍受该框架的所有优缺点。

当然就现在来说,这一天毫无疑问还要等很久同时,对WSGI来说让每一个框架都能在任何服务器上运行起来,又是一个十足的短期目标

最後,需要指出的是此版本的WSGI对于一个应用程序具体该以何种方式部署在web服务器或者服务器网关上并没有做具体说明。就现在来看这个昰需要由服务器或网关来负责定义怎么实现的。等到以后等有了足够多的服务器/网关通过实现了WSGI并积累了多样化的部署需求方面的领域經验,那么到时候也许会产生另一份PEP来描述WSGI服务器和应用框架的部署标准

WSGI接口可以分为两端:服务器/网关端和应用程序/Web框架端。服务器端调用一个由应用程序端提供的可调用者`(Callable)`至于它是如何被调用的,这要取决于服务器/网关这一端我们假定有一些服务器/网关会要求应鼡程序的部署人员编写一个简短的脚本来启动一个服务器/网关的实例,并提供给服务器/网关一个应用程序对象而还有的一些服务器/网关則不需要这样,它们会需要一个配置文件又或者是其他机制来指定应该从哪里导入或者获得应用程序对象

除了单纯的服务器/网关和应用程序/框架,还可以创建一种叫做中间件的组件中间件它对这份规范当中的两端(服务器端和应用程序端)都做了实现,我们可以这样解释中間件对于包含它们的服务器,中间件是应用程序而对于包含在中间件当中的应用程序来说,它又扮演着服务器的角色不仅如此,中間件还可以用来提供可扩展的API以及内容转换,导航和其他有用的功能

在这份规范说明书中,我们将使用的术语"callable(可调用者)"它的意思是"┅个函数,方法类,或者拥有 call 方法的一个对象实例"这取决于服务器,网关或者应用程序根据需要而选择的合适的实现技术。相反垺务器,网关或者请求一个可调用者(callable)的应用程序必须不依赖可调用者(callable)的具体提供方式。记住可调用者(callable)只是被调用,不会洎省(introspect)[译者注:introspect,自省Python的强项之一,指的是代码可以在内存中象处理对象一样查找其它的模块和函数]

####应用程序/框架 端 一个应用程序对象简单地说就是一个接受了2个参数的可调用对象`(callable object)`,这里的对象并不能理解为它真的需要一个对象实例:一个函数、方法、类、或者带囿 `__call__` 方法的对象实例都可以用来当做应用程序对象应用程序对象必须可以被多次调用,实质上所有的服务器/网关(除了CGI)都会产生这样的偅复请求

(注意:虽然我们把它叫做“应用程序”对象,但这并不意味着程序员需要把WSGI当做API来调用!我们假定应用程序开发者将会仍然使用更高层的框架服务来开发它们的应用程序WSGI只是一个提供给框架和服务器开发者们使用的工具,它并没有打算直接向应用程序开发者提供支持)

这里我们来看两个应用程序对象的示例:其中,一个是函数另一个是类:

"""这可能是最简单的应用程序对象了。""" """生成相同的输絀但是使用的是一个类。 (注意:这里‘AppClass’就是一个“应用程序”故调用它会返回一个‘AppClass’的实例,这个实例就是规范里面说的由一個“可调用的应用程序(application callable)”返回的可迭代者(iterable) 如果我们希望使用‘AppClass’的实例,而不是应用程序对象那么我们就必须实现这个‘__call__’方法,这个方法将用来执行应用程序然后我们需要创建一个实例来提供给服务器/网关使用。

####服务器/网关 端 每一次当HTTP客户端冲着应用程序发来一个请求,服务器/网关都会调用应用程序可调用者(callable)为了说明方便,这里有一个CGI网关简单的说它就是一个以应用程序对象为參数的函数实现,注意本例中对错误只做了有限的处理,因为默认情况下没有被捕获到的异常都会被输出到`sys.stderr`并被服务器记录下来

# 在第┅次输出之前发送已存储的报头。 # 如果报头已发送则重新抛出原始的异常。 if data: # 在报文体出现前不发送报头 write('') # 如果报文体为空,则发送报头

中间件:可扮演两端角色的组件

我们注意到,单个对象可以作为请求应用程序的服务器存在也可以作為被服务器调用的应用程序存在。这样的“中间件”可以执行以下这些功能:

  • 在相应地重写environ变量之后根据目标URL地址将请求路由到不同的應用程序对象。
  • 允许多个应用程序或框架在同一个进程中并行运行
  • 通过在网络上转发请求和应答,实现负载均衡和远程处理

中间件的存在对于“服务器/网关”和“应用程序/框架”来说是透明的,并不需要特殊的支持希望在应用程序中加入中间件的用户只须简单地把中間件当作应用程序提供给服务器,并配置中间件组件以服务器的身份来调用应用程序当然,中间件组件包裹的“应用程序”也可能是另外一个包裹了应用程序的中间件组件这样循环下去就构成了我们所说的“中间件栈”了。

最重要的别忘了中间件必须遵循WSGI的服务器和應用程序两端提出的一些限制和要求,甚至有些时候对中间件的要求比对单纯的服务器或应用程序还要严格,关于这些我们都会在这份規范文档中指出来

这里有一个(有趣的)中间件组件的例子,这个中间件使用Joe Strout 写的piglatin.py程序将text/plain的响应转换成pig latin [译者注:意思是将英语词尾改成拉丁语式] (注意:一个“真实”的中间件组件很可能会使用更加鲁棒的方式来检查上下文(content)的使用什么字段类型创建新的字段和上下文(content)的编码同样,这个简单的例子还忽略了一个单词还可能跨区块分割的可能性)

"""将可迭代的输出转换成拉丁语式,如果可以转换的话 注意“okayness”可能改变,直到应用程序生成(yield)出它自己的第一个非空字符串所以,‘transform_ok’必须是一个可变的真实值 # 默认情况下不传送输絀。 # 重置ok标志位以防这是一个重复的调用。

###规格的详细说明 应用程序对象必须接受两个位置参数(positional arguments)为了方便说明,我们不妨将它们汾别命名为`environ`和`start_response`但是这并不是说它们必须取这两个名字。服务器或网关必须用这两个位置参数(注意不是关键字参数)来调用应用程序对潒(比如像上面展示的那样调用`result =

environ参数是一个字典对象,也是一个有着CGI风格的环境变量这个对象必须是一个Python内建的字典对象(不能是子類、用户字典(UserDict)或其他对字典对象的模仿),应用程序必须允许以任何它需要的方式来修改这个字典 environ还必须包含一些特定的WSGI所需的变量(在后面章节里会提到),有时也可以包含一些服务器相关的扩展变量通过下文提到的命名规范来命名。

和 两章节中详细描述它只囿在应用程序捕获到了错误并试图在浏览器中显示错误的时候才会被用到。

只是为了支持一些现有框架的命令式输出APIs;新的应用程序或框架应当尽量避免使用write()详细情况请看 章节。)

当应用程序被服务器调用的时候它必须返回一个能够生成0个或多个字符串的可迭代者(iterable)。鈳以通过几种方式来实现比如通过返回一个包含一系列字符串的列表,或者是让应用程序本身就是一个能生成多个字符串的生成器(generator)又或者是使应用程序本身是一个类并且这个类的实例是一个可迭代者(iterable)。总之不论通过什么途径完成,应用程序对象必须总是能返囙一个能够生成0个或多个字符串的可迭代者(iterable)

服务器或者网关必须将产生的字符串以一种无缓冲的方式传送到客户端,并且总是在一個字符串传完之后再去请求下一个字符串(换句话说,也就是应用程序必须自己负责实现缓冲机制更多关于应用程序输出应该如何处悝的细节,请阅读下文的 章节)

服务器或网关应当将产生的字符串看做是一串二进制字节序列来对待:特别地,它必须确保行的结尾没有被修改应用程序必须负责确保将那些要传送至HTTP客户端的字符串以一种与客户端相匹配的编码方式输出(服务器/网关可能会对HTTP附加传输编碼,或者为了实现一些类似字节范围传输(byte-range transmission)这样的HTTP特性而进行一些转换更多关于HTTP特性的细节请参照下文的

假如服务器成功调用了len(iterable)方法,则它会认为此结果是正确的并且信赖这个结果也就是说,如果应用程序返回的可迭代者(iterable)字符串提供了一个可用的__len__() 方法那么服务器就会假定应用程序确实是返回了正确的结果。(关于这个方法在一般情况下是如何被使用的请阅读下文的 。)

如果应用程序返回的可迭玳者(iterable)有一个叫做close()的方法则不论当前的请求是正常结束还是由于异常而终止,服务器/网关都必须在结束该请求之前调用这个方法(這么做的目的是为了支持应用程序端的资源释放,这份规范将尝试完善对 中生成器的支持以及其它有close()方法的通用可迭代者(iterable)的支持。

(注意:应用程序必须在可迭代者(iterable)产生第一个报文主体(body)字符串之前请求start_response()可调用者(callable)这样服务器才能在发送任何报文主体(body)內容之前发送响应头。不过这一调用也可能在可迭代者(iterable)第一次迭代的时候执行,所以服务器不能假定在它们开始迭代之前

最后要说嘚是服务器和网关不能使用应用程序返回的可迭代者(iterable)的任何其他属性,除非是针对服务器或网关的特定使用什么字段类型创建新的芓段的实例比如wsgi.file_wrapper返回的“file wrapper”(请阅读 章节)。通常情况下只有在这里指定的属性,或者通过 访问的属性才是可以接受的

####`environ`变量 `environ`字典被用來包含这些CGI环境变量,这些变量定义可以在参考文献Common Gateway Interface specification[[2]](#refrence) 中找到除非是空字符串,否则下面所列出的这些变量都必须指定但是在(空字符串)这种情况下,它们会被忽略如果下面没有特别说明的话。



URL请求中‘路径’(‘path’)的开始部分对应了应用程序对象,这样应用程序就知道它的虚拟位置如果该应用程序对应服务器根目录的话, 那么SCRIPT_NAME的值可能为空字符串


URL请求中‘路径’(‘path’)的其余部分,指定请求的目標在应用程序内部的虚拟位置如果请求的目标是应用程序根目录并且末尾没有'/'符号结尾的话,那么PATH_INFO可能为空字符串


URL请求中紧跟在“?”後面的那部分,它可以为空或不存在




SERVER_PORT这两个变量永远不可能是空字符串,并且总是必须指定的


客户端发送请求的时候所使用的协议版夲。通常是类似“HTTP/1.0” 或 “HTTP/1.1”这样的字符串可以被应用程序用来判断如何处理HTTP请求报头。(事实上我认为这个变量更应该被叫做 REQUEST_PROTOCOL因为这個变量代表的是在请求中使用的协议,而且看样子和服务器响应时使用的协议毫无关系然而,为了保持和CGI的兼容性这里我们还是沿用巳有的名字SERVER_PROTOCOL。)


这组变量对应着客户端提供的HTTP请求报头(即那些名字以 “HTTP_” 开头的变量)这组变量的存在与否应和HTTP请求中相对应的HTTP报头保持一致。


一个服务器或网关应该尽可能多地提供其他可用的CGI变量另外,如果启用了SSL服务器或网关也应该尽可能地提供可用的Apache SSL环境变量,比如 HTTPS=onSSL_PROTOCOL不过要注意的是,假如一个应用程序使用了些上述没有列出的变量那么对于那些不支持相关扩展的服务器来说,就必然要栲虑到不可移植的缺点(比如,不发布文件的web服务器就没法提供一个有意义的 DOCUMENT_ROOT 或 PATH_TRANSLATED变量)

一个遵循WSGI规范的服务器或网关应该在文档中描述它们自己的定义的同时,适当地说明下它们可以提供哪些变量而应用程序这边则应该对它们要用到的每一个变量的存在性进行检查,並且在当检测到某些变量不存在时要有备用的措施

注意: 缺失的变量 (比如当没有发生身份验证时的REMOTE_USER变量) 应该被排除在environ字典之外。同样需要注意的是CGI定义的变量,如果有出现的话那必须是字符串使用什么字段类型创建新的字段。使用任何除了字符串使用什么字段类型創建新的字段以外的CGI变量都是违反本规范的

除了CGI定义的变量,environ 字典也可以包含任何操作系统相关的环境变量并且必须包含下面这些WSGI定義的变量:

应用程序被调用过程中的一个字符串,表示URL中的"scheme"部分正常情况下,它的值是“http”或者“https”视场合而定。
一个能被HTTP请求主体(body)读取的输入流(类文件对象) (由于应用程序是不定时发来请求服务器或网关在读取的时候可能是按需读取。或者它们会预读取客戶端的请求体然后缓存在内存或者磁盘中又或者根据它们自己的参数,利用其他技术来提供这样一种输入流)
输出流(类文件对象),鼡来写错误信息的目的是记录程序或者其他标准化及可能的中心化错误。它应该是一个“文本模式”的流;举一个例子应用程序应该鼡“\n”作为行结束符,并且默认服务器/网关能将它转换成正确的行结束符对很多服务器来说,wsgi.errors是服务器的主要错误日志当然也有其它選择,比如sys.stderr或者干脆是某种形式的日志文件。服务器的文档应当包含以下这类解释:比如该如何配置这些日志又或者该从哪里去查找這些记录下来的输出。如果需要一个服务器或网关还可以向不同的应用程序提供不同的错误流。
如果一个应用程序对象同时被处于同一個进程中的不同线程调用则这个参数值应该为“true”,否则就为“false”
如果相同的应用程序对象同时被其他进程调用,则此参数值应该为“true”;否则就为“false”
如果服务器/网关期待(但不保证)应用程序在它所在的进程生命期间只会被调用一次,则这个值应该为“true”正常凊况下,对于那些基于CGI(或类似)的网关这个值只可能是“true”。

最后想说的是这个environ字典有可能会包含那些服务器定义的变量。这些变量应该用小写数字,点号及下划线来命名并且必须定义一个该服务器/网关专有的前缀开头。举个例子mod_python在定义变量的时候,就会使用類似mod_python.some_variable这样的名字

#####输入和错误流 服务器提供的输入输出流必须提供以下的方法:

除了在注释栏有特别标注的注意点之外,以上所有方法的語义在Python Library Reference 里已经写得很具体了

  1. 服务器读取的长度不一定非要超过客户端指定的Content-length, 并且如果应用程序尝试去读取超过那个长度则服务器可鉯模拟一个流结束(end-of-file)条件。而应用程序这边则不应该去尝试读取比指定的CONTENT_LENGTH更长的数据
  2. 可选参数size是不支持用于readline()方法中的,因为它有可能會给开发服务器的作者们增大困难所以在实际中它不并常用。
  3. 请注意readlines()方法中的隐藏参数对于它的调用者和实现者都是可选的应用程序方可以自由地选择不提供它,同样的服务器或网关这端也可以自由地选择是否无视它。
  4. 由于错误流不能回转(rewound)服务器和网关可以立即选择自由地继续向前写操作(forward write),而不需要缓存在这种情况下,flush()方法可能就是个空操作(no-op)不过,一个具备可移植特优点的应用程序千萬不能假定这个输出是无缓冲的或假定flush是一个空操作一个可移植的应用程序如果需要确保输出确实已经被写入,则必须调用flush()方法(例洳:在多进程下对同一个日志文件写入操作的时候,可以起到最小化数据混杂的作用)

所有遵循此规范的服务器都必须支持上表中所列絀的每一个方法。所有遵循此规范的应用程序都不能使用除上表之外的其他方法或属性特别需要指出的是,应用程序千万不要试图去关閉这些流就算它们自己有对close()方法做处理也不行。

status参数是http的‘status’字符比如“200 OK”, “404 Not Found”这样的。也就是说它是包含了一个状态码和一个原洇短语的字符串,用空格分隔并且顺序是状态码在前原因短语在后status参数的两头不允许包含其他的字符或空格,(参见 6.1.1 章节获取更多信息),status字符串不能包含控制字符末尾也不能有终止符或换行符等其他的组合符号。

时返回值必须是ListType。如果需要服务器可以随意修改咜的内容,每一个header_name都必须是合法的HTTP header字段名 (参见 4.2章节)末尾不能有冒号或其他标点符号。

所有的header_value都不能包含任何控制字符如回车或换荇符等,中间嵌入或者末尾都不行(做这样的要求是为了方便那些必须检查响应头的服务器,网关中间件,使它们将必需的解析工作複杂度降到最低)

一般来说,服务器或网关负责确保将正确的头信息发送到客户端如果应用程序(application)遗漏了必要的头信息(或其他相關的规范信息),则服务器或网关必须补上 比如: HTTP date:Server: 头信息通常是由服务器或网关提供的。

(这里必须给服务器/网关的作者们提个醒: HTTP 头洺称是区分大小写的所以在检查应用程序提供的头信息时一定要考虑大小写的问题。)

应用程序和中间件禁止使用HTTP/1.1的‘逐跳路由 (hop-by-hop)’特性或头信息(headers)以及任何HTTP/1.0中那些可能会对客户端跟服务器之间的持久连接产生影响的类似特性或头信息。这类特性是现今Web服务器的专屬负责区域如果一个应用程序尝试发送这类特性,那么服务器/网关必须将这看作是一个严重错误并且如果它们是提供给start_response()的,则服务器/網关还必须抛出一个异常 (了解更多‘逐跳路由 (hop-by-hop)’的细节和特性,请参阅下面的 章节)

start_response可调用者肯定不能是“真实地”传送了响应頭信息。相反地它必须储存这类头信息以便服务器/网关用来传送,前提是应用程序返回值的第一次迭代生成了一个非空字符串或者说茬对应用程序的第一次调用中它的write()方法可被调用。换句话说也就是在没有真正的响应体数据可用之前,响应头不能被发送否则当应用程序到达的时候,可迭代者都已经被迭代完了(唯一可能的例外就是当响应头信息里显式地指定了Content-Length的值为0。)

响应头信息传输的延迟昰为了确保缓存的和异步的应用程序能够利用出错信息替换掉它们一开始打算的输出,一直到最后一刻举个例子,在应用程序缓存期间如果在生成body数据时发生了错误,那么应用程序可能会将响应状态从‘200 OK’替换成‘500 Internal Error’

handler)调用时才需要被提供。如果提供了exc_info参数并且还没囿任何HTTP头信息被发送那么start_response应当使用新提供的HTTP响应头去替换掉当前已存储的HTTP响应头,从而允许应用程序在错误发生的情况下可以针对输出“改变主意”

然而,假如提供了exc_info参数并且HTTP头信息也已经被发送那么start_response必须抛出错误,也必须抛出exc_info元组即:

这样会使得应用程序捕捉到嘚异常被重新抛出,并且原则上应该也会终止应用程序(一旦HTTP信息头已经被发送,则应用程序再试图向浏览器发送错误信息是不安全的)应用程序不应当捕获任何由start_response抛出的异常,如果它通过exc_info参数调用start_response的话相反,应用程序应该允许类似这样的异常传送回服务器或者网关更多信息请参考下文的

应用程序有可能多次调用start_response,当且仅当exc_info参数被提供的时候更确切的说是,如果start_response已经被当前应用程序调用过了那麼没有提供exc_info参数的情况下再调用start_response将会是一个很严重的错误。(参考上面CGI网关示例来帮助理解准确的逻辑)

注意:实现了start_response的服务器,网关或者中间件应当确保在函数调用期之外没有保存任何指向exc_info参数的引用,这样做的目的是为了避免通过回溯(traceback)及有关帧(frames involved)生成一个循环引用(circular reference)最简单的例子可以是这样:

####处理Content-Length头信息 如果应用程序没有提供Content-Length头信息,则服务器/网关可以有几种方法来处理它这些方法当中朂简单的就是在响应完成的时候关闭客户端连接。

然而在某些情况下服务器或网关可能会要么自己生成Content-Length头,要么至少避免了关闭客户端連接的需求如果应用程序没有调用write()可迭代者 返回一个长度(len())为1的可迭代者,则服务器可以自动地识别出Content-Length的长度这是通过可迭代者生荿的第一个字符串的长度来判断的。

特性那么服务器可以在每一次调用write()方法发送数据块(Chunk)或者由可迭代者迭代生成的字符串时,由此為每个数据块生成Content-Length头这样就可以让服务器保持与客户端的长连接,如果它希望这么做的话注意,如果真要这么做则服务器必须完全遵循 规范,要不然就转而寻找其它的策略来处理Content-Length的缺失

(注意:应用程序和中间件的输出一定不能使用任何使用什么字段类型创建新的芓段的传输编码(Transfer-Encoding)技术,比如chunking 或者 gzipping 这些;因为在“逐跳路由(hop-by-hop)”操作中这些编码都属于现实服务器/网关的职权范围。详细信息可以參见下文的 章节)

####缓冲和流 一般而言,应用程序都会选择先缓存(适当大小的)输出再一次性发送的方式来提高吞吐量现有的Zope框架就鼡的这种常见的处理方法:输出首先会被缓存到`StringIO`或类似的对象里面,然后跟着响应头再一次性被传送出去

在WSGI中,相应的处理方法是让应鼡程序简单地返回一个单一元素可迭代者(single-element iterable)比如列表(List)这个单一元素可迭代者包含一个单字符串形式的响应体(response body )。这是一种对于絕大多数应用程序都推荐的工作方式通过渲染那些文本信息很容易被保存到内存的HTML页面。

然而对于大文件或专门用途的HTTP流媒体(如多蔀分(multipart )的“服务器推送”),应用程序或许需要以较小块状的方式提供输出(比如说为了避免加载一个很大的文件到内存中这种情况)還有些时候某些响应体的部分内容可能需要花费很长的时间来生成,这种情况下提前发送该响应体中那些已经生成好的部分还是很有必要嘚

在这种情况下,应用程序通常会返回一个可迭代者(常见的是生成器迭代器(generator-iterator))这个可迭代者会以逐块(block-by-block)的方式生成输出。并苴这些块有可能会被破坏分成小块有时是为了跟多块分界线(mulitpart boundaries)(比如“服务器推送”)保持相符,又或者是在一些费时任务(比如读取磁盘文件的另一个块)之前

WSGI服务器/网关和中间件不允许延迟传送任何块;它们要么完整地将所有的块都传送给客户端,要么保证它们會继续向客户端传送块即使是应用程序正在生成下一个块。一个服务器/网关或者中间件可以以下列三种方案中的任意一种提供保证

  1. 在返回控制权给应用程序之前,发送整个块到操作系统(要求所有的O/S缓存被刷新(flushed))
  2. 当应用程序在生成下一个块的时候,使用一个不同的线程来确保当前块能被继续传送
  3. 仅使用中间件)来发送整个块到它的父级服务器/网关。

通过提供这样的保证措施WSGI 就能允许应用程序保证茬它们输出数据的过程中在任意点上都不会陷入停滞。这对于确保诸如多部分(multipart)“服务器推送(server push)”流的正常工作是至关重要的因为茬这种情况下多块分界线(multipart boundaries)之间的数据应当被完整地传送至客户端。

#####中间件处理块边界 为了更好地支持异步应用程序及服务器中间件組件一定不能阻塞迭代,该迭代等待从应用程序的可迭代者(iterable)中返回多个值如果中间件需要从应用程序中累积更多的数据才能够生成┅个输出,那么它必须生成(yield)一个空字符串

让我们换一种方式来表述这个要求,每一次当下层的应用程序生成了一个值中间件组件嘟必须生成至少一个值。如果中间件什么值都生成不了那么它也必须至少生成一个空字符串。

这个要求确保了异步的服务器和应用程序能同谋合作在同时运行给定数量的应用程序实例时,可以减少所需要的线程数量

同时也需要注意的是,这样的要求也意味着一旦处于丅层的应用程序返回了一个可迭代者(iterable)中间件就必须尽快地返回一个(iterable)。另外中间件也不被允许利用write() 可调用者(callable)来传输由下层應用程序生成的数据。中间件仅可以使用它父级服务器的write() 可调用者(callable)来传送由下层应用程序利用中间件提供的write() 可调用者(callable)发送来的数據

#####可调用的`write()`函数 一些现有框架的APIs与WSGI的一个不同处理方式是它们支持无缓存的输出,特别需要指出的是它们提供一个`write`函数或方法来写一個无缓冲的块或数据,或者它们提供一个缓冲的`write`函数和一个“刷新(flush)”机制来刷新缓冲

不幸的是,就WSGI这样“可迭代”的应用程序返回徝来说除非使用多线程或其他的机制,否则这样的APIs并没有办法实现

因此为了允许这些框架继续使用这些必要的APIs,WSGI中包含了一个特殊的write()調用它由start_response可调用者返回。

如果有办法避免的话新的WSGI应用程序和框架不应该使用write()调用。严格说来这个write()调用是用来支持必要的流APIs的。一般来说应用程序应该通过返回的可迭代对象(iterable)来生成输出,因为这样可以使得web服务器在同一个Python线程中不同任务之间的交织变得可能整体上来讲是为服务器提供了更好的吞吐量。

这个write()调用是由start_response可调用者返回的它接受一个唯一的参数:一个将作为部分HTTP响应体而被写入的芓符串,它被看作是已经被迭代生成后的结果换句话说,在writer()返回前它必须保证传入的字符串要么已经完全发送给客户端,要么已经在應用程序继续处理的过程当中被缓存用做传输了

一个应用程序必须返回一个可迭代对象,即使它使用write()来生成全部或部分响应体返回的鈳迭代对象可以是空的(例如生成一个空字符串),但是假如它不生成空字符串,那么它的输出就就必须被服务器或者网关正常处理(仳如说它必须立即被发送或者是立即加入到队列当中)。应用程序不能在它们返回的可迭代者内调用write()这样的话,任意一个由可迭代对潒生成的字符串均会在所有传递给write()的字符串都被传送至客户端之后被传送

####Unicode问题 HTTP本身并不对Unicode提供直接支持,同样我们这份接口也不支持Unicode。所有的编码/解码工作都应当由应用程序端来处理;所有传给服务器或从服务器传出的字符串都必须是Python标准的字节字符串而不能是Unicode对象倘若在被要求使用字符串对象的地方使用Unicode对象,则会产生不可预料的结果

也要注意,作为状态或响应头传给start_response()方法的字符串在编码方面都必须遵循 规范也就是说,它们必须使用ISO-8859-1字符集或者使用 MIME编码。

Unicode字符或代码点将有可能会发生严重错误。同样地服务器和网关也不尣许向应用程序提供任何 Unicode字符。

再次声明本规范中提到的所有的字符串都必须是 str 使用什么字段类型创建新的字段或 StringType 使用什么字段类型创建新的字段,不能是 unicode 或 UnicodeType 使用什么字段类型创建新的字段并且,针对本规范中所提到的“字符串”这个词就算是一些平台允许 str/StringType 对象超过 8 bits/芓符,也仅仅是该“字符串”的低位的 8 bits hui 被用到

一般来说,应用程序应当自己负责捕获自己的内部错误并且负责向浏览器输出有用的信息。(由应用程序自己来决定哪些是“有用的信息”)

然而要显示这样的一条信息,并不是说应用程序真的向浏览器发送了数据真这樣做的话有损坏响应体的风险。因此WSGI 提供了一种机制,要么允许应用程序发送它自己的错误信息要么就自动地终止应用程序:通过使鼡传递给start_responseexc_info参数。这里有个如何使用它的例子

# 这里是常规的应用程序代码 # 在这个简陋的‘except:’之前,XXX应该在一个单独的handler里捕捉运行时异常

当有异常发生时,如果输出还没有被写入则对start_response的调用将正常返回,然后应用程序会返回一个错误信息体发送至浏览器然而如果有部汾输出已经被发送到浏览器了,那么start_response将会重新抛出预备好的异常这个异常不应当会被应用程序捕获,因此应用程序它会异常终止服务器/网关会捕获这个(严重)异常并终止响应。

服务器应当捕获任何迫使应用程序或应用程序迭代返回值终止的异常并记录日志。如果应鼡程序出错的时候已经有一部分响应被写入到浏览器了则服务器或网关可以尝试添加一个错误消息给到输出,当然前提是已经发送了的頭信息里有指示一个text/* content 使用什么字段类型创建新的字段让服务器就知道应该如何干净地做修改

一些中间件可能会希望提供额外的异常处理垺务,或者拦截并替换应用程序的出错信息在这种情况下,中间件可以选择不重新抛出提供给start_responseexc_info转而换作是抛出中间件自己专有的异瑺,或者也可以在存储了所提供的参数之后简单地返回不包含任何异常。这将会导致应用程序返回它自己的错误信息体可迭代者(iterable)(戓调用write())然后让中间件来捕获并修改错误输出。以上这些只有在应用程序的开发者们做到下面这些时才可有作用:

  1. 每一次当开始一个错誤响应的时候都提供exc_info
  2. exc_info已经提供了的情况下不要去捕获由start_response产生的异常。
  1. 正常处理请求但是额外提供给应用程序一个wsgi.input流,当/如果应鼡程序第一次尝试从输入流中读取的时候就发送一个“100 Continue”响应这个读取请求必须一直保持阻塞状态直到客户端响应请求。
  2. 一直等待直箌客户端确认服务器不支持expect/continue特性,然后客户端自己发来请求体(这个方法较次,不是很推荐)

注意,以上这些行为的限制不适用于HTTP 1.0请求也不适用于那些往应用程序对象发送的请求。更多关于HTTP 1.1 Except/Continue的信息请参阅 的8.2.3章节和10.1.1章节。

####HTTP的其他特性 通常来说服务器和网关应当“尽尐干涉”,应当让应用程序对它们自己的输出有100%的控制权服务器/网关只做一些小的改动并且这些小改动不会影响到应用程序响应的语义(semantics )。应用程序的开发者总是有可能通过添加中间件来额外提供一些特性的所以服务器/网关的开发者在实现服务器/网关的时候可以适当偏保守些。在某种意义上说一个服务器应当将自己看作是一个HTTP“网关服务器(gateway server)”,应用程序则应当将自己看作是一个HTTP “源服务器(origin server)”(关于这些术语的定义请参照 [RFC

然而,由于WSGI服务器和应用程序并不是通过HTTP通信的 中提到的“逐跳路由(hop-by-hop)”并没有应用到WSGI内部通信中。因此WSGI应用程序一定不能生成任何"逐跳路由(hop-by-hop)"头信息,试图使用HTTP中要求它们生成这样的报头的特性或者依赖任何传入的"逐跳路由(hop-by-hop)"environ字典中报头。WSGI服务器必须自己处理所有已经支持的"逐跳路由(hop-by-hop)"头信息比如为每一个到达的信息做传输解码,解码也要包括那些分块編码(chunked-encoding)的如果有的话。

如果将这些原则应用到各种各样的HTTP特性中去应该很容易得知:服务器可以通过If-None-MatchIf-Modified-Since请求头,Last-ModifiedETag响应头等方式来處理缓存验证然而,这并不是必须的如果应用程序自身支持的话,则应用程序应当自己负责处理缓存验证因为服务器/网关就没有说必须要做这样的验证。

同样地服务器可能会对一个应用程序的响应做重编码或传输编码,不过应用程序应当对自己发送的内容做适当嘚编码并且不能做传输编码。如果客户端请求需要则服务器可能以字节范围(byte ranges)的方式传送应用程序的响应,应用程序并没有对字节范圍(byte ranges)提供原生支持再次申明,如果有需要应用程序则应当自己执行此功能。

注意这些对应用程序的限制不是说要求每一个应用程序都重新实现一次所有的HTTP特性;中间件可以实现许多HTTP特性的全部或者一部分,这样便可以让服务器和应用程序作者从一遍又一遍实现这些特性的痛苦中解放出来

####线程支持 除非本身不支持,否则支不支持线程也是取决于服务器自己的服务器虽然可以同时并行处理多个请求,但也应当提供额外的选择让应用程序可以以单线程的方式运行这样一来 ,一些不是线程安全的应用程序或框架就可以依旧在这些服务器上运行

###具体实现/应用程序

####服务器扩展API 一些服务器的作者可能希望暴露更多高级的API,让应用程序和框架的作者能用来做更特别的功能唎如,一个基于`mod_python`的网关可能就希望暴露部分Apache API作为WSGI的扩展

在最简单的情况下,这只需要定义一个environ变量其它的什么都不需要了,比如mod_python.some_api但昰,更多情况下那些可能出现的中间件会就使情况变得复杂的多。比如一个API,它提供了访问environ变量中出现的同一个HTTP报头的功能如果environ变量被中间件修改,则它很可能会返回不一样的值

通常情况下,任何重复、取代或者绕过部分WSGI功能的扩展API都会有与中间件组件不兼容的风險服务器/网关开发者不能寄希望于没人使用中间件,因为有一些框架的作者们明确打算(重新)组织他们的框架使之几乎完全就像各種中间件一样工作。

所以为了提供最大的兼容性,提供了扩展API来取代部分WSGI功能的服务器/网关必须设计这些API以便它们被部分替换过的API调鼡。例如:一个允许访问HTTP请求头的扩展API需必须要求应用程序传输当前的environ以便服务器/网关可以验证那些能被API访问的HTTP头,验证它们没有被中间件修改过如果该扩展的API不能保证它总是就HTTP报头内容同environ达成协议,它就必须拒绝向应用程序提供服务例如,通过抛出一个错误返回None来玳替头信息集合,或者其它任何适合该API的东西

同样地,如果扩展的API额外提供了一种方法来写响应数据或头信息它应当要求start_response 这个可调用鍺在应用程序能获得的扩展的服务之前被传入。如果传入的对象和最开始服务器/网关提供给应用程序的不一样则它就不能保证正确运转並且必须拒绝给应用程序提供扩展的服务。

这些指南同样适用于中间件中间件添加类似解析过的cookies信息,表单变量会话sessions,或者类似evniron特別地,这样的中间件提供的这些特性应当像操作environ的函数那样而不仅仅是简单地往evniron里面填充值。这样有助于保证来自信息是从evniron里计算得来嘚在所有中间件完成每一个URL重写或对evniron做的其它修改之后。

服务器/网关和中间件的开发者们遵守这些“安全扩展”规则是非常重要的否則以后就可能出现中间件的开发者们为了确保应用程序使用他们扩展的中间件时不被绕过, 而不得不从environ中删除一些或者全部的扩展API这样的倳情

####应用程序配置 这份规范没有定义一个服务器如何选择/获得一个应用程序来调用。因为这和其他一些配置选项一样都是高度取决于服務器的我们期望那些服务器/网关的作者们能关心并且负责将这些事情文档化:比如如何配置服务器来执行一个特定的应用程序对象,以忣需要带什么样的参数(如线程的选项)

另一方面,Web框架的作者应当关心这些事情并将它们文档化:比如应该怎样创建一个包装了框架功能的应用程序对象而已经选定了服务器和应用程序框架的用户,必须将这两者连接起来然而,现在由于Web框架和服务器有了两者之间囲同的接口使得这一切变成了一个机械式的问题,而不再是为了将新的应用程序和服务器配对组合的重大工程了

最后,一些应用程序框架,和中间件可能希望使用evniron字典来接受一些简单的字符串配置选项服务器和网关应当通过允许应用程序部署者向evniron字典里指定特殊的洺-值对(name-value pairs)对来支持这些。最简单的例子是由于部署者原则上可以配置这些外部的信息到服务器上,或者在CGI的情况下它们可能是通过服務器的配置文件来设置所以,可以仅仅从os.environ中复制操作系统提供的所有环境变量到environ字典中就可以了

应用程序本身应该尽量保持所需要的變量个数最少,因为并不是所有的服务器都支持简单地配置它们当然,即使在最槽糕的情况下部署一个应用程序的人还可以通过创建┅个脚本来提供一些必要的选项值:

但是,大多数现有的应用程序和框架很大可能只需用到environ里面的唯一一个配置值用来指示它们的应用程序或框架特有的配置文件位置。(当然应用程序应当缓存这些配置,以避免每次调用都重复读取)

注意,通过这种方式重建出来的URL鈳能跟客户端真实发过来的URI有些许差别举个例子,服务器的重写规则有可能会对客户端发来的最初请求的URL做修改以便让它看起来更规范。
有些服务器网关或者应用程序可能希望对Python2.2之前的版本提供支持。这在目标平台是Jython时甚是如此因为在我写这篇文档的时候,还没有┅个生产版本的Jython 2.2
对于服务器和网关来说,这是相当容易做到的:准备使用Python 2.2之前的版本的服务器和网关只需要简单地限定它们自己只使鼡标准的“for”循环来迭代应用程序返回来的所有可迭代者(iterable)即可。这是能在代码级别确保2.2之前的版本的迭代器协议(后续会讲)跟“现在的”迭代器协议(参照 [PEP234](https://www.python.org/dev/peps/pep-0234/) )兼容的唯一方法
(需要注意的是,这个技巧当然只针对那些由Python写的服务器网关,或者中间件至于如何正确地茬其他语言写的服务器中使用迭代器协议则不在我们这份PEP的讨论范围之内。)
不过对于应用程序这边来说,要提供对Python2.2之前的版本的支持則会稍微复杂些:
- 由于Python 2.2之前文件并不是可迭代的,故你不能返回一个文件对象并期望它能像一个可迭代者那样工作(总体来说,你也鈈能这么做因为大部分情况下这样做的表现很糟糕)。可以使用`wsgi.file_wrapper`或者一个应用程序特有的文件包装类(请参考 [可选的平台相关的文件處理](#optional) 章节获取更多关于`sgi.file_wrapper`的信息,该章节包含一个怎么把一个文件包装成一个可迭代者的例子)
- 如果你想返回一个定制加工过的可迭代者,那么它必须实现2.2版本之前的迭代器协议也就是说,提供一个` __getitem__`方法来接收一个整形的键值然后在所有数据都取完的时候抛出一个`IndexError`异常。(注意直接使用内置的序列使用什么字段类型创建新的字段也是可行的,因为它也实现了这个迭代器协议)
最后,如果中间件也希望對Python2.2之前的版本提供支持迭代应用程序返回的所有值或者由它自己返回一个可迭代者(又或者是两者都有),那么这些中间件必须遵循以仩提到的这些建议
(另外,为了支持Python2.2之前的版本毫无疑问,任何服务器网关,应用程序或者中间件必须只能使用该版本有的语言特性,比如用1和0而不是True和False,诸如此类)
####可选的平台相关的文件处理
有些操作环境提供了特殊的高性能文件传输机制,比如Unix下的`sendfile()`方法服務器和网关可以通过`environ`变量中的 `wsgi.file_wrapper` 这个选项来使用这个机制。应用程序可以使用这样的“文件包装(file wrapper)”来将一个文件或者类文件对象(file-like object )转換为一个可迭代者然后返回它例如:

如果一个服务器或网关有提供wsgi.file_wrapper选项,则它必须是个可调用者(callable)并且这个可调用者接受一个必要嘚位置参数,和一个可选的位置参数第一个参数是将要发送的类文件对象,第二个参数是可选的表示分块大小(block size)的建议(这个服务器/网关无需使用)。这个可调用者必须返回一个可迭代的对象(iterable object)并且在服务器/网关真正从应用程序那里接收到了一个可迭代者作为返囙值之前,不能执行任何的数据传送(否则会阻碍中间件解析或覆盖响应数据(response data))

至于那个由应用程序提供的被当作是类文件的对象,它则必须拥有一个read()方法并接受一个可选的size参数它可能还需要有一个close()方法,如果有那么由wsgi.file_wrapper返回的可迭代者它必须有一个close()方法可以调用朂初的类文件对象中的close()方法。如果这个“类文件“对象还拥有任何的方法或属性与Python内置的文件对象的属性或方法名相同(例如fileno())那么wsgi.file_warpper可能会假设这些方法或属性跟Python内置的文件对象的语义(semantics)是相同的。

在真实的实现中任何平台相关的的文件处理都必须发生在应用程序返囙之后,接着服务器/网关会去检查一个包装对象(wrapper object)是否有返回(再次声明,由于存在中间件错误处理等等类似的东西,所以并不保證任何生成的包装(wrapper)会被真正地使用到)

除了处理close()方法,从语义上讲应用程序返回一个包装的文件(file wrapper )应当看起来就像是应用程序返回了一个可迭代者iter(filelike.read, '')一样。换句话说当传输开始的时候,应当从文件的当前位置开始传输并且继续直到最后完成。

当然平台相关的攵件传输API通常不接受随意的类文件对象,所以一个wsgi.file_wrapper为了判断类文件对象是否适用于支持的平台相关的API,不得不对提供的对象做一些类似fileno()(类Unix 平台下)或者是java.nio.FileChannel(Jython下)的自省检查

注意:即使对象不适用与特定的平台API,wsgi.file_wrapper必须仍旧返回一个包装了的read()close()的迭代因此应用程序使用這文件包装器便可以再不同平台间移植。这里有个简单的平台无关的文件包装类适应于旧的(2.2之前)和新的Python,如下:

这里是一段来自服務器/网关的小程序它提供了访问一个特定平台的API的办法:

# 如果是,则使用该API来传送结果 # 如果不是,则按正常情况循环处理可迭代者(iterable)

鼡字典的原理是为了最大化地满足在服务器之间的移植性。还有另一种选择就是定义一些字典方法的子集并以字典的方法作为标准的便捷接口。然而事实上大多数的服务器可能只需要找到一个合适的字典就足够它们用了,并且框架的作者们往往期待完整可用的字典特性因为多半情况下是这样的。不过问题是如果有一些服务器选择不用字典那么尽管这类服务器也“符合”规范,还是会出现互用性的问題因此强制使用字典的话,就简化了这份规范并且并确保了互用性

注意,以上这些并不妨碍服务器或框架的开发者们向evnrion字典里加入自萣义的变量来提供特殊的服务事实上我们鼓励使用这种方式来提供任意的增值服务。

2.为什么你既可以调用write()又可以生成(yield)字符串/返回一個可迭代者(iterable)我们难道不应该只选择一种做法吗?

如果我们仅仅使用迭代的做法那么现存的框架将遭受“推送(push)”可用性的折磨。但是如果我们只支持通过write()推送,那么服务器在传输大文件的时候性能将恶化(如果一个工作线程(worker)没有将所有的输出都发送完成那么它将无法进行下一个新的请求)。因此我们做这样的妥协,好处是允许应用程序支持这两种方法视情况而定,并且比起单纯的 push-only 的方式来说只会给那些服务器的实现者们增加一点点负担而已。

3.close()方法是拿来做什么的

在应用程序执行期间,当写动作(writes)完成之后应鼡程序可以通过一个try/finally代码块来确保资源都被释放了。但是如果应用程序返回一个可迭代者(iterable),那么在迭代器被垃圾收集器收集之前任哬资源都不会被释放这里的close()惯用法允许应用程序在一个请求完成阶段释放重要资源,并且它向前兼容

4.为什么这个接口要设计地这么初级我希望添加更多酷炫的功能!(比如cookies, 会话(sessions) 持久性(persistence),balabala...)

记住这并不是另一个Python的web框架,这仅仅是一个框架向web服务器通信的方法反之亦然。如果你想拥有上面所说的这些特性你需要选一个提供了这些特性的框架。并且如果这个框架让你创建一个WSGI应用程序你将鈳以让它跑在大多数支持WSGI的服务器上面。同样的一些WSGI服务器或许会通过在它们的environ字典里提供的对象来提供一些额外的服务;可以参阅这些服务器具体的文档了解详情。(当然使用这类扩展的应用程序将面临着无法移植到其他基于WSGI的服务器上的风险。)

5.为什么使用CGI的变量洏不是旧的HTTP头呢并且为什么将它们和WSGI定义的变量混在一起呢?

许多现有的框架很大程度上是建立在CGI规范基础上的并且现有的web服务器知噵如何生成CGI变量。相比之下另一种表示到达的HTTP信息的方式不仅分散破碎更缺乏市场支持。因此使用CGI“标准”看起来是个不错的办法它能最大化发挥现有的实现。至于将它们同WSGI变量混合在一起那是因为分它们的话会导致需要传入两个字典参数,显然这样做没什么好处

6.那关于状态字符串,我们可不可以仅仅使用数字来代替比如说传入“200”而不是“200 OK”?

这样做会使服务器/网关变得复杂化因为那样的话垺务器/网关就需要一个数值状态和相应信息的映射表。相比之下让应用程序或框架的作者们在他们处理专门的响应代码时顺便输入一些額外的信息则显得要简单地多,并且事实上经常是现有的框架已经有一个这样的映射表包含这些需要的信息了。总之权衡之后,我们認为这个让应用程序/框架来负责要比服务器或网关来负责要更适合些

因为它仅仅只是建议应用程序应当“装备妥当但不需要经常性地运荇(rig for infrequent running)”。这是因为应用程序框架在操作缓存、会话这些东西的时候有多种模式在“多重运行(Multiple Run)”模式下,框架可能会预先加载缓存并且在每个请求之后可能不会有写操作,比如写日志或会话数据到硬盘上等操作在“单运行(single run)”模式下,框架没有预加载避免了茬每一个请求之后刷新(flush)所有必要的写操作。

然而为了验证在后者的模式下应用程序或框架的正确操作,可能会必要地(或是权宜之計)不止一次调用它因此,一个应用程序不应当仅仅因为设置了wsgi.run_once为True就认定它肯定不会被再次运行

8.在应用程序代码里使用Feature X(字典(dictionaries),鈳调用者(callables)等等)这些特性显得很丑陋难道我们不可以使用对象来代替吗?

WSGI中这些所有特性的实现选择都是为了从另外一个特性中解耦合考虑的;将这些特性重新组装到一个封装完好了的对象之中只会在一定程度上增大写服务器/网关的难度并且在将来希望写一个中间件来只代替/修改一小部分整体功能的时候,难度会上升一个数量级

本质上,中间件希望有个“职责连”的模式凭借这个模式它可以在┅些功能中被看成是一个“handler”,而同时允许其他功能保持不变这样的要求,在接口想要保持可扩展性的前提下用普通的Python对象是比较难實现的。例如你必须使用__getattr__或者__getattribut__的重写(override)来确保这些扩展(比如未来的WSGI版本定义的变量)是被通过的。

这种使用什么字段类型创建新的芓段的代码是出了名的难以保证100%正确的并且极少人愿意自己重写。他们倾向于简单地复用别人的实现可是一旦别人修改了实现的另一處地方时他们却未能及时更新自己的拷贝。

进一步讲这种必需的样本代码将是纯碎的消费税,一种纯粹由中间件开发者们承担的开发者消费税它的目的仅仅是为了能给应用程序框架开发者们支持稍微“漂亮”点儿的API而已。但是应用框架开发者们往往只会更新一个框架來支持WSGI,这只占他们所有框架的非常有限的部分这很可能是他们的第一个(也可能是唯一一个)WSGI实现,因此他们很有可能去实现这份现荿的规范这样,花时间利用对象的属性或诸如此类的东西让这些API看起来"更漂亮"对正在读此文的您们来说,可能就是浪费时间

我们鼓勵那些希望在直接的Web应用程序编程(相对于web框架开发)中有更漂亮的(或是改进的)WSGI接口的人,鼓励他们去开发APIs或者框架来包装WSGI使WSGI对那些应用程序开发者们更加便利。这样的话WSGI就不仅可以在底层维持对服务器或中间件的便利性,同时对应用程序开发者来说又不会显得太“丑陋”

###尚在讨论中的提议 下面这些项都还正在Web-SIG或其他地方讨论中,或者说还在PEP作者的计划清单中:

  • wsgi.input是否改成一个迭代器而不是一个文件这对于那些异步应用程序和分块编码( chunked-encoding)的输入流是有帮助的。
  • 我们正在讨论可选的扩展它们将用来暂停一个应用程序输出的迭代,直到输入可用或者发生一个回调事件
  • 添加一个章节,关于同步 vs 异步应用程序和服务器相关的线程模型,以及这方面的问题/设计目标

###鸣谢 感谢那些Web-SIG邮件组里面的人,没有他们周全的反馈将不可能有我这篇修正草案。特别地我要感谢:

  • mod_python的作者Gregory “Grisha” Trubetskoy,是他毫不留情地指出了我的第一版草案没有提供任何比“普通旧版的CGI”有优势的地方他的批评促进了我去寻找更好的方法。
  • Ian Bicking是他总是唠叨着要我适当哋提供多线程(multithreading)及多进程(multiprocess)相关选项,对了他还不断纠缠我让我提供一种机制可以让服务器向应用程序提供自定义的扩展数据。
  • Tony Lownds昰他提出了start_response函数的概念,提供给它status和headers两个参数然后返回一个write函数他的这个想法为我后来设计异常处理功能提供了灵感,尤其是在考虑到Φ间件重写(overrides)应用程序的错误信息这方面
  • Mark Nottingham,是他为这份规范的HTTP RFC 发行规范做了大量的后期校对工作特别针对HTTP/1.1特性,没有他的指出我甚至鈈知道有这东西存在。

  • 本人翻译的初衷是为了自身学习和记录翻译不好或有误的地方,欢迎在我的Github上
}

我要回帖

更多关于 使用什么字段类型创建新的字段 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信