欢迎光临!
若无相欠,怎会相见

源码阅读 – Flask v0.1 [02] Flask 与 WSGI 相关知识

序言

上篇文章写了源码阅读的准备工作,详情请看 源码阅读 – Flask 0.1版 阅读准备 – 01 ,这篇文章写 WSGI 的相关知识,可以方便阅读 Flask 0.1 源码 。

Flask 与 WSGI

WSGI 指 Python Web Server Gateway Interface , 它是为了让 Web 服务器与 Python 程序能够进行数据交流而定义的一套接口标准 / 规范 。

Werkzeug 是一个 WSGI 工具库 , 它是 Flask 的核心拓展。

WSGI 的具体定义在 PEP 333 中可以看到 。 WSGI 的新版本在 PEP 3333 中发布 , 新版本主要增加了 Python 3 支持  。

客户端和服务器端进行沟通遵循了 HTTP 协议 , 可以说 HTTP 就是它们之间沟通的语言 。 从 HTTP 请求到我们的 Web 程序之间 , 还有另外一个转换过程 —— 从 HTTP 报文到 WSGI 规定的数据格式 。 WSGI 则可以视为 WSGI 服务器和我们的 Web 程序进行沟通的语言 。 WSGI 是开发 Python Web 程序的标准 , 所有的 Python Web 框架都需要按照 WSGI 的规范来编写程序 。

WSGI 程序

根据 WSGI 的规定 , Web 程序 (或被称为 WSGI 程序) 必须是一个可调用对象 (callable object) 。 这个可调用对象接收两个参数 :

  • environ : 包含了请求的所有信息的字典 。
  • start_response : 需要在可调用对象中调用的函数 , 用来发起响应 , 参数是状态码 、 响应头部等 。

WSGI 服务器会在调用这个可调用对象时传入这两个参数 。 另外 , 这个可调用对象还要返回一个可迭代 (iterable) 的对象 。 这个可调用对象可以是函数 、 方法 、 类或是实现了 __call__ 方法的类实例 , 下面我们分别借助简单的实例来了解最主要的两种实现 : 函数和类 。

from wsgiref.simple_server import make_server

