个性化阅读
专注于IT技术分析

WSGI:Python的服务器应用程序接口

本文概述

1993年, 网络仍处于起步阶段, 拥有大约1400万用户和100个网站。页面是静态的, 但是已经需要产生动态内容, 例如最新的新闻和数据。对此, Rob McCool和其他贡献者在国家超级计算应用程序中心(NCSA)HTTPd Web服务器(Apache的前身)中实现了通用网关接口(CGI)。这是第一台可以提供由单独的应用程序生成的内容的Web服务器。

从那时起, Internet上的用户数量激增, 动态网站变得无处不在。当第一次学习一种新语言甚至是第一次学习编码时, 开发人员很快就会想知道如何将其代码连接到网络中。

Web上的Python和WSGI的兴起

自创建CGI以来, 发生了许多变化。 CGI方法变得不切实际, 因为它需要在每个请求时创建一个新进程, 从而浪费了内存和CPU。出现了其他一些底层方法, 例如FastCGI](http://www.fastcgi.com/)(1996)和mod_python(2000), 它们在Python Web框架和Web服务器之间提供了不同的接口。随着各种方法的激增, 开发人员对框架的选择最终限制了Web服务器的选择, 反之亦然。

为了解决这个问题, Phillip J. Eby在2003年提出了PEP-0333, 即Python Web服务器网关接口(WSGI)。这个想法是在Python应用程序和Web服务器之间提供高级的通用接口。

在2003年, PEP-3333更新了WSGI界面以添加对Python 3的支持。如今, 几乎所有的Python框架都将WSGI用作与其Web服务器进行通信的一种手段, 即使不是唯一的手段。 Django, Flask和许多其他流行的框架就是这样做的。

本文旨在向读者简要介绍WSGI的工作原理, 并允许读者构建一个简单的WSGI应用程序或服务器。但是, 这并不意味着要详尽无遗, 打算实现生产就绪型服务器或应用程序的开发人员应更全面地研究WSGI规范。

Python WSGI接口

WSGI指定服务器和应用程序必须遵循的简单规则。让我们首先回顾一下这种总体模式。

Python WSGI服务器应用程序接口。

应用介面

在Python 3.5中, 应用程序界面如下所示:

def application(environ, start_response):
    body = b'Hello world!\n'
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return [body]

在Python 2.7中, 该界面没有太大不同;唯一的变化是主体由str对象表示, 而不是由一个字节表示。

尽管在这种情况下我们使用了一个函数, 但任何可调用函数都可以。应用程序对象的规则如下:

  • 必须是带有environ和start_response参数的可调用对象。
  • 发送正文之前, 必须调用start_response回调。
  • 必须返回带有文档主体的可迭代对象。

满足这些规则并会产生相同效果的对象的另一个示例是:

class Application:
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start_response = start_response

    def __iter__(self):
        body = b'Hello world!\n'
        status = '200 OK'
        headers = [('Content-type', 'text/plain')]
        self.start_response(status, headers)
        yield body

服务器接口

WSGI服务器可能与此应用程序交互, 如下所示:

def write(chunk):
    '''Write data back to client'''
    ...

def send_status(status):
   '''Send HTTP status code'''
   ...

def send_headers(headers):
    '''Send HTTP headers'''
    ...

def start_response(status, headers):
    '''WSGI start_response callable'''
    send_status(status)
    send_headers(headers)
    return write

# Make request to application
response = application(environ, start_response)
try:
    for chunk in response:
        write(chunk)
finally:
    if hasattr(response, 'close'):
        response.close()

你可能已经注意到, start_response可调用对象返回了一个写可调用对象, 应用程序可将其用于将数据发送回客户端, 但是我们的应用程序代码示例未使用该调用。此写接口已被弃用, 我们现在可以忽略它。稍后将在本文中简要讨论。

服务器职责的另一个特殊之处是在响应迭代器上调用可选的close方法(如果存在)。正如Graham Dumpleton在本文中所指出的那样, 它是WSGI经常被忽视的功能。调用此方法(如果存在)将允许应用程序释放它可能仍保留的任何资源。

Application Callable的环境参数

environ参数应该是一个字典对象。与CGI一样, 它用于将请求和服务器信息传递给应用程序。实际上, 所有CGI环境变量在WSGI中都是有效的, 服务器应将所有适用于该应用程序的参数传递给它。

虽然可以传递许多可选键, 但其中一些是必需的。以以下GET请求为例:

$ curl 'http://localhost:8000/auth?user=obiwan&token=123'

这些是服务器必须提供的密钥, 以及它们将采用的值:

注释
REQUEST_METHOD “得到”
SCRIPT_NAME “” 服务器设置相关
PATH_INFO ” / auth”
请求参数 “令牌= 123”
内容类型 “”
CONTENT_LENGTH “”
服务器名称 “127.0.0.1” 服务器设置相关
服务器端口 “8000”
SERVER_PROTOCOL ” HTTP / 1.1″
HTTP_(…) 客户端提供的HTTP标头
wsgi.version (1, 0) WSGI版本的元组
wsgi.url_scheme ” http”
wsgi.input 类文件对象
wsgi.errors 类文件对象
wsgi.multithread false 如果服务器是多线程的, 则为True
wsgi.multiprocess false 如果服务器运行多个进程则为真
wsgi.run_once false 如果服务器希望此脚本仅运行一次(例如, 在CGI环境中), 则为true

此规则的例外是, 如果这些键之一为空(如上表中的CONTENT_TYPE), 则可以从字典中将其省略, 并假定它们对应于空字符串。

wsgi.input和wsgi.errors

大多数环境键很简单, 但是其中两个更需要澄清:wsgi.input和wsgi.errors, wsgi.input必须包含带有来自客户端的请求正文的流, 应用程序在其中报告遇到的任何错误。从应用程序发送到wsgi.errors的错误通常会发送到服务器错误日志。

这两个键必须包含类似文件的对象。也就是说, 提供以流形式进行读取或写入的接口的对象, 就像在Python中打开文件或套接字时获得的对象一样。乍一看, 这似乎很棘手, 但幸运的是, Python为我们提供了解决此问题的好工具。

首先, 我们在谈论什么样的流?根据WSGI的定义, wsgi.input和wsgi.errors必须处理Python 3中的字节对象和Python 2中的str对象。无论哪种情况, 如果我们想使用内存中的缓冲区通过WSGI传递或获取数据, 接口, 我们可以使用io.BytesIO类。

例如, 如果我们正在编写WSGI服务器, 则可以向应用程序提供请求主体, 如下所示:

  • 对于Python 2.7
import io
...
request_data = 'some request body'
environ['wsgi.input'] = io.BytesIO(request_data)
  • 对于Python 3.5
import io
...
request_data = 'some request body'.encode('utf-8') # bytes object
environ['wsgi.input'] = io.BytesIO(request_data)

在应用程序方面, 如果我们想将收到的流输入转换为字符串, 则需要编写如下内容:

  • 对于Python 2.7
readstr = environ['wsgi.input'].read() # returns str object
  • 对于Python 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object
readstr = readbytes.decode('utf-8')      # returns str object

应使用wsgi.errors流向服务器报告应用程序错误, 并且行应以\ n结尾。 Web服务器应根据系统转换为不同的行结尾。

可调用应用程序的start_response参数

start_response参数必须是可调用的, 带有两个必需的参数, 即status和header, 以及一个可选的参数exc_info。在将正文的任何​​部分发送回Web服务器之前, 应用程序必须先调用它。

在本文开头的第一个应用程序示例中, 我们将响应的主体作为列表返回, 因此, 我们无法控制何时迭代列表。因此, 我们必须在返回列表之前调用start_response。

在第二个响应中, 我们在生成响应主体的第一个(在这种情况下, 也是唯一的)响应之前调用了start_response。两种方法在WSGI规范中均有效。

从网络服务器端开始, 对start_response的调用实际上不应将标头发送给客户端, 而应将其延迟, 直到响应正文中至少有一个非空字节串发送回客户端为止。这种体系结构可以正确地报告错误, 直到应用程序执行到最后一刻为止。

start_response的状态参数

传递给start_response回调的status参数必须是由HTTP状态代码和描述组成的字符串, 并用单个空格分隔。有效示例为:” 200 OK”或” 404未找到”。

start_response的标头参数

传递给start_response回调的headers参数必须是Python元组列表, 每个元组的组成为(header_name, header_value)。每个标头的名称和值都必须是字符串(与Python版本无关)。这是类型很重要的罕见示例, 因为WSGI规范确实要求这样做。

这是标题参数看起来像的有效示例:

response_body = json.dumps(data).encode('utf-8')

headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]

