WSGI web服务网关协议(2)

本文译自:https://www.python.org/dev/peps/pep-0333

前一部分内容请查看:http://10111000.com/2017/12/22/PEP333_1/

tips: 以下, 服务端代指服务端和网关, 框架代指应用端和框架端

规范细节

应用对象必须接收两个位置参数。为了更好的说明,我们把两个变量分别命名为environ和start_response,但是这个规范里并不要求和变量名完全一致。服务器端必须传入相应的位置参数(不是字典参数)去调用应用对象。(如上所述,我们可以以这种方式进行调用:result = application(environ, start_response)。)

environ是一个字典对象,包含CGI风格的环境变量,这个对象必须是一个内置的python字典(不是一个子类,用户自定义的字典或其他拥有类似字典操作的对象),且可以被框架端任意修改成它想要的形式。这个environ也必须包含某些WSGI所需要的变量(稍后会加以详述),也可能包含服务器的某些扩展变量,按照惯例,如何命名将会在下文提及。

start_response是一个可接受两个位置参数,一个可选参数的可调用对象。同样为了说明,这个三个参数分别命名为status, response_headers 和 exc_info,它们也可以有不同的命名,框架端必须传入对应的位置参数去调用start_response方法。(例如:start_response(status, response_headers))

status参数是一个形如“999 message here”的状态字符串,response_headers是由HTTP响应头形成的元组(header_name, header_value)在一起组成的列表,对于可选参数exc_info,我们会在start_response()调用及错误处理章节进行解释。它仅在应用程序捕获错误后并尝试向浏览器端显示错误时使用。

start_response必须返回一个可调用的对象 - write(body_data), 这个对象的调用需要一个可以作为HTTP响应主体的字符串作为位置参数(注意:write仅用于支持某些现有的框架的输出API,如果新的框架可以避免的话,就不应该再使用,这一部分细节会在缓冲和流一节进一步阐述)。

当服务端调用框架端时,框架端应该返回包含0个或多个字符串的可迭代对象,这可以通过多种方式实现,比如返回一个包含多个字符串的列表,或者一个产生字符串的生成器函数,又或者是一个可迭代的实例对象。不管它是如何完成的,框架端都应该返回满足此要求的结果。

服务端必须在响应另一个请求之前,把产生的字符串以无缓冲的方式传输到客户端。(换句话说,框架端应该有它自己的缓冲区,有关框架端如何处理输出的更多信息,请参阅下面的缓冲和流一节)。

服务端应该把产生的字符串当成二进制字节码来处理:特别是要确保行结束符不被修改。框架端则负责确保写入的字符串是适合客户端的格式。服务端可以应用HTTP传输编码,或者为了实现诸如字节范围传输的HTTP功能而执行其他转换。(参阅:其他的HTTP功能)

如果len(iterable)方法调用成功,那么服务端也会认为结果是正确的,也就是说,如果框架提供了len方法,那它也必须返回一个与此方法相匹配的一个结果。(请参阅处理Content-Length头部分了解如何正确使用。)

如果框架端返回的可迭代对象有close()方法的话,那么不管请求有没有正常结束或者由于错误提前中止,服务端都必须在完成当前的请求后调用该方法(这个方法是为了支持框架端的一些资源释放)。该协议旨在对PEP 325的生成器及其他拥有close()方法的可迭代对象进行支持。

(注意:框架端必须在生成第一个响应主体的字符串之前调用start_response方法,这样服务端才能在发送任何主体内容之前发送响应头,然而,这个调用可能是由迭代器的第一次迭代执行的,所以服务器不能假定start_response()方法是在迭代器开始迭代之前就已经被调用了)

最后,服务端不能直接使用由框架端返回的可迭代对象中的任何属性,除非它是一个特定服务器的实例,比如由wsgi.file_wrapper返回的file wrapper(请参阅:可选特定平台的文件处理),一般情况下,只有这里指定的属性或通过PEP 234文档定义的API是可以使用的。

environ变量

environ字典需要包含如通用网关接口规范定义CGI环境变量。以下的变量是必须存在的,除非它们是空的字符串,在这种情况下,除非是另有说明,否则它们可能会被忽略。

