首页 > 编程语言 > 详细

python wsgi

时间:2020-10-31 16:48:46      阅读:26      评论:0      收藏:0      [点我收藏+]

由于需要在公司内部分享wsgi ,看了 pep3333 文章以及pycon中关于 wsgi 的一些视频,对 wsgi 有了那么一点的了解,赶紧趁现在还有感觉的时候记录下来

1. 没有框架的PYTHON程序

假设一下,当我们没有 **Flask、Django **这些web框架使用,我们如何自己手动写一个简单服务。理一下思路 ,我们需要的步骤差不多如下:

  1. 通过 socket 获取 http请求内容
  2. 解析http请求文本,获取 请求头、请求方法 等http信息
  3. 业务代码,处理http请求,返回结果
  4. 根据结果拼装http响应文本
  5. 通过 socket 发送http消息

代码如下:

import socket


def parse_http(http_text):
    print(http_text)
    request, *headers, _, body = http_text.split(‘\r\n‘)
    method, path, protocol = request.split(‘ ‘)
    headers = {}
    for line in headers:
        key, value = line.split(‘:‘, maxsplit=1)
        headers[key] = value
    return method, path, protocol, headers, body


def process_http_response(response):
    list_ = (
        ‘HTTP/1.1 200 OK\r\n‘,
        ‘Content-Length: {}\r\n‘.format(len(response)),
        ‘Content-Type: text/plain\r\n‘,
        ‘\r\n‘,
        ‘{}\r\n‘.format(response),
        ‘\r\n‘,
    )
    return ‘‘.join(list_)


def view(http_request):
    method, path, *_ = http_request
    response = ‘%s %s‘ % (method, path)
    return response


if __name__ == ‘__main__‘:
    host = ‘127.0.0.1‘
    port = 5000
    with socket.socket() as s:
        s.bind((host, port))
        s.listen(1)
        while True:
            conn, addr = s.accept()
            with conn:
                http_text = conn.recv(2048).decode(‘utf-8‘)  # 1. 通过socket获取http请求
                http_request = parse_http(http_text)    # 2. 解析http请求,获取请求方法、请问内容等
                response = view(http_request)   # 3. 调用具体的业务逻辑代码,返回结果
                http_response = process_http_response(response)    # 4. 拼装http响应
                conn.sendall(http_response.encode(‘utf-8‘))    # 5. 发送http响应

运行下代码,通过 curl 打一下请求试一试

> curl 127.0.0.1:5000/hello
GET /hello

一切ok。
突然我们的同事小沈发现认为我们写的这个web服务器性能很棒,于是决定将它写的 Flask 程序接入我们的web服务器,结果发现我们的程序根本无法支持 Flask 程序 。但是小沈说为什么 Flask 可以被其他的python web服务器(gunicorn、uwsig)启动,为什么我们的不行

2. WSGI

去谷歌找到了 pep333 文档,发现了 gunicron 和 Flask 等python web程序之间的py交易。决定加入他们。
WSGI 是 web服务器 以及 web框架之间规定的一个接口规定,他规定了两者之间如何进行数据传输。WSGI 的web应用/框架,不再需要局限于特定的一个web服务器,只要是遵守了WSGI协议的都可以。例如 Flask 程序可以被 gunicorn、uwsgi 甚至是 django内置的wsgi服务器启动。
在改造 web服务器 之前,我们首先认识一下wsgi application

2.1 application

来看一个最简单的 wsgi application

def application(environ, start_response):
    """ 一个简单 wsgi application

    :param environ: dict字典。包含 http信息(请求头、url、方法...)以及 wsgi规定的一些参数(wsgi.input...)
    :param start_response: callable对象。用于发送 响应状态和响应头。接收 响应码、响应头、异常信息 三个参数
    :return: iterable对象。bytes类型的可迭代对应,返回具体的印象内容
    """
    status = ‘200 OK‘   # string类型。状态码和状态码信息用空格隔开
    response_headers = [(‘Content-type‘, ‘text/plain‘)]    # list类型。里面的元素都是(‘响应头的名称‘, ‘响应头的值‘)格式的元组
    start_response(status, response_headers)
    yield b‘hello‘

用 flask自带的wsgi 服务器启动我们的应用后,用 curl 请求下

from werkzeug.serving import run_simple

if __name__ == ‘__main__‘:
    run_simple(‘127.0.0.1‘, 7000, application)
   
# 请求接收
> curl 127.0.0.1:7000
hello

知道了 wsgi application 是什么样子,就可以改造我们的web服务器了

2.2 server

server需要做以下的改变以适配 wsgi。

  1. 解析 http请求时,将http信息放到字典中
  2. 提供一个start_response方法,用于返回 响应头和响应码
  3. 循环读取 application 的内容,返回http响应

首先改造 parse_http 方法,我们不再返回一个list,而是一个dict字典。

