WSGIGateway

WSGIGateway 及 WSGI application

1 WSGI Gateway

众所周知,对于所有的 Web 应用(Web 框架),本质上其实就是一个 socket 服务端,用户的浏览器其实就是一个 socket 客户端。

  • 1、自己写 socket,自己处理请求

    • 代表框架是 Tornado(Tornado 有两种模式,可以通过修改配置让它使用自己写的 socket 也可以基于 wsgi)

  • 2、基于 wsgi(Web Server Gateway Interface WEB 服务网关接口),自己处理请求

    • 代表框架是 django

1.1 WSGI

在 Python 中,WSGI(Web Server Gateway Interface)定义了 Web 服务器与 Web 应用(或 Web 框架)之间的标准接口。在 WSGI 的规范下,各种各样的 Web 服务器和 Web 框架都可以很好的交互。

wsgi

目前咱们的 butterfly 也是基于 WSGI 写的 web 框架

1.1.1 WSGI 接口示例

WSGI 接口定义非常简单,它只要求 Web 开发者实现一个函数,就可以响应 HTTP 请求。我们来看一个最简单的 Web 版本的“Hello, web!”:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return '<h1>Hello, web!</h1>'

上面的 application() 函数就是符合 WSGI 标准的一个 HTTP 处理函数,它接收两个参数:

  • environ:一个包含所有 HTTP 请求信息的 dict 对象;

  • start_response:一个发送 HTTP 响应的函数。

在 application() 函数中,调用:

start_response('200 OK', [('Content-Type', 'text/html')])

就发送了 HTTP 响应的 Header,注意 Header 只能发送一次,也就是只能调用一次 start_response() 函数。start_response() 函数接收两个参数,一个是 HTTP 响应码,一个是一组 list 表示的 HTTP Header,每个 Header 用一个包含两个 str 的 tuple 表示。

通常情况下,都应该把 Content-Type 头发送给浏览器。其他很多常用的 HTTP Header 也应该发送。

然后,函数的返回值 '<h1>Hello, web!</h1>'将作为 HTTP 响应的 Body 发送给浏览器。

有了 WSGI,我们关心的就是如何从 environ 这个 dict 对象拿到 HTTP 请求信息,然后构造 HTML,通过 start_response() 发送 Header,最后返回 Body。

3.2 基于 WSGI 开发 Web 框架流程

  • 1 应用程序接收 environ、start_response 这两个参数,返回一个可迭代的对象

    • environ:一个包含所有 HTTP 请求信息的 dict 对象

    • start_response:一个发送 HTTP 响应的函数,该函数接收两个参数,一是 HTTP 响应码,二是 HttpHeader 元组列表

  • 2 将应用程序注册到服务器上

  • 3 服务器接收、解析请求,事先准备好 environ 和 start_response,然后调用注册的应用程序。

3.3 demo

python 内置了一个 WSGI 服务器,这个模块叫做 wsgiref,不过这个模块没有考虑运行效率,只是为了开发和测试使用。

# hello.py

def application(environ, start_response):
    # 添加业务代码逻辑处理
    # edit code

    start_response('200 OK', [('Content-Type', 'text/html')])
    result = 'Hello, WSGI!'
    # return result #python3 直接返回字符串会报错
    return [result.encode()] # 转成字节列表

# run.py

from wsgiref.simple_server import make_server  # 使用 python 内置的 WSGI 服务器
from hello import application

if __name__ == '__main__':
    httpd = make_server('0.0.0.0', 8585, application)  # 创建一个服务器,可接收 ip 地址,端口是 8585,应用程序是 application:
    print("SERVER RUN ON 8585")
    # 开始监听 HTTP 请求
    httpd.serve_forever()

2 WSGI App

2.1 WSGI App MVC 模型

2.4.1 Model 是核心

Model,也就是模型,是对现实的反映和简化。对问题的本质的描述就是 Model。解决问题就是给问题建立 Model。

当我们关注业务问题时,只有描述 “用户所关心的问题” 的代码才是 Model。当你的关注转移到其他问题时,Model 也会相应发生变化。

失去了解决特定问题这一语境,单谈 Model 没有意义。

