Python WSGI 初探

标签: Python Web | 发表时间:2017-02-07 03:00 | 作者:Liao
出处:http://liaoph.com/

WSGI 是什么

在构建 Web 应用时,通常会有 Web Server 和 Application Server 两种角色。其中 Web Server 主要负责接受来自用户的请求,解析 HTTP 协议,并将请求转发给 Application Server,Application Server 主要负责处理用户的请求,并将处理的结果返回给 Web Server,最终 Web Server 将结果返回给用户。

由于有很多动态语言和很多种 Web Server,他们彼此之间互不兼容,给程序员造成了很大的麻烦。因此就有了 CGI/FastCGI 协议,定义了 Web Server 如何通过输入输出与 Application Server 进行交互,将 Web 应用程序的接口统一了起来。

那么 WSGI 又是什么呢?WSGI(Web Server Gateway Interface) 是 Python PEP333中提出的一个 Web 开发统一规范(后更新为 PEP3333)。虽然前面说过,有了 CGI/FastCGI 协议,不同的 Application Server 和 Web Server 之前能够以相同的规范进行交互。但是,Web 应用的开发通常都会涉及到 Web 框架的使用,各个 Web 框架内部由于实现不同相互不兼容,给用户的学习,使用和部署造成了很多麻烦。WSGI 就是一个用于统一 Application Server 和 Web 框架之间交互和调用的规范。WSGI 约定了怎么调用 web 应用程序的代码,web 应用程序需要符合什么样的规范,只要 web 应用程序和 Application Server 都遵守 WSGI 协议,那么,web 应用程序和 Application Server 就可以随意的组合,开发者也可以编写具有通用功能的 WSGI middleware,可以被加载到任务 WSGI Server 中。

WSGI 标准简介

WSGI 标准中主要定义了两种角色:

  • “server” 或 “gateway” 端
  • “application” 或 “framework” 端

这两种角色的调用关系如下图:

这里可以看到,WSGI 服务器需要调用应用程序的一个可调用对象,这个可调用对象(callable object)可以是一个函数,方法,类或者可调用的实例,总之是可调用的。

下面是一个 callable object 的示例,这里的可调用对象是一个函数:

   def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello World']

这里,我们首先要注意,这个对象接收两个参数:

  • environ:请求的环境变量,它是一个字典,包含了 HTTP 请求的首部,方法等信息
  • start_response:一个回调函数,在返回内容之前必须先调用这个回掉函数

上面的 start_response 这个回调函数的作用是用于让 WSGI Server 返回响应的 HTTP 首部和 HTTP 状态码。这个函数有两个必须的参数,返回的状态码和返回的响应首部组成的元祖列表。返回状态码和首部的这个操作始终都应该在响应 HTTP body 之前执行。

还需要注意的是,最后的返回结果,应该是一个可迭代对象,这里是将返回的字符串放到列表里。如果直接返回字符串可能导致 WSGI 服务器对字符串进行迭代而影响响应速度。

当然,这个函数是一个最简单的可调用对象,它也可以是一个类或者可调用的类实例。

WSGI Server & Web 框架

首先 Web 框架是什么?

基于之前对 WSGI 的简单介绍,一个 Web 框架至少要能够 WSGI Server 提供一个 callable object,每一次请求 WSGI Server 都会调用这个 callable object 来完成。一般来说,一个 Web 框架还需要对开发者提供如下功能:

  • URL 的匹配和路由转发
  • Request/Response 的封装
  • 错误、异常的处理
  • session 的支持
  • 模板的支持
  • 数据库 ORM 的支持
  • 一些高级功能如 Form 表单对象,admin 后台管理的支持等

以上功能都是 Web 框架对开发者提供的接口,而 WSGI 服务器只关心需要调用的 callable object. Python 的 wsgiref 库就实现了一个简单的 WSGI 服务器,我们可以借助这个 WSGI 服务器来测试我们的 application.

Werkzeug

Werkzeug 是一个 WSGI 工具包,它可以作为 web 框架的底层库,当 web 框架的开发者更轻松的开发 WSGI Web 框架。在之前,我们使用这样的方式构造我们的 WSGI 应用:

   def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Hello World!']

Werkzeug允许我们以一个轻松的方式访问数据,提供了更好的方法来创建响应。如下所示:

   from werkzeug.wrappers import Response

 def application(environ, start_response):
    response = Response('Hello World!', mimetype='text/plain')
    return response(environ, start_response)

wsgiref

我们可以借助 python 的 wsgiref 库运行一个 WSGI 服务器(当然这个 WSGI 服务器同时也是 Web 服务器),用它来运行我们的 application.

   #! /usr/bin/env python

