WSGI协议及werkzeug-概念篇

323 阅读7分钟
从WSGI协议来看,一个WSGI应用应该是这样的
def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Hello World!']
我们可以使用Werkzeug封装好的类Response,来帮我们简化上述代码
from werkzeug.wrappers import Response
def application(environ, start_response):
    response = Response('Hello World!', mimetype='text/plain')
    return response(environ, start_response)
2.1 开始我们的应用
我们用类的方式来实现一个application,在python的魔法函数__call__下执行start_response,这样我们可以用执行对象的方式app()来处理请求。
class MyApp(object):
    def __init__(self):
        print("创建app")
​
    def dispatch_request(self, request):
        return Response("hello world")
​
    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)
​
    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)
函数wsgi_app是我们整个应用的核心,这样的写法容易让我们去扩展WSGI中间件,里面创建了一个Request对象,传递给dispatch_request并返回一个WSGI应用Response。
现在我们可以构建一个统一的生成函数来生成一个MyApp对象,方便管理。
def create_app():
    app = MyApp()
    return app
2.1.1 flask应用的是怎么启动的
  app.run('0.0.0.0', 8001, debug=app.config["DEBUG"], threaded=True)
我们进入到app.run()发现里面有这么一句
from werkzeug.serving import run_simple
try:
    run_simple(host, port, self, **options)
finally:
    self._got_first_request = False
2.1.2 那到底这个run_simple干了些什么
def run_simple(
    hostname,
    port,
    application,
    use_reloader=False,
    use_debugger=False,
    use_evalex=True,
    extra_files=None,
    reloader_interval=1,
    reloader_type="auto",
    threaded=False,
    processes=1,
    request_handler=None,
    static_files=None,
    passthrough_errors=False,
    ssl_context=None,
):
    """Start a WSGI application. Optional features include a reloader,
    multithreading and fork support.
    ……
启动一个WSGI的应用,可选特性包括 自动加载、多线程、多进程。这其中我们注意两个参数,threaded和processes。在整个run_simple里核心是这个函数的内置函数inner
    def inner():
        try:
            fd = int(os.environ["WERKZEUG_SERVER_FD"])
        except (LookupError, ValueError):
            fd = None
        srv = make_server(
            hostname,
            port,
            application,
            threaded,
            processes,
            request_handler,
            passthrough_errors,
            ssl_context,
            fd=fd,
        )
        if fd is None:
            log_startup(srv.socket)
        srv.serve_forever()
这里的`fd`其实就是一个文件描述符,让socket可以从文件描述符来创建对象。
进入make_server我们查看下代码
def make_server(
    host=None,
    port=None,
    app=None,
    threaded=False,
    processes=1,
    request_handler=None,
    passthrough_errors=False,
    ssl_context=None,
    fd=None,
):
    """Create a new server instance that is either threaded, or forks
    or just processes one request after another.
    """
    if threaded and processes > 1:
        raise ValueError("cannot have a multithreaded and multi process server.")
    elif threaded:
        return ThreadedWSGIServer(
            host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
        )
    elif processes > 1:
        return ForkingWSGIServer(
            host,
            port,
            app,
            processes,
            request_handler,
            passthrough_errors,
            ssl_context,
            fd=fd,
        )
    else:
        return BaseWSGIServer(
            host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
        )
整个过程的逻辑很清晰,根据参数threaded和processes来判断最后返回的是那个WSGIServer,其实这个函数的注释也说明了实现的功能:创建一个服务支持对请求的多线程处理、或者多进程处理、或者一个请求接一个请求处理的单线程,但是不支持多进程加多线程的方式。 到此我们也知道,Flask原生支持的并发模式其实是依赖werkzeug,否则就是一个单线程的应用,我们可以再往里挖一点。 2.1.3 WSGIServer又是如何工作的
我们找到BaseWSGIServer,它继承自HTTPServer
class BaseWSGIServer(HTTPServer, object):
​
    """Simple single-threaded, single-process WSGI server."""
告诉我们它是个单线程的WSGI server,在回顾run_simple下的inner函数最后执行了srv.serve_forever() 看这个名字貌似就是启动了一个永久的服务。
    def serve_forever(self):
        self.shutdown_signal = False
        try:
            HTTPServer.serve_forever(self)
        except KeyboardInterrupt:
            pass
        finally:
            self.server_close()
你能看到就是扩展了HTTPServer.serve_forever函数,最后执行server_close
	if hasattr(selectors, 'PollSelector'):
    		_ServerSelector = selectors.PollSelector
		else:
    		_ServerSelector = selectors.SelectSelector
    ……    
  	def serve_forever(self, poll_interval=0.5):
        """Handle one request at a time until shutdown.

        Polls for shutdown every poll_interval seconds. Ignores
        self.timeout. If you need to do periodic tasks, do them in
        another thread.
        """
        self.__is_shut_down.clear()
        try:
            with _ServerSelector() as selector:
                selector.register(self, selectors.EVENT_READ)

                while not self.__shutdown_request:
                    ready = selector.select(poll_interval)
                    # bpo-35017: shutdown() called during select(), exit immediately.
                    if self.__shutdown_request:
                        break
                    if ready:
                        self._handle_request_noblock()

                    self.service_actions()
        finally:
            self.__shutdown_request = False
            self.__is_shut_down.set() 
我们去寻找_ServerSelector函数,最后指向的是标准库selectors.py ,实现IO模型select、poll、epoll,kqueue(大部分unix系统上都存在,包括OS X)的封装的封装。这里用的是 poll,监控一组文件句柄,返回当有活跃的文件描述符活跃,去执行_handle_request_noblock
    def _handle_request_noblock(self):
        """Handle one request, without blocking.