def hello(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/html')]
    start_response(status, response_headers)
    name = environ['PATH_INFO'][1:] or 'web'
    return [b'<h1>Hello, %s!</h1>' % name]


class AppClass:

    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response

    def __iter__(self):
        status = '200 OK'
        response_headers = [('Content-type', 'text/html')]
        self.start(status, response_headers)
        yield b'<h1>Hello, Web!</h1>'


server1 = make_server('localhost', 5000, hello)
server2 = make_server('localhost', 5000, AppClass)
server1.serve_forever()
server2.serve_forever()

这里的 hello() 函数就是我们的可调用对象 , 也就是我们的 Web 程序 。 hello() 的末尾返回一行问候字符串 , 注意这是一个列表 。

根据 WSGI 的定义 , 请求和响应的主体应该为字节串 (bytestrings) , 即 Python 2 中的 str 类型 。 在 Python 3 中字符串默认为 unicode 类型 , 因此需要在字符串前添加 b 前缀 , 将字符串声明为 bytes 类型 。 这里为了兼容两者 , 统一添加了 b 前缀 。

类形式的可调用对象如代码中的 AppClass , 注意 , 类中实现了 __iter__ 方法 (类被迭代时将调用这个方法) , 它返回 yield 语句 。 如果想以类的 实例 作为 WSGI 程序 , 那么这个类必须实现 __call__ 方法 。

在上面创建的两个简单的 WSGI 程序 , 你应该感觉很熟悉吧 ! 事实上 , 这两个程序的实际功能和书开始介绍的 Flask 程序 hello 完全相同 。

Flask 也是 Python Web 框架 , 自然也要遵循 WSGI 规范 , 所以 Flask 中也会实现类似的 WSGI 程序 , 只不过对请求和响应的处理要丰富完善得多 。 在 Flask 中 , 这个可调用对象就是我们的程序实例 app , 我们创建 app 实例时调用的 Flask 类就是另一种可调用对象形式 —— 实现了 __call__ 方法的类 :

class Flask(_PackageBoundObject):
    ...
    def wsgi_app(self, environ, start_response):
        with self.request_context(environ):
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
        response = self.make_response(rv)
        response = self.process_response(response)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
    """Shortcut for :attr:`wsgi_app`."""
        return self.wsgi_app(environ, start_response)

这个 __call__ 方法内部调用了 wsgi_app() 方法 , 请求进入和响应的返回就发生在这里 , WSGI 服务器通过调用这个方法来传入请求数据 , 获取返回的响应 , 后面会详细介绍 。

WSGI 服务器

程序编写好了 , 现在需要一个 WSGI 服务器来运行它 。 作为 WSGI 服务器的实现示例 , Python 提供了一个 wsgiref 库 , 可以在开发时使用 。 以 hello() 函数为例 , 在函数定义的下面添加如下代码 。

from wsgiref.simple_server import make_server

def hello(environ, start_response):
    ...

server = make_server('localhost', 5000, hello)
server.serve_forever()

这里使用 make_server(host, port, application) 方法创建了一个本地服务器 , 分别传入主机地址 、 端口和可调用对象 (即 WSGI 程序) 作为参数 。 最后使用 serve_forever() 方法运行它 。

WSGI 服务器启动后 , 它会监听本地机的对应端口 (我们设置的 5000) 。 当接收到请求时 , 它会把请求报文解析为一个 environ 字典 , 然后调用 WSGI 程序提供的可调用对象 , 传递这个字典作为参数 , 同时传递的另一个参数是一个 start_response 函数 。 目前对于 start_response 函数有些不太理解 。

在命令行使用 Python 解释器执行 hello.py , 这会启动我们创建的 WSGI 服务器 :

python hello.py

然后像以前一样在浏览器中访问 http://localhost:5000 时 , 这个 WSGI 服务器接收到这个请求 , 接着调用 hello() 函数 , 并传递 environ 和 start_response 参数 , 最后把 hello() 函数的返回值处理为 HTTP 响应返回给客户端 。 这一系列工作完成后 , 我们就会在浏览器看到一行 “Hello,Web!” 。

下面是这个程序的变式 , 通过从 environ 字典获取请求 URL 来修改响应的内容 。

def hello(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/html')]
    start_response(status, response_headers)
    name = environ['PATH_INFO'][1:] or 'web'
    return [b'<h1>Hello, %s!</h1>' % name]

从 environ 字典里获取路径中根地址后的字符作为名字 : environ[‘PATH_INFO’][1:] , 然后插入到响应的字符串里 。 这时在浏览器中访问 localhost:5000/Grey , 则会看到浏览器显示一行 “Hello,Grey!” 。

到此 , 大概了解了 wsgi 的相关信息 , 如下是我的总结 :

  • 函数式 : 接收两个参数 , 并返回一个 list
  • 类形式 : 如果以类实例作为 WSGI 程序 , 类必须实现 __call__ 方法

中间件

WSGI 允许使用中间件 (Middleware) 包装 (wrap) 程序 , 为程序在被调用前添加额外的设置和功能 。 当请求发送来后 , 会先调用包装在可调用对象外层的中间件 。 这个特性经常被用来解耦程序的功能 , 这样可以将不同功能分开维护 , 达到分层的目的 , 同时也根据需要嵌套 。 如下代码是一个简单的例子 。

from wsgiref.simple_server import make_server

def hello(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/html')]
    start_response(status, response_headers)
    return [b'<h1>Hello, web!</h1>']

class MyMiddleware(object):

    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        def custom_start_response(status, headers, exc_info=None):
            headers.append(('A-CUSTOM-HEADER', 'Nothing'))
            return start_response(status, headers)
        return self.app(environ, custom_start_response)

wrapped_app = MyMiddleware(hello)
server = make_server('localhost', 5000, wrapped_app)
server.serve_forever()

中间件接收可调用对象作为参数 。 这个可调用对象也可以是被其他中间件包装的可调用对象 。 中间件可以层层叠加 , 形成一个 “中间件堆栈” , 最后才会调用到实际的可调用对象 。

使用类定义的中间件必须实现 __call__ 方法 , 接收 environ 和 start_response 对象作为参数 , 最后调用传入的可调用对象 , 并传递这两个参数 。 这个 MyMiddleware 中间件其实并没有做什么 , 只是向首部添加了一个无意义的自定义字段 。 最后传入可调用对象 hello 函数来实例化这个中间件 , 获得包装后的程序实例 wrapped_app 。

因为 Flask 中实际的 WSGI 可调用对象是 Flask.wsgi_app() 方法 , 因此 , 如果我们自己实现了中间件 , 那么最佳的方式是嵌套在这个 wsgi_app 对象上 , 比如 :

class MyMiddleware(object):
    pass

app = Flask(__name__)
app.wsgi_app = MyMiddleware(app.wsgi_app)

作为 WSGI 工具集 , Werkzeug 内置了许多方便的中间件 , 可以用来为程序添加额外的功能 。 比如 , 能够为程序添加性能分析器的 werkzeug.contrib.profiler.ProfilerMiddleware 中间件 , 这个中间件可以在处理请求时进行性能分析 , 作用和 Flask-DebugToolbar 提供的分析器基本相同 ; 另外 , 支持多应用调度的 werkzeug.wsgi.DispatcherMiddleware 中间件则可以让你将多个 WSGI 程序作为一个 “程序集” 同时运行 , 你需要传入多个程序实例 , 并为这些程序设置对应的 URL 前缀或子域名来分发请求 。

结语

本文就先到此结束了,后面接着写。

如有错误,敬请指出,感谢指正!   — 2021-04-11  20:43:42

赞(0) 打赏
转载请注明:飘零博客 » 源码阅读 – Flask v0.1 [02] Flask 与 WSGI 相关知识
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

欢迎光临