可以说,View 和 Controller 是 Model 的一部分。

为什么人们要单独把 View 和 Controller 跟 Model 分开呢?

View 和 Controller 是外在的东西,只有 Model 是本质的东西。
外在的东西天天变化,但很少影响本质。把他们分开比较容易维护。

2.4.2 本框架中的 MVC

简单来说,Controller 和 View 分别是 Model 的 输入 和 输出。

  • Model : 也就是模型,是对现实的反映和简化

  • View : 也就是视图 / 视野,是你真正看到的,而非想象中的 Model

  • Controller : 也就是控制器,是你用来改变 Model 方式

View 建议在 butterfly-fe 实现,即前后端分离

使用 butterfly 框架时主要是编写 handler

                                               handler
                              +-----------------------------------------+
    +-----------+ user action | +------------+  update  +-------------+ |
    |   view    |-------------+>| controller |--------->|    model    | |
    |           |<------------+-|            |<---------|             | |
    +-----------+  update     | +------------+   notify +-------------+ |
                              +-----------------------------------------+

3 开发

接收 HTTP 请求,解析 HTTP 请求,发送 HTTP 请求等操作就交由 WSGI 服务器去完成,WSGI 接口只负责业务逻辑。

所以主要代码就是完成 WSGI 接口 中内容

即如何处理对应的 environ ,然后返回对应的结果

3.1 environ

如访问:curl "http://127.0.0.1:8585/echo?name=meetbill&age=16"

则对应的 environ 如下:

{
    'LESS': '-R',
    'SERVER_PROTOCOL': 'HTTP/1.1',                            #  HTTP 协议版本,HTTP/1.0 或者 HTTP/1.1
    'SERVER_SOFTWARE': 'WSGIServer/0.1 Python/2.7.14',
    'LC_CTYPE': 'zh_CN.UTF-8',
    'TERM_PROGRAM_VERSION': '3.2.3',
    'REQUEST_METHOD': 'GET',                                  # HTTP 请求方法,是个字符串,'GET'、 'POST'等,这个不可能是空字符串,所以是必须给出的。
    'LOGNAME': 'meetbill',
    'USER': 'meetbill',
    'QUERY_STRING': 'name=meetbill&age=16',                   # HTTP 请求中的查询字符串,URL 中?后面的内容
    'HOME': '/Users/meetbill',
    'ZSH': '/Users/meetbill/.oh-my-zsh',
    'wsgi.errors': < open file '<stderr>',mode 'w' at 0x1052e11e0 > ,
    'wsgi.url_scheme': 'http',
    '_': '/usr/local/bin/python',
    'SERVER_PORT': '8585',                                    # 端口
    'HTTP_CONNECTION': 'keep-alive',
    'CONTENT_LENGTH': '',                                     #  HTTP headers 中的 content-length 内容,记录着 post 请求,body 的长度
    'wsgi.input': < socket._fileobject object at 0x105749b50 > ,
    'HTTP_HOST': '127.0.0.1:8585',
    'SCRIPT_NAME': '',                                        # URL 请求中路径的开始部分,对应应用程序对象,这样应用程序就知道它的虚拟位置。如果该应用程序对应服务器的根目录的话,它可能是空字符串。
    'PATH_INFO': '/echo',                                     # HTTP 请求的 path 中剩余的部分,也就是 application 要处理的部分
                                                              # SCRIPT_NAME 和 PATH_INFO 不能同时为空字符串
    'wsgi.multithread': True,
    'wsgi.version': (1, 0),
    'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
    'GATEWAY_INTERFACE': 'CGI/1.1',
    'wsgi.run_once': False,
    'wsgi.multiprocess': False,
    'wsgi.file_wrapper': < class wsgiref.util.FileWrapper at 0x10583ebb0 > ,
    'REMOTE_ADDR': '192.168.21.21',                           # REMOTE_ADDR -- 客户端 IP
    ...
}
  • environ 字典中包含了在 CGI 规范中定义了的 CGI 环境变量,WSGI 变量和一些系统变量

    • 其中 wsgi. 开头的为 WSGI 变量

    • 大写字母的变量为 CGI 环境变量

解析 environ