变量名 描述
REQUEST_METHOD HTTP请求方法,比如GET和POST。这个变量不能为空,所以总是需要的
SCRIPT_NAME 请求URL的路径对应于框架端的初始位置,框架端会根据这个变量找到对应的虚拟位置。如果这个位置是框架端的根位置的话,那么这个变量可能是一个空字符串
PATH_INFO 请求的URL除去SCRIPT_NAME的剩余部分,指出了请求目标在框架端的虚拟位置。如果请求的目标是这个框架的根路径且没有“/”符号结尾的话,那么这个变量可能是一个空字符串。
QUERY_STRING URL中紧跟“?”后的那一部分。也可能是空的字符串或者不存在此变量。
CONTENT_TYPE HTTP请求头Content-Type中的内容, 可能是空的字符串或者不存在此变量。
CONTENT_LENGTH HTTP请求头Content-Length中的内容, 可能是空的字符串或者不存在此变量。
SERVER_NAME SERVER_PORT 当这些变量和SCRIPT_NAME,PATH_INFO拼在一起时,可以得到完整的URL。注意,如果存在HTTP_HOST的话,应该优先考虑使用HTTP_POST来重新构造请求URL.请参阅URL重建部分获取更多细节。SERVER_NAME, SERVER_PORT不能为空,所以总是需要的。
SERVER_PROTOCOL 用户用来发送请求的协议版本,典型的有“HTTP/1.0”或者“HTTP/1.1”和由应用程序用来确定如何处理的HTTP请求头。(这个变量也可能叫做请求协议- REQUEST_PROTOCOL,因为它表示的是请求的协议,并不一定是服务端用于响应的协议。不过为了和现有的通用网关接口兼容,我们仍然使用现在的这个变量名)
HTTP_VARIABLES 变量对应于客户端提供的HTTP请求头(比如以“HTTP_”开头的变量)这些变量的存在或不存在应该与HTTP请求头中的变量的存在或是不存在相对应。

服务端应该尝试提供尽可能多适用的CGI变量。此外,如果使用了SSL,服务端还应该提供一些相应的Apache SSL环境变量。比如HTTPS = on和SSL_PROTOCOL。不过,使用上面列出CGI变量以外的任何变量的应用程序,对于不支持相关扩展的Web服务器必然是不可移植的(例如,不提供文件发布服务的服务器就不能够给出有意义的DOCUMENT_ROOT或PATH_TRANSLATED。)

一个兼容WSGI规范的服务器必以文档的形式给出它所提供的变量,以及适当的定义。框架端应该检查它所需要的任何变量是否存在,并且在有变量不存在的情况下有对应的备用方案。

缺失的变量应该排除在environ字典之外(比如没有发生认证时的REMOTE_USER),另外CGI定义的变量必须是字符串,如果CGI定义的变量为任意的其他类型而不是字符串的话都是违反本规范的。

除去CGI定义的变量,environ字典也可能包含任意的操作系统环境变量,并且必须包含如下WSGI定义的变量。

变量
wsgi.version 元组(1,0),表示WSGI的版本1.0
wsgi.url_scheme 表示正在调用框架端的URL协议部分的字符串,一般来说值为“http”或“https”。
wsgi.input HTTP请求主体可以读取的输入流(类文件对象)。服务端可以根据框架端的请求按需读取,或者预读客户端发来请求并缓存在内存或磁盘中,或根据自己的偏好,用其他的方式提供的输入流。
wsgi.errors 一个输出流,可以写入错误对象,用于在标准化及中心位置记录程序及其他错误。这应该是一个“文本模式”流,比如框架端应该用“\n”来做为行结束符,并且假定服务端可以正确处理。对于大多数的服务器,wsgi.errors将是服务器端主要的错误日志,或者可能是sys.stderr,或某种类型的日志文件。服务器端应该提供相应文档解释如何配置及从哪里找到输出日志。如果需要的话,服务器端也可以提供不同的错误处理流给不同的框架端。
wsgi.multithread 如果框架端可以在一个进程中同时被不同的线程所调用的话,那么应该设为TRUE,否则为FALSE
wsgi.multiprocess 如果框架端可以同时被另外一个进程所调用的话,那么应该设为TRUE,否则为FALSE
wsgi.run_once 如果服务端期望框架端在一个进程的生命周期中只被调用一次(但不能保证),那么值应该为TRUE, 通常情况下,对于基于CGI(或类似的)的网关来说,这只会为TRUE。