def parse_http(http_text):
    request, *headers, _, body = http_text.split(‘\r\n‘)
    method, path, protocol = request.split(‘ ‘)
    environ = {
        ‘REQUEST_METHOD‘: request,
        ‘PATH_INFO‘: path,
        ‘SERVER_PROTOCOL‘: protocol,
    }
    for line in headers:
        key, value = line.split(‘:‘, maxsplit=1)
        environ[‘HTTP_{}‘.format(key.upper())] = value
    return environ

environ 的key必须符合 wsgi规定,不能够直接乱造。由于这里的只是demo,所以很多必须的key值都没有放,如果需要知道详细信息,可以自己去阅读 pep333

接下来,我们需要提供一个 start_response 方法,用于发送 响应码和响应头。

headers_set = []
headers_sent = []
http_response = ""

def write(data):
    global headers_sent, headers_set, http_response

    if not headers_set:
        raise AssertionError("write() before start_response()")

    elif not headers_sent:
        status, response_headers = headers_sent[:] = headers_set
        http_response += ‘Status:%s\r\n‘ % status
        for header in response_headers:
            http_response += ‘%s: %s\r\n‘ % header
        http_response += ‘\r\n‘

    http_response += data.decode(‘utf-8‘)


def start_response(status, response_headers, exc_info=None):
    headers_set[:] = [status, response_headers]

    return write

这里用 headers_set 接收 响应头和响应码,是因为如果直接写入到 http_response 或者直接通过socket 返回到客户端。不利于其他中间件对 响应码和响应头 进行修改。

最后,用for循环解析application返回的iterable对象,并返回最终的结果

if __name__ == ‘__main__‘:
    host = ‘127.0.0.1‘
    port = 5001
    with socket.socket() as s:
        s.bind((host, port))
        s.listen(1)
        while True:
            conn, addr = s.accept()
            with conn:
                http_text = conn.recv(2048).decode(‘utf-8‘)  # 1. 通过socket获取http请求
                environ = parse_http(http_text)
                response = application(environ, start_response)
                try:
                    for data in response:
                        if data:
                            write(data)
                    if not headers_sent:
                        write(‘\r\n‘)
                    conn.sendall(http_response.encode(‘utf-8‘))
                    # 清空 响应数据
                    http_response = ""
                    headers_sent = []
                finally:
                    if hasattr(response, ‘close‘):
                        response.close()

application 就是我们写的wsgi application,当然你也可以替换成 flask app或者其他的wsgi application.

2.3 middleware

当我们的 应用需要支持 URL路由、CROS跨域、接口监控 等功能的时候,我们可以使用中间件。中间件很特殊,它即是application也是server,对于 application来说它是server,对于 server来说他是application。所以他既要满足 application,也要满足server的要求。
接下来我们实现一个支持 URL路由 的中间件。

class UrlRoutingMiddleware:
    apis = dict()

    def route(self, uri_path: str):

        def decorator_route(func):
            self.apis[uri_path] = func

            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)

            return wrapper
        return decorator_route

    @staticmethod
    def _execute_exc(environ, start_response):
        path = environ[‘PATH_INFO‘]
        start_response(‘200 OK‘, [(‘Content-type‘, ‘text/plain‘)])
        result = (‘%s not found‘ % path).encode(‘utf-8‘)
        yield result

    def __call__(self, environ, start_response):
        path = environ[‘PATH_INFO‘]
        api = self.apis.get(path, self._execute_exc)
        return api(environ, start_response)
    
    mw = UrlRoutingMiddleware()

注意??:UrlRoutingMiddleware 并不是我们的中间件,他的实例 mw 才是我们需要的中间件。

我们可以用 mw.route 在 apis 添加的 url 和 wsgi application 的映射关系。输入不同的 url 时,可以调用不同的wsgi application;如果在 apis 没有找到对应 url,则最终调用 _execute_exec 这个方法(他同时也是一个 wsgi application)。

@mw.route(‘/a1‘)
def application1(environ, start_response):
	pass

@mw.route(‘/a2‘)
def application1(environ, start_response):
	pass

在server端,我们传入 mw 替代我们的application

3. ASGI

wsgi 是同步的,并且他只能处理类似于 http 这样一输入一输出 的网络协议。所以 python3 的推出,协程被越来越多的使用,以及 websocket 等长连接的兴起,wsgi 显然已经更不上时代的发展。
asgi 是django团队一个新的python web服务的规范,它具有以下的特点:

  1. 支持协程
  2. 支持 websocket 等长连接网络协议
  3. 他是wsgi的超集。wsgi application通过一点小小的改变就可以运行在 asgi server中

目前,新出的框架以及服务基本都实现了 asgi,例如 fastapi、sanic 等 新的

python wsgi

原文:https://www.cnblogs.com/Baskershu/p/13905522.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!