​
        I assume that selector.select() has returned that the socket is
        readable before this function was called, so there should be no risk of
        blocking in get_request().
        """
        try:
            request, client_address = self.get_request()
        except OSError:
            return
        if self.verify_request(request, client_address):
            try:
                self.process_request(request, client_address)
            except Exception:
                self.handle_error(request, client_address)
                self.shutdown_request(request)
            except:
                self.shutdown_request(request)
                raise
        else:
            self.shutdown_request(request)
注释和函数名都表明了,只要在selector返回的ready为真情况下,执行过程就不可能被阻塞。
request, client_address = self.get_request()
这个get_request函数在HTTPServer的父类TCPServer里定义了
 def get_request(self):
 """Get the request and client address from the socket.
​
        May be overridden.
​
        """
 return self.socket.accept()
最后走到了socket的accept. 返回新的socket(这里是连接套接字, 而前面的socket则是监听套接字),和请求方的地址,在socket编程里,我们执行accept函数会阻塞,一直到客户端有消息过来,但其实这里是根本不会阻塞的,这就是pollio模型的特点,遍历所有的连接,直到找到一个有新消息的连接就返回真,通知服务端的socket的去accept,这时候是必定能收到值。

2.2 添加路由

现在我们已经有了一个基本的wsgi应用,接下来我们需要完善一下应用的路由规则。
Werkzeug 提供了一个灵活的集成路由。 你需要创建一个 Map 实例并添加一系列 Rule 对象。每一个Rule我们可以传入两个参数,一个是url, 一个是endpoint
我们在MyApp中维护一个Map, 并在create_app函数里注册我们的路由
# 添加路由
app.url_map = Map(
  [Rule('/', endpoint="new_url")]
)
endpoint在这里其实就是路由对应的函数的名称,接下来我们实现的就是通过endpoint去指向一个函数
def dispatch_request(self, request):
  # 根据请求路由找出匹配的endpoint,value是一个字典,代表的是路由的位置参数
 adapter = self.url_map.bind_to_environ(request.environ)
 try:
 endpoint, values = adapter.match()
 # 通过 endpoint + _handler 找到对应的函数
 return getattr(self, endpoint + '_handler')(request, **values)
 except HTTPException as e:
 print(repr(e))
 return Response("hello world")

2.2.1 flask是如何添加路由的呢

装饰器@route(“/home”, methods=["GET"])添加路由,我们可以从源码分析下
def route(self, rule, **options):
    def decorator(f):
        endpoint = options.pop("endpoint", None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator
rule指的就是路由,这个endpoint其实和上面的是一个意思,主要逻辑都在add_url_rule
def add_url_rule(
        self,
        rule,
        endpoint=None,
        view_func=None,
        provide_automatic_options=None,
        **options,
    ):
        # 没有传入endpoint就默认为view_func的函数名
        if endpoint is None:
            endpoint = _endpoint_from_view_func(view_func)
        options["endpoint"] = endpoint

        # 获取当前view_func支持的说有函数
        methods = options.pop("methods", None)

        methods = {item.upper() for item in methods}
        required_methods = set(getattr(view_func, "required_methods", ()))
        methods |= required_methods

        # 生成werkzeug Rule对象 绑定endpoint和路由地址
        rule = self.url_rule_class(rule, methods=methods, **options)

        # werkzeug Map对象中添加Rule对象
        self.url_map.add(rule)

        # 通过endpoint来寻找绑定的视图函数,如果已经绑定了且和当前函数不同就抛错
        if view_func is not None:
            old_func = self.view_functions.get(endpoint)
            if old_func is not None and old_func != view_func:
                raise AssertionError(
                    "View function mapping is overwriting an existing"
                    f" endpoint function: {endpoint}"
                )
            # 存取view_func到属性view_functions里
            self.view_functions[endpoint] = view_func

2.2 添加WSGI中间件

从WSGI协议中我们得知可以通过app=mw1(app)的方式添加中间件
我们新建一个中间件类,作用是记录整个响应过程用时。结构和我们的MyApp类很像,实现了一个__call__这也是一个WSGI应用,不同的是我们先保存了一个application,然后再执行中间件是实例时去处理保存的该application
class MyMiddleWare(object):
    """
    wsgi中间件
    """
    def __init__(self, application):
        self.application = application
        print("创建middleware")
​
    def __call__(self, environ, start_response):
        b = time.time()
        result = self.application(environ, start_response)
        duration = (time.time() - b)/1000
        print("duration: %f" % duration)
        return result
现在我们在create_app中添加该中间件, 我们可以直接改写app的wsgi_app函数为中间件的对象(实现了__call__使它可以像函数样调用)
app.wsgi_app = MyMiddleWare(app.wsgi_app)

2.2.1 Flask中如何添加中间件呢

在dsp项目的app_runner文件中
def create_app():
    flask_app = Flask('csp-controller')
    with flask_app.app_context():
        i18n(flask_app)
        create_db(flask_app)
        configure_models()
        configure_blueprints(flask_app)
        init_monitor(flask_app)
        setup_default_data()
        add_app_hook(flask_app)
    return flask_app

def run_worker(app=None):
    from app.scheduling.celery_app import make_celery
    if not app:
        app = create_app()
    celery_app = make_celery(app)

2.3 最终代码

from werkzeug.wrappers import Response
class MyMiddleWare(object):
    """
    wsgi中间件
    """
    def __init__(self, application):
        self.application = application
        print("创建middleware")
​
    def __call__(self, environ, start_response):
        b = time.time()
        result = self.application(environ, start_response)
        duration = (time.time() - b)/1000
        print("duration: %f" % duration)
        return result
​
​
class MyApp(object):
    def __init__(self):
        self.url_map = None
        print("创建app")
​
    def url_adapter(self):
        pass# handler方法需要返回Response对象(werkzeug封装的实现wsgi application)
    def new_url_handler(self, request):
        return Response('{"code": 0}', status=404)
​
    def dispatch_request(self, request):
        adapter = self.url_map.bind_to_environ(request.environ)
        try:
            # 根据请求路由找出匹配的endpoint,value是一个字典,代表的是路由的位置参数
            endpoint, values = adapter.match()
            # 通过 endpoint + _handler 找到对应的函数
            return getattr(self, endpoint + '_handler')(request, **values)
        except HTTPException as e:
            print(repr(e))
            return Response("hello world")
​
    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)
​
    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)
​
​
def create_app():
    app = MyApp()
    # 加入中间件
    app.wsgi_app = MyMiddleWare(app.wsgi_app)
​
    # 添加路由,endpoint指向的是一个函数,通过路由地址绑定到该endpoint上
    app.url_map = Map(
        [Rule('/', endpoint="new_url")]
    )
    return app

if __name__ == '__main__':
    app = create_app()
    run_simple('127.0.0.1', 5000, app)