使用 json.dumps(environ) 时,会报 "xx is not JSON serializable" 错误,也就是无法序列化某些对象。

如果想用 json 的话,可以通过如下方式,自定义序列化方法

from datetime import datetime
import json

class DateEncoder(json.JSONEncoder ):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.__str__()
        return json.JSONEncoder.default(self, obj)

json_1 = {'num':1112, 'date':datetime.now()}

CGI 相关变量

变量
说明

REQUEST_METHOD

POST,GET 等,HTTP 请求的动词标识

SERVER_PROTOCOL

服务器运行的 HTTP 协议。这里当是 HTTP/1.0.

PATH_INFO

附加的路径信息,由浏览器发出.

QUERY_STRING

请求 URL 的“?”后面的部分

CONTENT_TYPE

HTTP 请求中任何 Content-Type 字段的内容

CONTENT_LENGTH

标准输入口的字节数.

HTTP_变量

其他一些变量,例如 HTTP_ACCEPT,HTTP_REFERER 等

上述内容是动态开发的根基,只有根据上述内容才可以标准化的动态处理请求。

WSGI 定义变量

变量
说明

wsgi.version

WSGI 版本,要求是元组 (1,0), 标识 WSGI 1.0 协议

wsgi.url_scheme

表示调用应用程序的 URL 的协议,http 或 https

wsgi.input

类文件对象,读取 HTTP 请求体字节的输入流

wsgi.errors

类文件对象,写入错误输出的输出流

wsgi.multithread

如果是多线程,则设置为 True,否则为 False。

wsgi.multiprocess

如果是多进程,则设置为 True,否则为 False。

wsgi.run_once

如果只需要运行一次,设置为 True

WSGI 协议对于两个输入输出流有一些方法必须要实现

方法

wsgi.input

read(size)

wsgi.input

readline()

wsgi.input

readlines(hint)

wsgi.input

iter()

wsgi.errors

flush()

wsgi.errors

write(str)

wsgi.errors

writelines(seq)

这些基本上就是 WSGI 协议中定义的主要变量,也基本上涵盖了我们开发时所需要的变量。

3.2 解析 get 请求

environ['QUERY_STRING']

'QUERY_STRING': 'name=meetbill&age=16'

3.3 解析 post 请求

  • environ['CONTENT_LENGTH']

  • environ['wsgi.input']

当请求类型是 POST 时,查询字符串不再通过 URL 传递,而是包含在 HTTP 请求体之中来传递。请求体在环境字典中保存为键为“wsgi.input”对应的一个类文件变量。

为了从 wsgi.input 中读出请求体,有必要先知道请求体的长度,即 CONTENT_LENGTH 变量。WSGI 规范中指出,存放有请求体大小的这一 CONTENT_LENGTH 变量是不可靠的,有可能是空值,或者直接缺失,所以获取时应采用 try/except 语法块来进行异常防错。

 # 环境变量 CONTENT_LENGTH 可能为空值或缺失,采用 try/except 来防错
   try:
      request_body_size = int(environ.get('CONTENT_LENGTH', 0))
   except (ValueError):
      request_body_size = 0

   # 当请求方法为 POST 时查询字符串被放在 HTTP 请求体中进行传递。它被 WSGI 服务器具体存放在名为 wsgi.input 的一个类文件环境变量中。
   request_body = environ['wsgi.input'].read(request_body_size)

4 总结

(1)、RESTful 只是设计风格而不是标准,而 WSGI(Web Server Gateway Interface,Web 服务器网关接口)则是 Python 语言中所定义的 Web 服务器和 Web 应用程序之间或框架之间的通用接口标准。

(2)、WSGI 就是一座桥梁,桥梁的一端称为服务端或网关端,另一端称为应用端或者框架端,WSGI 的作用就是在协议之间进行转化。WSGI 将 Web 组件分成了三类:Web 服务器(WSGI Server)、Web 中间件(WSGI Middleware)与 Web 应用程序(WSGI Application)。

(3)、Web Server 接收 HTTP 请求,封装一系列环境变量,按照 WSGI 接口标准调用注册的 WSGI Application,最后将响应返回给客户端。

5 传送门

Last updated