最后,environ也可能包含服务端自定义变量,这些变量的变量名应该只能由小写字母,数字,点和下划线组成,并且应该以定义的服务器或网关唯一的名称作为前缀。例如,mod_python 自定义的变量可能为mod_python.some_variable的形式。

输入和错误流

服务端提供的输入和错误流必须支持以下方法。

Method Stream Notes
read(size) input 1
readline() input 1,2
readlines(hint) input 1,3
iter() input
flush errors 4
write(str) errors
writelines(seq) errors

上述每种方法的定义可以参考Python库中的定义,下面则是上表中Notes的一个说明:

  1. 服务器不需要读取超过客户端指定长度的内容,如果客户端尝试读取超过内容长度的点时,服务端可以模拟一个文件结束条件强制结束。框架端不应该读取超过content-length中定义的长度的内容。
  2. 可选的size参数,对于这里的readline函数来说是不支持的,因为其不太易于实现,在实际中也不太用的到。
  3. hint参数对于调用者和实现者来说都是可选的,框架端可以不提供,服务端也可以忽略它。
  4. 因为错误可能不能重现,所以服务端可以执行写操作而不经过缓冲区。在这种情况下,flush()可能是个空操作。不过,框架端不能假设这个输出是不缓冲的或flush()是个空操作。如果想要确保输出被写入,则必须调用flush()方法。(例如:最大限度地减少来自多个进程的数据混合写入一个相同的错误日志)

上面列出的方法都必须被遵循这个规范的服务端支持,框架端也应该不使用其他任何方法或input及errors对象的属性。需要指出的是,框架端不要尝试关闭这些流,即使它们拥有close()方法。

可调用的start_response函数

第二个传给框架端的参数是一个可调用的函数,它的调用形式为start_response(status, response_headers, exc_info = None)。(同所有的WSGI变量一样,参数必须以位置参数的形式提供,而不是字典参数。)start_response通常用来开始对HTTP请求做响应,它必须返回一个可调用的write(body_data)对象。(请参阅缓冲与流一节)

status参数是一个HTTP状态字符串,比如说“200 OK”或“400 Not Found”。它是一个由状态码和一个简短的理由组成的一个字符串,并且按照状态码,理由的顺序,中间由空格字符分开,没有额外的空格字符或者其他的字符。(请查阅RFC 2616的6.1.1节获取更多信息)。字符串一定不能包含控制字符,也不能用回车,行结束符或是它们的组合。

response_headers参数是一个元组列表。它必须是一个python内置的列表对象;比如:type(response_headers) 就是一个list,服务端也可以按照它自己的需求去修改其中的值。每个header_name都必须是一个有效的HTTP响应头。(RFC 26164.2节中有定义),不以冒号结尾,也没有其他的标点符号。

每个响应头的值,无论在值中间还是结尾,都不能含有任何控制字符,包括回车和行结束符。(这些需求是为了最小化服务端或需要检查、修改响应头的中间处理器,在执行解析操作时的复杂性)。

一般来说,服务端需要确保发送给客户端的是正确的响应头:如果框架端忽略了HTTP协议必须的响应头(或其他有效的规范),服务端必须在发送响应时添加。例如,HTTP Date:和Server: 这两个响应头正常来说必须由服务端提供。

(这里给服务端开发者一个提醒: HTTP响应头名字是大小写敏感的,所以在检查框架端发来的响应头时,应该把这一部分也考虑进去。)

