说说我对 WSGI 的理解

先说下 WSGI 的表面意思,Web Server Gateway Interface 的缩写,即 Web 服务器网关接口。python

以前不知道 WSGI 意思的伙伴,看了上面的解释后,我估计也仍是不清楚,因此下面结合实际场景说明,先让你们有个大体的认识。最后咱们再本身实现一个,加深对 WSGI 的理解。git

咱们如今使用 Python 编写 Web 应用,能够用比较流行的 Flask、Django 框架,也能够按本身的想法直接写一个。可选的服务器软件也特别多,好比常见的有 Apache、Nginx、IIS 等,除此外,也有不少小众的软件。可是,如今问题来了,我该怎么部署?在没有 WSGI 规范以前,一个服务器调度 Python 应用是用这种方式,另外一款服务器使用的是那种方式,这样的话,编写出来的应用部署时只能选择局限的某个或某些服务器,达不到通用的效果。github

注意:下文中的代码基于 Python 3.6 编写。flask

假若有这么一个服务器后端

wsgi/server.pybash

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    response = b""" HTTP/1.1 200 OK Hello, World! """
    client_connection.sendall(response)
    client_connection.close()
复制代码

实现比较简单,就是监听 8080 端口,若是有请求在终端进行打印,并返回 Hello, World! 的响应。服务器

终端中启动服务器app

➜  wsgi python server.py
Serving HTTP on 0.0.0.0 port 8080 ...
复制代码

再开一个终端,请求下框架

➜  ~ curl 127.0.0.1:8080
HTTP/1.1 200 OK

Hello, World!
复制代码

说明服务器工做正常。curl

另外有一个 Web 应用

wsgi/app.py

# coding=utf-8


def simple_app():
    return b'Hello, World!\r\n'
复制代码

如今要部署(也就是让这个总体跑起来),简单粗暴的作法就是在服务器里面直接调用 app 中相应的方法。就像这样

wsgi/server2.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    from app import simple_app
    response = 'HTTP/1.1 200 OK\r\n\r\n'
    response = response.encode('utf-8')
    response += simple_app()

    client_connection.sendall(response)
    client_connection.close()
复制代码

运行脚本

注意:由于使用端口相同的缘故,请先关闭上次的脚本,而后再执行,否则会因为端口冲突而报错。

➜  wsgi python server2.py
Serving HTTP on 0.0.0.0 port 8080 ...
复制代码

而后请求一下看看效果

➜  ~ curl 127.0.0.1:8080
Hello, World!
复制代码

嗯,能够了。可是,上面的服务器和应用总体是跑起来了,那么我换一个服务器或者应用呢。因为服务器与应用之间怎么交互彻底没有规范,好比服务器应该如何把请求信息传给应用,应用处理完毕后又怎么告诉服务器开始返回响应,若是都是各搞各的,服务器须要定制应用,应用也要定制服务器,这要一个应用能跑起来也太麻烦了点吧。

因此,WSGI 的出现就是为了解决上面的问题,它规定了服务器怎么把请求信息告诉给应用,应用怎么把执行状况回传给服务器,这样的话,服务器与应用都按一个标准办事,只要实现了这个标准,服务器与应用随意搭配就能够,灵活度大大提升。

WSGI 规范了些什么,下图能很直观的说明。

图片来自 https://www.toptal.com

首先,应用必须是一个可调用对象,能够是函数,也能够是实现了 __call__() 方法的对象。

每收到一个请求,服务器会经过 application_callable(environ, start_response) 调用应用。

应用在处理完毕准备返回数据的时候,先调用服务传给它的函数 start_response(status, headers, exec_info),最后再返回可迭代对象做为数据。(不理解可迭代对象的伙伴能够看下我以前的一篇文章《搞清楚Python的迭代器、可迭代对象、生成器》)

其中,environ 必须是一个字典,包括了请求的相关信息,好比请求方式、请求路径等等,start_response 是应用处理完毕后,须要调用的函数,用于告诉服务设置响应的头部信息或错误处理等等。

status 必须是 999 Message here 这样的字符串,好比 200 OK404 Not Found 等,headers 是一个由 (header_name, header_value) 这样的元祖组成的列表,最后一个 exec_info 是可选参数,通常在应用出现错误的时候会用到。

知道了 WSGI 的大体概念,下面咱们来实现一个。

首先是应用

wsgi/wsgi_app.py

# coding=utf-8


def simple_app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [f'Request {environ["REQUEST_METHOD"]}'
            f' {environ["PATH_INFO"]} has been'
            f' processed\r\n'.encode('utf-8')]
复制代码

这里定义了一个函数(可调用对象),它可使用服务器传给它的请求相关的内容 environ,这里使用了 REQUEST_METHOD 和 PATH_INFO 信息。在返回以前调用了 start_response,方便服务器设置一些头部信息。

而后是服务器