from wsgiref.simple_server import make_server

def application (environ, start_response):

    # 这里从 environ 字典中获取所有的变量值
    response_body = [
        '%s: %s' % (key, value) for key, value in sorted(environ.items())
    ]
    response_body = '\n'.join(response_body)

    status = '200 OK'
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)

    return [response_body]

# 启动 WSGI  服务器
httpd = make_server (
    'localhost',
    8051,
    application # 这里指定我们的 application object)

# 开始处理请求
httpd.handle_request()

运行上面的程序,并访问 http://localhost:8051,将返回此次请求所有的首部信息。

这里,我们利用 environ 字典,获取了请求中所有的变量信息,构造成相应的内容返回给客户端。

environ 这个参数中包含了请求的首部,URL,请求的地址,请求的方法等信息。可以参考 PEP3333 来查看 environ 字典中必须包含哪些 CGI 变量。

WSGI Server

上面我们使用 wsgiref 库中的 WSGI 服务器来运行我们的程序,其实,知道了 WSGI 标准,我们可以自己实现一个 WSGI 服务器,它需要实现如下基本功能:

  • 监听端口,接收请求
  • 解析 HTTP 协议
  • 调用 application,构造响应首部
  • 将响应返回给客户端

这里是一个 WSGI Server 的范例:

   import socket
import StringIO
import sys


class WSGIServer(object):

    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM
    request_queue_size = 1

    def __init__(self, server_address):
        # 创建监听端口
        self.listen_socket = listen_socket = socket.socket(
            self.address_family,
            self.socket_type
        )
        listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        listen_socket.bind(server_address)
        listen_socket.listen(self.request_queue_size)
        # 获取主机名和端口信息
        host, port = self.listen_socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port
        self.headers_set = []

    def set_app(self, application):
        self.application = application

    def serve_forever(self):
        listen_socket = self.listen_socket
        while True:
            # 处理客户端连接
            self.client_connection, client_address = listen_socket.accept()
            self.handle_one_request()

    def handle_one_request(self):
        self.request_data = request_data = self.client_connection.recv(1024)
        # 打印接收的数据
        print(''.join(
            '< {line}\n'.format(line=line)
            for line in request_data.splitlines()
        ))

        self.parse_request(request_data)

        # 使用请求的信息构造 CGI 环境变量
        env = self.get_environ()

        # 调用 WSGI callable object
        result = self.application(env, self.start_response)

        # 将结果响应给客户端
        self.finish_response(result)

    def parse_request(self, text):
        request_line = text.splitlines()[0]
        request_line = request_line.rstrip('\r\n')
        # 解析 HTTP request line
        (self.request_method,  # GET
         self.path,            # /hello
         self.request_version  # HTTP/1.1
         ) = request_line.split()

    def get_environ(self):
        # 构造响应的 WSGI 环境变量
        env = {
        		'wsgi.version': (1, 0),
        		'wsgi.url_scheme': 'http',
        		'wsgi.input': StringIO.StringIO(self.request_data),
        		'wsgi.errors': sys.stderr,
        		'wsgi.multithread': False,
        		'wsgi.run_once': False,
        		'REQUEST_METHOD': self.request_method,
        		'PATH_INFO': self.path,
        		'SERVER_NAME': self.server_name,
        		'SERVER_PORT': str(self.server_port),
        	}
        return env

    def start_response(self, status, response_headers, exc_info=None):
        # 构造响应的首部和状态码
        server_headers = [
            ('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'),
            ('Server', 'WSGIServer 0.2'),
        ]
        self.headers_set = [status, response_headers + server_headers]

    def finish_response(self, result):
        try:
            status, response_headers = self.headers_set
            response = 'HTTP/1.1 {status}\n'.format(status=status)
            for header in response_headers:
                response += '{0}: {1}\n'.format(*header)
            response += '\n'
            for data in result:
                response += data
            # Print formatted response data a la 'curl -v'
            print(''.join(
                '> {line}\n'.format(line=line)
                for line in response.splitlines()
            ))
            self.client_connection.sendall(response)
        finally:
            self.client_connection.close()


SERVER_ADDRESS = (HOST, PORT) = '', 8888


def make_server(server_address, application):
    server = WSGIServer(server_address)
    server.set_app(application)
    return server


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit('请提供可用的wsgi应用程序, 格式为: module:callable')
    app_path = sys.argv[1]
    module, application = app_path.split(':')
    module = __import__(module)
    application = getattr(module, application)
    httpd = make_server(SERVER_ADDRESS, application)
    print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT))
    httpd.serve_forever()