框架和中间件中禁止使用HTTP/1.1中的“hop-by-hop”的首部或者类似的功能,以及任何在HTTP/1.0协议中与之相应的功能或其他可能影响客户端连接持久性的首部。这些功能与真正的WEB服务器的功能是独立的,服务端如果接收到框架端尝试发送类似的响应,应该判定其发生了致命的错误,并抛出异常。(关于“hop-by-hop”的更多细节,请参阅其他的HTTP特征一节)。

start_response不能直接传输响应头。相反,它必须存储相关的信息,等到服务端拿到框架返回一个非空字符串,或者在框架端第一次调用write方法后,交由服务端处理,再发送给客户端。换句话说,响应头必须等到有真正的传输主体时或直到框架端迭代完所有的主体内容才能进行传输。(唯一的例外是响应头中包含Content-Length,且其值为0.)

这样延迟发送响应头是为了确保在发送响应前的最后一刻,框架端也可以用错误的输出替换他们原来的输出。例如,当框架端在生成响应主体时发生错误,那么就可能需要把响应头从“200 OK”改为“500 Internal Error”。

exc_info参数,必须是一个Python的sys.exc_info()元组对象。这个参数只是在start_response在被错误处理器调用时,由框架端提供。如果exc_info存在并且HTTP响应头还没有输出时,start_response应该用新提供的响应头来替换现在已经存储的,因此通过这种方式可以在一个错误发生时,让框架端有机会“改变主意”。

不过,如果exc_info存在但是HTTP响应头也已经发送出去,那么start_response应该必须抛出一个异常,并且这个异常是由exc_info组成的元组。也就是:

raise exc_info[0], exc_info[1], exc_info[2]

这个再次抛出的异常会被框架再次捕获,原则上应该抛弃。(一旦HTTP请求头已经被发送,框架端试图发送错误输出到浏览器是不安全的。)框架端不应该捕获任何由start_response抛出的异常。相反,框架端应该允许把这些异常传播给服务端。请参阅错误处理一节查看更多细节。

框架可能会调用start_response不止一次,这种情况当且仅当提供了exc_info参数时才会发生。更准确的说,如果在框架端已经调用了start_response后,却在再次调用时没有提供exc_info参数,这是一个致命的错误。(请参阅上面的CGI网关的示例,了解正确的逻辑。)

注意:服务端、中间件在实现start_response方法时,应该确保在函数执行期间没有持续引用指向exc_info参数,这也是为了避免在回溯时产生循环引用。最简单避免该情况的方法如下:

def start_response(status, response_headers, exc_info=None):
    if exc_info:
         try:
             # do stuff w/exc_info here
         finally:
             exc_info = None    # Avoid circular ref.

CGI网关的例子也很好的说明了这个情况。

处理Content-Length首部

如果框架没有提供Content-Length首部,服务端可能需要选择一个方式去处理这种情况。最简单的方法是当响应结束时,关闭客户端连接。

某些情况下,服务端也许可以创建Content-Length首部, 或者至少避免关闭客户端连接。如果框架不需要调用write(),并且返回的可迭代对象长度为1,那么服务端就可以自动用第一个可迭代对象中每个字符串的长度来定义Content-Length的值。

如果服务端和客户端都支持HTTP/1.1的“块编码”,那么服务端也可以用块编码来发送每一个write()或迭代对象中每一个字符串,并为每一个块创建Content-Length,其值为块长度。这也允许服务端保持客户端连接,当然如果需要的话。注:当服务端这样使用时,必须遵循RFC 2616协议,或者回退到Content-Length缺失的其他策略。

(注意:框架和中间件不需要实现任何一个Transfer-Encoding的输出,比如块编码,gzipping;以及“hop-by-hop”操作,编码这些属于服务端的范围,请参阅其他HTTP特征一节获取细节。)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,504评论 6 13
  • 在 从零开始搭建论坛(一):Web服务器与Web框架 中我们弄清楚了Web 服务器、Web 应用程序、Web框架的...
    selfboot阅读 2,257评论 0 8
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 昨日听闻奶奶生病住院的消息,当时并没有特别的悲伤和难过,而是一遍遍机械地拨打爸妈的电话,通过他们去打听我叔叔的联系...
    黑桃K先森阅读 302评论 0 1