HTTP标头不区分大小写, 如果我们正在编写WSGI兼容的Web服务器, 则在检查这些标头时要注意这一点。此外, 应用程序提供的标题列表也不应该是详尽无遗的。在将响应发送回客户端之前, 服务器有责任确保所有必需的HTTP标头都存在, 并填充应用程序未提供的所有标头。

start_response的exc_info参数

start_response回调应支持第三个参数exc_info, 用于错误处理。对于生产Web服务器和应用程序, 此参数的正确用法和实现至关重要, 但这不在本文讨论范围之内。

可以在此处的WSGI规范中获得有关它的更多信息。

start_response返回值–写回调

为了向后兼容, 实现WSGI的Web服务器应返回write可调用的对象。此回调应允许应用程序将主体响应数据直接写回客户端, 而不是通过迭代器将其生成给服务器。

尽管存在该接口, 但该接口已被弃用, 新应用程序应避免使用该接口。

生成响应主体

实现WSGI的应用程序应通过返回一个可迭代的对象来生成响应主体。对于大多数应用程序, 响应主体不是很大, 可以轻松放入服务器的内存中。在这种情况下, 最有效的发送方式是一次发送, 并且一个元素可以迭代。在特殊情况下, 将整个主体加载到内存中是不可行的, 应用程序可能会通过此可迭代接口将其部分返回。