上面的 WSGI 服务器运行过程为:

  1. 初始化,创建套接字,绑定端口
  2. 接收客户端请求
  3. 解析 HTTP 协议
  4. 构造 WSGI 环境变量( environ
  5. 调用 application
  6. 回调函数 start_response 设置好响应的状态码和首部
  7. 返回信息

本文对 WSGI 进行了简单的介绍,关于更多 WSGI 的细节,可以阅读 PEP3333

参考:

相关 [python wsgi] 推荐:

Python WSGI 初探

- - 坚实的幻想
在构建 Web 应用时,通常会有 Web Server 和 Application Server 两种角色. 其中 Web Server 主要负责接受来自用户的请求,解析 HTTP 协议,并将请求转发给 Application Server,Application Server 主要负责处理用户的请求,并将处理的结果返回给 Web Server,最终 Web Server 将结果返回给用户.

dropbox讲python

- chuang - Initiative
dropbox定制优化CPython虚拟机,自己搞了个malloc调度算法. 那个 !!!111cos(0). 期待这次PyCon China 2011.

Python调试

- - 企业架构 - ITeye博客
原文地址: http://blog.csdn.net/xuyuefei1988/article/details/19399137. 1、下面网上收罗的资料初学者应该够用了,但对比IBM的Python 代码调试技巧:. IBM:包括 pdb 模块、利用 PyDev 和 Eclipse 集成进行调试、PyCharm 以及 Debug 日志进行调试:.

Python实现逻辑回归(Logistic Regression in Python)

- - 神刀安全网
Logistic Regression in Python ,作了中文翻译,并相应补充了一些内容. 本文并不研究逻辑回归具体算法实现,而是使用了一些算法库,旨在帮助需要用Python来做逻辑回归的训练和预测的读者快速上手. 逻辑回归是一项可用于预测二分类结果(binary outcome)的统计技术,广泛应用于金融、医学、犯罪学和其他社会科学中.

python 下载文件

- Eric - python相关的python 教程和python 下载你可以在老王python里寻觅
之前给大家分享的python 多线程抓取网页,我觉的大家看了以后,应该会对python 抓取网页有个很好的认识,不过这个只能用python 来抓取到网页的源代码,如果你想用做python 下载文件的话,上面的可能就不适合你了,最近我在用python 做文件下载的时候就遇到这个问题了,不过最终得以解决,为了让大家以后碰过这个问题有更好的解决办法,我把代码发出来:.

python代码调试

- - 阿里古古
【转自: http://blog.csdn.net/luckeryin/article/details/4477233】. 本文讨论在没有方便的IDE工具可用的情况下,使用pdb调试python程序. 例如,有模拟税收计算的程序:. debug_demo函数计算4500的入账所需的税收. 在需要插入断点的地方,加入红色部分代码:如果_DEBUG值为True,则在该处开始调试(加入_DEBUG的原因是为了方便打开/关闭调试).

python编程规范

- - 互联网 - ITeye博客
@FileName: @Author:[email protected] @Create date: @description:用一行文字概述模块或脚本,用句号结尾. 不影响编码的效率,不与大众习惯冲突.. 使代码的逻辑更清晰,更易于理解..   *所有的 Python 脚本文件都应在文件头标上如下标识或其兼容格式的标识.

Python 编程速成

- - SegmentFault 最新的文章
本文首发微信公众号:前端先锋. 欢迎关注,每天都给你推送新鲜的前端技术文章. Python是一种非常流行的脚本语言,而且功能非常强大,几乎可以做任何事情,比如爬虫、网络工具、科学计算、树莓派、Web开发、游戏等各方面都可以派上用场. 同时无论在哪种平台上,都可以用 Python 进行系统编程. 机器学习可以用一些 Python 库来实现,比如人工智能常用的 TensorFlow.

《Dive into Python 3》中文版

- hama - Wow! Ubuntu
Dive Into Python 是一份很知名的 Python 入门教程,由 Mark Pilgrim 编写,用户可以免费获取电子版本,而中文版则由啄木鸟社区翻译发布 [ 英文版 / 中文版 ]. 前阵子,Mark Pilgrim 又发布了 《Dive into Python 3》,此版本的内容涵盖了 Python 3 及其与 Python 2 的区别.

分享一套 python 试题

- eastxing - 赖勇浩的编程私伙局
赖勇浩(http://laiyonghao.com). 今天在 simple-is-better.com 看到一篇《Python 面试题集合》(http://simple-is-better.com/news/596),里面有一些很好的试题,如“Python是如何进行类型转换的. ”,也有一些让人吐血的试题,如“Python如何实现单例模式.