wsgi/wsgi_server.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    headers_set = None

    def start_response(status, headers):
        global headers_set
        headers_set = [status, headers]

    method, path, _ = request.split(b' ', 2)
    environ = {'REQUEST_METHOD': method.decode('utf-8'),
               'PATH_INFO': path.decode('utf-8')}
    from wsgi_app import simple_app
    app_result = simple_app(environ, start_response)

    response_status, response_headers = headers_set
    response = f'HTTP/1.1 {response_status}\r\n'
    for header in response_headers:
        response += f'{header[0]}: {header[1]}\r\n'
    response += '\r\n'
    response = response.encode('utf-8')
    for data in app_result:
        response += data

    client_connection.sendall(response)
    client_connection.close()
复制代码

服务器监听相关代码没怎么变化,主要是处理请求的时候有些不一样。

首先定义了 start_response(status, headers) 函数,自身并不会调用。

而后调用应用,将当前的请求信息 environ 和上面的 start_response 函数传给它,让其本身决定使用什么请求信息以及在处理完成准备返回数据以前调用 start_response 设置头部信息。

好了,启动服务器后(即执行服务器代码,和以前的相似,这里不赘述),而后请求看看结果

➜  ~ curl 127.0.0.1:8080/user/1
Request GET /user/1 has been processed
复制代码

嗯,程序是正常的。

上面为了说明,代码耦合性较大,若是服务器须要更换应用的话,还得修改服务器代码,这显然是有问题的。如今原理差很少说清楚了,咱们把代码优化下

wsgi/wsgi_server_oop.py

# coding=utf-8

import socket
import sys


class WSGIServer:
    def __init__(self):
        self.listener = socket.socket()
        self.listener.setsockopt(socket.SOL_SOCKET,
                                 socket.SO_REUSEADDR, 1)
        self.listener.bind(('0.0.0.0', 8080))
        self.listener.listen(1)
        print('Serving HTTP on 0.0.0.0'
              ' port 8080 ...')
        self.app = None
        self.headers_set = None

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

    def start_response(self, status, headers):
        self.headers_set = [status, headers]

    def serve_forever(self):
        while True:
            listener = self.listener
            client_connection, client_address = \
                listener.accept()
            print(f'Server received connection'
                  f' from {client_address}')
            request = client_connection.recv(1024)
            print(f'request we received: {request}')

            method, path, _ = request.split(b' ', 2)
            # 为简洁的说明问题,这里填充的内容有些随意
            # 若是有须要,能够自行完善
            environ = {
                'wsgi.version': (1, 0),
                'wsgi.url_scheme': 'http',
                'wsgi.input': request,
                'wsgi.errors': sys.stderr,
                'wsgi.multithread': False,
                'wsgi.multiprocess': False,
                'wsgi.run_once': False,
                'REQUEST_METHOD': method.decode('utf-8'),
                'PATH_INFO': path.decode('utf-8'),
                'SERVER_NAME': '127.0.0.1',
                'SERVER_PORT': '8080',
            }
            app_result = self.app(environ, self.start_response)

            response_status, response_headers = self.headers_set
            response = f'HTTP/1.1 {response_status}\r\n'
            for header in response_headers:
                response += f'{header[0]}: {header[1]}\r\n'
            response += '\r\n'
            response = response.encode('utf-8')
            for data in app_result:
                response += data

            client_connection.sendall(response)
            client_connection.close()


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit('Argv Error')
    app_path = sys.argv[1]
    module, app = app_path.split(':')
    module = __import__(module)
    app = getattr(module, app)

    server = WSGIServer()
    server.set_app(app)
    server.serve_forever()
复制代码

基本原理没变,只是使用了面向对象的方式修改了下原来的代码,同时 environ 添加了一些必要的环境信息。

可使用之前的应用

➜  wsgi python wsgi_server_oop.py wsgi_app:simple_app
Serving HTTP on 0.0.0.0 port 8080 ...
复制代码

请求

➜  ~ curl 127.0.0.1:8080/user/1
Request GET /user/1 has been processed
复制代码

获得和以前相同的结果。

Flask 应用能行吗?来试一试,先新建一个

wsgi/flask_app.py

# coding=utf-8

from flask import Flask
from flask import Response

flask_app = Flask(__name__)


@flask_app.route('/user/<int:user_id>',
                 methods=['GET'])
def hello_world(user_id):
    return Response(
        f'Get /user/{user_id} has been'
        f' processed in flask app\r\n',
        mimetype='text/plain'
    )
复制代码

从新启动服务器

➜  wsgi python wsgi_server_oop.py flask_app:flask_app
Serving HTTP on 0.0.0.0 port 8080 ...
复制代码

请求

➜  ~ curl 127.0.0.1:8080/user/1
Get /user/1 has been processed in flask app
复制代码

由于 Flask 也是遵照 WSGI 规范的,因此执行也没有问题。

至此,一个粗略的 WSGI 规范就实现了,虽然说代码不优雅,一些核心的东西仍是体现出来了。不过毕竟忽略了不少东西,好比错误处理等,要在生产环境中使用的话还远远不够,想知道得更全面的伙伴能够去看看 PEP 3333。

目前流行的 Web 应用框架好比 Django、Bottle 等,服务器 Apahce、Nginx、Gunicorn 等也都支持这个规范。所以,框架和应用随意搭配基本没什么问题。

参考

关注公众号「小小后端」阅读更多精彩文章。

相关文章
相关标签/搜索