# 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](https://github.com/meetbill/butterfly/blob/master/images/wsgi.png?raw=true)

目前咱们的 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，不过这个模块没有考虑运行效率，只是为了开发和测试使用。

```python
# 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](https://github.com/meetbill/butterfly-fe) 实现，即前后端分离

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

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

## 3 开发

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

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

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

### 3.1 environ

```python
如访问：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

```python
使用 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 语法块来进行异常防错。

```python
 # 环境变量 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 传送门

> * [用 Python 理解服务器模型](https://www.textarea.com/zhicheng/yong-python-lijie-fuwuqi-moxing-shang-566/)
> * [网关协议学习笔记](https://blog.csdn.net/WANGXINGYUUUUU/article/details/108044263)
> * 让我们一起来构建一个 Web 服务器
>   * [让我们一起来构建一个 Web 服务器（一）](https://mozillazg.com/2015/06/let-us-build-a-web-server-part-1-zh-cn.html)
>   * [让我们一起来构建一个 Web 服务器（二）](https://mozillazg.com/2015/06/let-us-build-a-web-server-part-2-zh-cn.html)
>   * [让我们一起来构建一个 Web 服务器（三）](https://mozillazg.com/2015/08/let-us-build-a-web-server-part-3-zh-cn.html)