Python 2和Python 3的WSGI之间只有很小的区别:在Python 3中, 响应主体由bytes对象表示;在Python 2中, 正确的类型是str。

将UTF-8字符串转换为字节或str是一件容易的事:

  • Python 3.5:
body = 'unicode stuff'.encode('utf-8')
  • Python 2.7:
body = u'unicode stuff'.encode('utf-8')

如果你想了解有关Python 2的unicode和字节串处理的更多信息, YouTube上有一个不错的教程。

如上所述, 实现WSGI的Web服务器还应支持write回调以实现向后兼容性。

在没有Web服务器的情况下测试你的应用程序

了解了这个简单的界面之后, 我们可以轻松创建脚本来测试我们的应用程序, 而无需实际启动服务器。

以这个小脚本为例:

from io import BytesIO

def get(app, path = '/', query = ''):
    response_status = []
    response_headers = []

    def start_response(status, headers):
        status = status.split(' ', 1)
        response_status.append((int(status[0]), status[1]))
        response_headers.append(dict(headers))

    environ = {
        'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), }

    response_body = app(environ, start_response)
    merged_body = ''.join((x.decode('utf-8') for x in response_body))

    if hasattr(response_body, 'close'):
        response_body.close()

    return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}

这样, 例如, 我们可以将一些测试数据和模拟模块初始化到我们的应用程序中, 并进行GET调用以测试其是否响应。我们可以看到它不是实际的网络服务器, 但可以通过为应用程序提供start_response回调和包含我们的环境变量的字典来与我们的应用程序交互。在请求结束时, 它使用响应主体迭代器, 并返回包含所有内容的字符串。可以为不同类型的HTTP请求创建类似的方法(或通用方法)。

本文总结

WSGI是几乎所有Python Web框架的关键部分。

在本文中, 我们没有探讨WSGI如何处理文件上传, 因为这可能被认为是更”高级”的功能, 不适合介绍性文章。如果你想进一步了解它, 请查看PEP-3333一节中有关文件处理的内容。

我希望本文对帮助你更好地了解Python如何与Web服务器进行通信有帮助, 并允许开发人员以有趣和创造性的方式使用此接口。

致谢

我要感谢我的编辑Nick McCrea为本文提供的帮助。由于他的工作, 原始文本变得更加清晰, 并且一些错误并未得到纠正。

赞(0)
未经允许不得转载:srcmini » WSGI:Python的服务器应用程序接口

评论 抢沙发

评论前必须登录!