作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
莱安德罗·利马的头像

Leandro Lima

Leandro有15年以上的IT/开发经验. 自2013年开始使用Python,他喜欢构建高效且具有成本效益的系统.

Previously At

Embraer
Share

1993年,互联网还处于起步阶段,大约有 1400万用户和100个网站. 页面是静态的,但已经需要生成动态内容, 比如最新的新闻和数据. Responding to this, Rob McCool和其他贡献者在国家超级计算应用中心实现了通用网关接口(CGI)。 HTTPd web server (Apache的前身). 这是第一个可以为独立应用程序生成的内容提供服务的web服务器.

Since then, 互联网用户的数量呈爆炸式增长, 动态网站已经无处不在. 当你第一次学习一门新的语言,甚至是第一次学习编程, developers, soon enough, 想知道如何将他们的代码连接到web上.

Web上的Python和WSGI的兴起

自从CGI诞生以来,很多事情都发生了变化. CGI方法变得不切实际, 因为它需要在每次请求时创建一个新进程, 浪费内存和CPU. 其他一些低级别的方法也出现了,比如FastCGI [http://www].fastcgi.com/) (1996) and mod_python (2000),在Python web框架和web服务器之间提供不同的接口. 随着不同方法的激增, 开发人员对框架的选择最终限制了web服务器的选择,反之亦然.

为了解决这个问题,2003年菲利普. Eby proposed PEP-0333Python Web服务器网关接口(WSGI). 我们的想法是提供一个高层, Python应用程序和web服务器之间的通用接口.

In 2003, PEP-3333 更新了WSGI接口,加入了Python 3支持. Nowadays, 几乎所有的Python框架都使用WSGI作为一种手段, 如果不是唯一的方法, 与他们的web服务器通信. This is how Django, Flask 许多其他流行的框架都是这样做的.

本文旨在让读者了解WSGI是如何工作的, 并允许读者构建一个简单的WSGI应用程序或服务器. 本文并非详尽无遗, though, 打算实现生产就绪服务器或应用程序的开发人员应该更彻底地了解 WSGI specification.

Python WSGI接口

WSGI指定了服务器和应用程序必须遵守的简单规则. 让我们从回顾这个总体模式开始.

Python WSGI服务器-应用程序接口.

应用程序接口

In Python 3.5、应用程序接口是这样的:

Def application(environ, start_response):
    你好,世界!\n'
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(地位、头)
    return [body]

In Python 2.7, this interface wouldn’t be much different; the only change would be that the body is represented by a str 对象,而不是 bytes one.

虽然我们在这个例子中使用了一个函数,any callable will do. 应用程序对象的规则如下:

  • 必须是一个可调用的 environ and start_response parameters.
  • Must call the start_response 在发送body之前回调.
  • 必须返回文档主体的可迭代对象吗.

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

类的应用程序:
    Def __init__(self, environ, start_response):
        self.environ = environ
        self.Start_response = Start_response

    def __iter__(自我):
        你好,世界!\n'
        status = '200 OK'
        headers = [('Content-type', 'text/plain')]
        self.start_response(地位、头)
        yield body

Server Interface

WSGI服务器与这个应用程序的接口可能是这样的::

def write(chunk):
    "将数据写回客户端"
    ...

def send_status(状态):
   "发送HTTP状态码"
   ...

def send_headers(头):
    "发送HTTP头文件" "
    ...

Def start_response(地位、头):
    " WSGI start_response可调用" "
    send_status(状态)
    send_headers(头)
    return write

#向应用程序提出请求
Response = application(environ, start_response)
try:
    对于响应中的块:
        write(chunk)
finally:
    如果hasattr(response, 'close'):
        response.close()

你可能已经注意到了 start_response 可调用对象返回 write 可调用,应用程序可以使用它将数据发送回客户端, 但是我们的应用程序代码示例没有使用这种方法. This write 接口已弃用,我们现在可以忽略它. 本文稍后将对其进行简要讨论.

服务器职责的另一个特点是调用可选项 close 方法,如果存在的话. 正如Graham Dumpleton的文章所指出的那样 here,这是WSGI的一个经常被忽视的特性. 调用这个方法, if it exists,允许应用程序释放它可能仍然持有的任何资源.

应用程序的可调用对象 environ Argument

The environ 参数应该是一个字典对象. 它用于将请求和服务器信息传递给应用程序,与CGI的作用非常相似. In fact, 所有CGI环境变量在WSGI中都是有效的,服务器应该传递所有适用于应用程序的变量.

虽然有许多可选的键可以传递,但有几个是强制性的. 举个例子 GET request:

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

这些是服务器的密钥 must 提供,以及他们所接受的价值观:

KeyValueComments
REQUEST_METHOD"GET"
SCRIPT_NAME""依赖于服务器设置
PATH_INFO"/auth"
QUERY_STRING"token=123"
CONTENT_TYPE""
CONTENT_LENGTH""
SERVER_NAME"127.0.0.1"依赖于服务器设置
SERVER_PORT"8000"
SERVER_PROTOCOL"HTTP/1.1"
HTTP_(...) 客户端提供的HTTP标头
wsgi.version(1, 0)元组与WSGI版本
wsgi.url_scheme"http"
wsgi.input File-like object
wsgi.errors File-like object
wsgi.multithreadFalse True 如果服务器是多线程的
wsgi.multiprocessFalse True 如果服务器运行多个进程
wsgi.run_onceFalse True 如果服务器希望这个脚本只运行一次(e.g.:在CGI环境中)

该规则的例外情况是,如果其中一个键为空(如 CONTENT_TYPE 在上表中), 然后它们可以从字典中删除, 假设它们对应于空字符串.

wsgi.input and wsgi.errors

Most environ 关键字很简单,但其中两个值得进一步澄清: wsgi.input,它必须包含带有来自客户端的请求体的流,以及 wsgi.errors,应用程序在其中报告遇到的任何错误. 从应用程序发送的错误 wsgi.errors 通常会将错误日志发送到服务器.

These two keys must contain file-like objects; that is, 提供要作为流进行读写的接口的对象, 就像我们在Python中打开文件或套接字时得到的对象一样. 乍一看,这似乎很棘手,但幸运的是,Python为我们提供了很好的工具来处理这个问题.

首先,我们讨论的是哪种流? 根据WSGI的定义, wsgi.input and wsgi.errors must handle bytes 对象在Python 3和 str Python 2中的对象. In either case, 如果我们想使用内存缓冲区通过WSGI接口传递或获取数据, 我们可以利用课堂 io.BytesIO.

As an example, 如果我们正在编写WSGI服务器, 我们可以像这样向应用程序提供请求体:

  • For Python 2.7
import io
...
Request_data = '请求正文'
environ['wsgi.input'] = io.BytesIO (request_data)
  • For Python 3.5
import io
...
Request_data = '请求正文'.Encode ('utf-8') # bytes对象
environ['wsgi.input'] = io.BytesIO (request_data)

在应用程序端, 如果我们想把接收到的流输入转换成字符串, 我们想要这样写:

  • For Python 2.7
Readstr = environ['wsgi . s].input'].Read() #返回STR对象
  • For Python 3.5
Readbytes = environ['wsgi . s].input'].Read() #返回字节对象
Readstr = readbytes.Decode ('utf-8') #返回STR对象

The wsgi.errors 流应该用于向服务器报告应用程序错误,并且行应该以 \n. web服务器应该负责根据系统转换为不同的行结尾.

应用程序的可调用对象 start_response Argument

The start_response 参数必须是具有两个必需参数的可调用对象,即 status and headers,还有一个可选参数, exc_info. 它必须在正文的任何部分被发送回web服务器之前由应用程序调用.

在本文开头的第一个应用程序示例中, 我们已经将响应的主体作为列表返回, and thus, 我们无法控制何时对列表进行迭代. 正因为如此,我们不得不打电话 start_response 在返回列表之前.

在第二个例子中,我们调用了 start_response 在生成响应体的第一部分(在本例中是唯一的一部分)之前. 这两种方式在WSGI规范中都是有效的.

从web服务器端调用 start_response 实际上不应该发送报头给客户端, 但是要延迟它,直到响应体中至少有一个非空字节串要发送回客户端. 这种体系结构允许正确报告错误,直到应用程序执行的最后一刻.

The status Argument of start_response

The status 参数传递给 start_response callback必须是一个由HTTP状态码和描述组成的字符串, 用一个空格隔开的. 有效的例子有: '200 OK', or '404 Not Found'.

The headers Argument of start_response

The headers 参数传递给 start_response callback必须是Python list of tupleS,每个元组组成为 (header_name header_value). 每个头文件的名称和值都必须是字符串(无论Python版本如何). 这是一个少见的类型重要的例子,因为这确实是WSGI规范所要求的.

这是一个有效的例子 header 参数可能看起来像:

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

headers = [('Content-Type', 'application/json'),
           (内容长度,str (len (response_body)))

HTTP报头不区分大小写, 如果我们正在编写一个符合WSGI的web服务器, 这是检查这些头时要注意的事情. 此外,应用程序提供的标头列表不应该是详尽无遗的. 在将响应发送回客户端之前,服务器有责任确保所有必需的HTTP头都存在, 填写应用程序未提供的任何标头.

The exc_info Argument of start_response

The start_response 回调应该支持第三个参数 exc_info,用于错误处理. 正确使用和实现这个参数对于生产web服务器和应用程序至关重要, 但这超出了本文的讨论范围.

关于它的更多信息可以在WSGI规范中获得。 here.

The start_response Return Value – The write Callback

为了向后兼容,实现WSGI的web服务器应该返回一个 write callable. 这个回调应该允许应用程序将主体响应数据直接写回客户端, 而不是通过迭代器将其返回给服务器.

尽管它存在, 这是一个过时的接口,新的应用程序应该避免使用它.

生成响应体

实现WSGI的应用程序应该通过返回一个可迭代对象来生成响应体. 对于大多数应用程序,响应体不是很大,很容易装入服务器的内存中. 在这种情况下,最有效的发送方式是一次性发送,使用一个单元素可迭代对象. In special cases, 在这种情况下,将整个身体装入内存是不可行的, 应用程序可以通过这个可迭代接口部分地返回它.

在Python 3中,Python 2和Python 3的WSGI之间只有很小的区别, 响应体表示为 bytes objects; in Python 2, the correct type for this is str.

将UTF-8字符串转换为 bytes or str is an easy task:

  • 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服务器的情况下测试应用程序

通过理解这个简单的界面, 我们可以很容易地创建脚本来测试我们的应用程序,而不需要启动服务器.

以这个小脚本为例:

从io导入BytesIO

Def (app, path = '/', query = '/'):
    Response_status = []
    Response_headers = []

    Def start_response(地位、头):
        status = status.split(' ', 1)
        response_status.追加(int(状态[0]),状态[1]))
        response_headers.追加(dict类型(标题))

    environ = {
        “HTTP_ACCEPT”:“* / *”,
        'HTTP_HOST': '127.0.0.1:8000',
        “HTTP_USER_AGENT”:“TestAgent / 1.0',
        'PATH_INFO': path,
        “QUERY_STRING”:查询,
        “REQUEST_METHOD”:“得到”,
        “SERVER_NAME”:127年.0.0.1',
        “SERVER_PORT”:“8000”,
        “SERVER_PROTOCOL”:“HTTP / 1.1',
        “SERVER_SOFTWARE”:“TestServer / 1.0',
        'wsgi.错误:BytesIO (b”),
        'wsgi.输入:BytesIO (b”),
        'wsgi.多进程”:假的,
        'wsgi.多流”:假的,
        'wsgi.run_once': False,
        'wsgi.url_scheme”:“http”,
        'wsgi.version': (1, 0),
    }

    Response_body = app(environ, start_response)
    merged_body = ''.join((x.解码('utf-8')在response_body中的x)

    如果hasattr(response_body, 'close'):
        response_body.close()

    返回{'状态':response_status[0],
            “标题”:response_headers [0],
            “身体”:merged_body}

In this way, we might, for example, 在我们的应用中初始化一些测试数据和模拟模块, and make GET 调用,以测试它是否相应响应. 我们可以看到它不是一个真正的web服务器, 但是通过为应用程序提供一个 start_response 回调和一个带有环境变量的字典. 在请求的最后, 它使用响应体迭代器并返回包含其所有内容的字符串. 可以为不同类型的HTTP请求创建类似的方法(或通用方法).

Wrap-Up

WSGI是几乎所有Python web框架的关键部分.

In this article, 我们还没有讨论WSGI如何处理文件上传, 因为这可以被认为是一个更“高级”的功能, 不适合作为介绍性文章. 如果你想了解更多,可以看看 PEP-3333节中提到的文件处理.

我希望本文有助于更好地理解Python如何与web服务器通信, 并允许开发人员以有趣和创造性的方式使用这个界面.

Acknowledgments

我要感谢我的编辑 Nick McCrea 谢谢你帮我写这篇文章. 由于他的工作,原文变得更加清晰,一些错误没有被纠正.

聘请Toptal这方面的专家.
Hire Now
莱安德罗·利马的头像
Leandro Lima

Located in 圣保罗州 -巴西圣保罗州

Member since November 19, 2015

About the author

Leandro有15年以上的IT/开发经验. 自2013年开始使用Python,他喜欢构建高效且具有成本效益的系统.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Embraer

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.