python web--Flask工作流程

为什么需要web框架

web框架的作用是构建web应用程序,在讨论框架之前,需要先对网络的工作原理进行了解:当我们在浏览器中打开一个新标签并浏览至www.baidu.com时,浏览器中显示页面采取了哪些步骤?

web做了什么工作

抛开DNS解析及之后的3次握手、4次挥手不谈,我们主要来看下web做了什么工作。HTML是浏览器用来描述网页内容和结构的语言,web服务器负责将html发送到你的浏览器,从而进行网页的显示。

无论多么复杂的web应用程序,其真正要做的事情就是将HTML发送给浏览器。(其他格式的如json、css原理其实一样,这里统称为HTML)

解决了web应用程序是干什么的后,接下来的问题是:web应用程序要发送什么内容给浏览器?

首先浏览器从web应用程序请求数据,然后web应用程序将使用浏览器请求的数据来响应请求,最终发送回浏览器进行显示。(这里的浏览器也可以叫客户端)

要注意的是:通信始终是由浏览器启动,服务器无法启动连接,并向浏览器发送未经请求的数据。这和浏览器使用HTTP协议有关,HTTP协议是基于请求-响应的模型。

Http协议的方法

Http协议中的每个消息都有一个关联的方法,不同的Http方法对应于客户端(浏览器)不同的请求类型,这些请求代表的是客户端不同意图。例如:请求网页的HTML和提交表单是不同的,前者只需响应内容进行显示,因此这两个操作需要使用不同的方法。

get方法

get方法:从web服务器获取请求数据,在get请求中,web应用程序除了相应请求页面的HTML,不需要做其他事情。具体来说,web应用程序不应由于get请求而更改应用程序的状态,因此通常认为get请求是安全的,因为其不会导致应用程序发生更改。

post方法

post请求通常会导致应用程序状态发生更改,Post请求并不总是导致将新的html页面发送到客户端,而是客户端使用响应码来确定应用程序的操作是否成功。

HTTP响应码

正常情况下,web服务器对get请求的返回响应码为200,表示已经按你请求的做成功了;对post请求的返回响应码为204,表示已经成功,但没有东西可展示。web应用程序必须随每个响应请求发送一个,以对给定请求发生的情况进行指示。

从浏览器中输入一个网址,浏览器做了哪些事情,才显示到看到的网页;(DNS之类查询解析的就略过,需要的可以查看web做了什么(DNS))

浏览器接收到的网页都是 HTML 文件,HTML 是一种描述网页内容和结构的语言。负责给浏览器发送 HTML 的程序称为 web server(web服务器),容易混淆的是,这个应用程序所在的机器通常也被称为 web server。

所有的 web 应用做的事情就是把 HTML 内容发送给浏览器。不论这个 web 应用有多么复杂,最终的任务都是把 HTML(我故意忽略掉其他格式的内容,比如 JSON,CSS 文件,因为原理都是一样的)发送给浏览器。

那么这时候问题来了,web要发送浏览器请求的哪些内容返回给浏览器?

web要发送什么内容给浏览器

那么web应用如何知道其要发送什么内容给浏览器?它会发送浏览器请求的内容给浏览器,

HTTP协议:
HTTP 协议的基础是 请求-应答 (request-response) 模型。客户端(你的浏览器)请求某台物理机上 web 应用的数据,web server 则负责应答请求的数据。

所有的通信都是客户端(你的浏览器)发起的。服务端(web server)是不可能主动连接你,发送没有请求的数据的。如果你收到了数据,只是因为你的浏览器主动请求了这些数据。

HTTP 协议的每条消息都有对应的方法(method),不同的方法对应了客户端能发起的不同请求,也对应了客户端不同的意图。比如,请求 网页的 HTML 和提交一个表格在逻辑上是不同的,所以这两种方法需要两种不同的方法。

GET 方法就是从 web server 获取(get)数据,GET 请求也是目前最常用的 HTTP 请求。 处理 GET 请求的过程中,web 应用只需要返回请求的数据,无需其他操作。尤其是,不应该修改应用的状态(比如,GET 请求不应该导致一个新用户被创建)。因为这个原因,GET 请求通常被看做是安全 的。

和网站的交互不只是查看网页,当然也会通过表格等形式向web应用发送数据。POST 请求通常会传递用户创建的信息,导致 web 应用执行某些动作。输入自己的信息,来注册某个网站就会用到 POST 请求,请求中会包含你输入的数据。

和 GET 请求不同的是, POST 请求通常会导致 web 应用状态的改变。上面提及的例子中,表单被提交后,一个新的用户会被创建。还有一点不同,POST 请求的结果可能不会返回 HTML 数据给客户端,客户端需要通过 response code 来判断操作是否成功。

正常情况下,web server 会返回 200 的 response code,意思是:我已经完成了你要我做的事情,并且一切都没有问题。response code 是三位的数字,每次应答都要包含一个 response code,来标识请求的结果。200 表示 OK,是 GET 方法常见的返回值。POST 请求经常会返还 204(No contnet),表示:一切正常,但是我没有数据可以展示给你。

还需要注意的是:POST 请求发送给的 url,可能和数据发送出去的 url 不同。假如我们刚开始访问的是www.baidu.com,点击搜索后,包含数据的post请求可能被发送到www.baidu.com/question。POST 请求要发送到哪个地址,一般在HTML 源码中进行指定。

Web 应用

GET和POST方法是最常见的两种HTTP方法,Web应用负责接收HTTP请求并以HTTP响应进行回复,该响应通常包含表示所请求页面的HTML。POST请求会导致Web应用采取一些动作,如进行数据库相关的交互。还有许多其他的HTTP方法,但本文将重点放在GET和POST上。

一个简单的web应用

编写一个应用程序,来监听8080端口的连接,一旦受到连接,将等待客户端发送请求,然后使用一些简单的HTML对请求进行回复(response)。

import socket
HOST = '127.0.0.1'
PORT = 8080
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.bind((HOST, PORT))
listen_socket.listen(2)
connection, address = listen_socket.accept()
request = connection.recv(1024)
connection.sendall("""HTTP/1.1 200 OK
Content-type: text/html
<html>
	<body>
		<h1>Hello, World!</h1>
	</body>
</html>""".encode(encoding='utf_8', errors='strict'))
connection.close()

上述代码会接受单个连接和单个请求,对127.0.0.1:8080进行请求,无论请求是什么类型,都会以HTTP 200的响应码来进行响应。(因此它实际上不是Web服务器,服务器会有不同的响应码)。通过Content-type:text / html来告诉客户端请求或响应的数据是HTML。

Web应用包含的问题:
我们如何检查请求的URL并返回适当的页面?

除了简单的GET请求外,我们如何处理POST请求

我们如何处理更高级的概念,例如会话和cookie?

我们如何扩展应用程序以处理数千个并发连接?

可以看到,没有人想每次构建Web应用程序时去考虑这些问题的解决,因此Web框架的目的就是:隐藏并处理HTTP请求和响应相关的样板代码和基础结构的代码,从而完成接受HTTP请求,调度生成HTML代码,并使用该内容创建HTTP响应的任务。

Web框架

以python web的两大框架flask及django为例,web框架构建的web应用中,主要是解决路由和模板两大问题,即:
– 如何将请求的URL映射到处理它的代码?
– 如何创建请求的HTML,并为其动态的注入信息?

Django和flask都是采用的MVC方式,MVC只是逻辑上分离应用程序不同职责的一种方法,其中models表示数据库资源,controllers表示应用的业务逻辑或操作models,views表示负责动态生成代表页面的html。当然在Django中,MVC被称为MVT,控制器被称作视图,视图被称作模板,只是叫法不同,实际并无太大差别。

路由

路由是将请求的URL映射到负责生成关联该url对应HTML的代码的过程,在最简单的情况下,所有的请求get、post或其他,都会由相同的代码来处理(如上代码的例子)。在实际中,我们需要的是对于每个url,采用不同的请求方式,会有不同的代码来进行处理。

Django采用的方式是建立映射表,如果当请求某个url时,如:www.dapeng.com/hello,则handler_hello()函数负责对此生成响应,通过建立此映射表,直到所有的url都有其对应的函数。但是,当URL中有时会包含id之类拥有的数据,如:www.dapeng.com/books/1/此时如果再通过URL映射到视图函数,会显得颇为麻烦。Django中可以通过映射URL正则表达式的方法,来查看带有参数的函数,如:对匹配$^ / books/(?P \ d +)/ $的url调用handler_books(id)函数,其中参数id是通过正则表达式捕获的id值,这样,不同id的url都将映射到handler_books函数机械能处理。

Flask对待路由采取了不同的方式:通过route()装饰器的方法将函数连接到请求的URL上。

@app.route('/books/<id:int>/')
def handler_books(id):

通过给route()传递的url中包含name:type指令来捕获参数。

模板

当url映射到合适的代码段时,会通过模板的方式来动态的生产html。

模板类似于是字符串格式化,所需要输出的内容用动态的占位符,然后再将内容替换为format的参数,Django和Flask使用的模板引擎的jinja2均是以这种方式来使用。

总结下web框架的作用:隐藏基本的http请求和应答的代码,完成接受HTTP请求,然后分发任务,并生成HTML,最后返回包含HTML的HTTP应答。

web应用程序:客户端发送请求(request),收到服务器端的响应(response)。


WSGI

python程序是放在服务器上的http server,现在的问题是:服务器程序怎么把接收到的请求传递给python,怎么在网络的数据流和python的结构体之间做一个转换?

WSGI 是一套关于程序端和服务器端的规范,或者说统一的接口。(程序端和服务器之间的中间件)

在这里插入图片描述客户端、服务器、WSGI、程序之间的关系。

WSGI(Web Server Gateway Interface) 的任务就是把上面的数据在 http server 和 python 程序之间简单友好地传递。其规定每个py程序app必须是一个可以调用的对象(实现了__call__函数的方法或类),接受两个参数environ(WSGI的环境信息)和start_response(开始响应请求的函数),并且返回iterable。

  1. 从 flask 程序开始,要运行 web 应用,必须要有 web server,使用 run() 方法运行应用时创建了 WSGIServer,继承自 werkzeug 中的 BaseServer。然后一直被继承,不断被重写;
  2. WSGI 看成中间件,WSGI 中有一个非常重要的概念:每个 python web 应用都是一个可调用(callable)的对象(类定义时写了__call__ 方法);
  3. Flask 类中的__call__ 方法会调用 wsgi_app()方法,wsgi_app()的作用就是找到各种请求处理函数,调用这些函数,将处理结果返回到服务器;

有了上面的知识,从最简单的这个 flask 程序来看 WSGI 的实现。

  1. 使用app.run()方法来启动 flask 应用(app.run()代码隐藏着创建一个服务器),app 应用本身会作为参数传递到 WSGI 服务器中。
  2. 在客户端(这里就是浏览器)输入网址(发送一个请求),服务器使用 WSGI 中间件来处理这个请求。
  3. WSGI 处理请求对应着wsgi_app(self, environ, start_response)方法,self参数对应着app,即 flask 程序;environ和 start_response由服务器提供。
  4. wsgi_app()作用就是调用各种请求处理函数来处理请求,然后返回处理结果。即用户输入网址后,看到了网页响应。

WSGI:每个python web应用都是一个可调用(callable)的对象,在flask中,这个对象就是app = Flask(name) 创建出来的 app。

如果要运行web应用,必须有web server,

WSGI

WSGI其实是作为一个接口,来接受Server传递过来的信息,然后通过这个接口调用后台app里的view function来进行响应。

在调用app的时候,其实是在调用app内部的wsgi_app这个功能,而wsgi_app是包含在webapp框架内的,所以可以把wsgi区域和最右端的看作是一个整体,只不过在功能上,wsgi区域的功能和view function是分开的。

WSGI具体的功能

WSGI可以起到一个接口的功能,前面对接服务器,后面对接app的具体功能;

def application(environ, start_response):
    #一个符合wsgi协议的应用程序写法应该接受2个参数
    start_response('200 OK', [('Content-Type', 'text/html')])  
    #environ为http的相关信息,如请求头等 start_response则是响应信息
    return [b'<h1>Hello, web!</h1>']        
    #return出来是响应内容

作为app本身,就算启动了程序,也没办法给application传递参数,实际调用application和传递2个参数的动作,是服务器来进行的。application 即叫做wsgi_app;

flask 实例

from flask import Flask 
app = Flask(__name__)         
#生成app实例
@app.route('/')
def index():
	return 'Hello World'

当服务器收到http请求时,去调用app时,实际上调用的是flask的__call__方法。

# __call__方法的源码:
class Flask(_PackageBoundObject):        
#Flask类 
#中间省略一些代码
def __call__(self, environ, start_response):    
#Flask实例的__call__方法
	"""Shortcut for :attr:`wsgi_app`."""
	return self.wsgi_app(environ, start_response)  
	#注意他的return,他返回的时候,实际上是调用了wsgi_app这个功能

当http请求从server发送过来的时候,会启动__call__功能,实际上是调用wsgi_app功能来传入environ和srart_response。

wsgi_app定义

class Flask(_PackageBoundObject):
#中间省略一些代码
#请注意函数的说明,说得非常准确,这个wsgi_app是一个真正的WSGI应用
	def wsgi_app(self, environ, start_response): #他扮演的是一个中间角色
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                ctx.push()
                response = self.full_dispatch_request()
                #full_dispatch_request起到了预处理和错误处理以及分发请求的作用
            except Exception as e:
                error = e
                response = self.handle_exception(e)
                #如果有错误发生,则生成错误响应
            except:  # noqa: B001
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
            #如果没有错误发生,则正常响应请求,返回响应内容
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

wsgi_app的内部流程:

  1. 生成request请求对象,并请求上下文环境;
ctx = self.request_context(environ)

这里涉及到了flask使用请求上下文和应用上下文的概念,结构为栈结构。

这里将其理解为:作用是生成了一个request请求对象,以及包含请求信息在内的request context。

  1. 请求进入预处理、错误处理以及请求转发到响应的过程;
    进入到wsgi_app的函数内部,生成了request对象和上下文环境后,进入到try:
response = self.full_dispatch_request()

响应被赋值成full_dispatch_request方法的内容,接着再来看下full_dispatch_request方法:
2.1 full_dispatch_request方法1

class Flask(_PackageBoundObject):
	def full_dispatch_request(self):
        self.try_trigger_before_first_request_functions()
        #进行发生真实请求前的处理
        try:
            request_started.send(self)
            rv = self.preprocess_request()
            if rv is None:
                rv = self.dispatch_request()
        except Exception as e:
            rv = self.handle_user_exception(e)
        return self.finalize_request(rv)

2.1.1 try_trigger_before_first_request_function
full_dispatch_request方法中首先执行了try_trigger_before_first_request_function方法,该方法的主要目的是将_got_first_request属性值置为True。

class Flask(_PackageBoundObject):
    def try_trigger_before_first_request_functions(self):
        """Called before each request and will ensure that it triggers
        the :attr:`before_first_request_funcs` and only exactly once per
        application instance (which means process usually).
        :internal:
        """
        if self._got_first_request:
            return
        with self._before_request_lock:
            if self._got_first_request:
                return
            for func in self.before_first_request_funcs:
                func()
            self._got_first_request = True

2.1.1.1 got_first_request的定义
_got_first_request 的默认值是 False,当应用程序启动时,此属性的值设置为True。

@property
    def got_first_request(self):
        """This attribute is set to ``True`` if the application started
        handling the first request.

        .. versionadded:: 0.8
        """
        return self._got_first_request

2.2 full_dispatch_request方法2
继续往下走,下一段代码是:request_started.send(),这部分的作用是进行socket部分的功能,暂时不深入追溯。

2.3 full_dispatch_request方法3
rv = self.preprocess_request()
主要是进行 flask 的 hook 钩子, before_request 功能的实现,也就是在真正发生请求之前,有些事情需要提前做。

flask一共有4个hook钩子。

2.4 full_dispatch_request方法4

rv = self.dispatch_request()

一个 http 请求到了这里,实际上已经完成了从 wsgi 部分的过渡,进入到了寻找响应的阶段。一个请求通过 url 进来以后,app 怎么知道要如何响应呢?

(app是由Flask类的对象)

答案就是通过dispatch_request的方法来进行请求判定和分发。

def dispatch_request(self):
    """Does the request dispatching.  Matches the URL and returns the
    return value of the view or error handler.  This does not have to
    be a response object.  In order to convert the return value to a
    proper response object, call :func:`make_response`.

    .. versionchanged:: 0.7
       This no longer does the exception handling, this code was
       moved to the new :meth:`full_dispatch_request`.
    """
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        self.raise_routing_exception(req)
    rule = req.url_rule
    # if we provide automatic options for this URL and the
    # request came with the OPTIONS method, reply automatically
    if (
        getattr(rule, "provide_automatic_options", False)
        and req.method == "OPTIONS"
    ):
        return self.make_default_options_response()
    # otherwise dispatch to the handler for that endpoint
    return self.view_functions[rule.endpoint](**req.view_args)

req = _request_ctx_stack.top.request 可以暂时理解为:首先会将请求对象赋值给 req。

当每个url进来后,都会对应一个view_function:

@app.route('/')
def index():
    return 'Hello world'

比如对路径’/‘对应的是index函数。实际上这其实是分两步走的,第一步是’/'对应的endpoint为index,第二步接着是endpoint index对应到index()视图函数。

URL–>endpoint–>viewfunction

view_function是一个字典形式,其key和value的关系是endpoint–>viewfunction,故当每个有效的URL进来时,都能找到它对应的视图函数viewfunction,取回对应的返回值并赋值给rv。

这个示例简单代码中的index中,取得的就是’Hello world’字符串的值。

当请求分发完成后,此时已经拿到了返回的值。

2.5 full_dispatch_request方法5
make_response函数的作用是将刚才取得的viewfunction的rv值生成响应,重新赋值给response。

再用process_response来处理一个

response = self.make_response(rv)
response = self.process_response(response)
request_finished.send(self, response=response)
return response

再通过 process_response 功能主要是处理一个 after_request 的功能,比如你在请求后,要把数据库连接关闭等动作,和上面提到的 before_request 对应和类似。

最后进行request_finished.send的处理,这块也和socket处理有关,暂时不详细深入。

最后将处理的response对象返回。

make_response 函数是一个非常重要的函数,他的作用是返回一个 response_class 的实例对象,也就是可以接受 environ 和 start_reponse 两个参数的对象。

#注意函数说明,converts the return value from view function to a real response object
class Flask(_PackageBoundObject):
    def make_response(self, rv):
        status = headers = None
        # unpack tuple returns
        if isinstance(rv, tuple):
            len_rv = len(rv)

            # a 3-tuple is unpacked directly
            if len_rv == 3:
                rv, status, headers = rv
            # decide if a 2-tuple has status or headers
            elif len_rv == 2:
                if isinstance(rv[1], (Headers, dict, tuple, list)):
                    rv, headers = rv
                else:
                    rv, status = rv
            # other sized tuples are not allowed
            else:
                raise TypeError(
                    "The view function did not return a valid response tuple."
                    " The tuple must have the form (body, status, headers),"
                    " (body, status), or (body, headers)."
                )

        # the body must not be None
        if rv is None:
            raise TypeError(
                "The view function did not return a valid response. The"
                " function either returned None or ended without a return"
                " statement."
            )

        # make sure the body is an instance of the response class
        if not isinstance(rv, self.response_class):
            if isinstance(rv, (text_type, bytes, bytearray)):
                # let the response class set the status and headers instead of
                # waiting to do it manually, so that the class can handle any
                # special logic
                rv = self.response_class(rv, status=status, headers=headers)
                status = headers = None
            elif isinstance(rv, dict):
                rv = jsonify(rv)
            elif isinstance(rv, BaseResponse) or callable(rv):
                # evaluate a WSGI callable, or coerce a different response
                # class to the correct type
                try:
                    rv = self.response_class.force_type(rv, request.environ)
                except TypeError as e:
                    new_error = TypeError(
                        "{e}\nThe view function did not return a valid"
                        " response. The return type must be a string, dict, tuple,"
                        " Response instance, or WSGI callable, but it was a"
                        " {rv.__class__.__name__}.".format(e=e, rv=rv)
                    )
                    reraise(TypeError, new_error, sys.exc_info()[2])
            else:
                raise TypeError(
                    "The view function did not return a valid"
                    " response. The return type must be a string, dict, tuple,"
                    " Response instance, or WSGI callable, but it was a"
                    " {rv.__class__.__name__}.".format(rv=rv)
                )

        # prefer the status if it was provided
        if status is not None:
            if isinstance(status, (text_type, bytes, bytearray)):
                rv.status = status
            else:
                rv.status_code = status

        # extend existing headers with provided headers
        if headers:
            rv.headers.extend(headers)

        return rv

最后:

wsgi

return self.finalize_request(rv)
def finalize_request(self, rv, from_error_handler=False):
    response = self.make_response(rv)
    try:
        response = self.process_response(response)
        request_finished.send(self, response=response)
    except Exception:
        if not from_error_handler:
            raise
        self.logger.exception(
            "Request finalizing failed with an error while handling an error"
        )
    return response

当response从刚刚的full_dispatch_request返回之后,函数会对返回的response加上environ, start_response 的参数返回给服务器。至此,一个HTTP从请求到响应的流程就结束了。

这个流程的关键步骤可以归纳总结如下:

通过wsgi server 传入environ和start_response–>由wsgi_app来生成request对象和上下文环境–>启动full_dispatch_request的功能–>通过dispatch_request进行url–>view fuction的逻辑判断转发,并取得view function的返回值;

通过mask_response函数,将一个view function的返回值转换成一个response_class对象,

通过向response对象传入environ和start_response参数,将最终响应对象返回给服务器。

在这里插入图片描述

作者计算机硕士,从事大数据方向,公众号致力于技术专栏,主要内容包括:算法,大数据,个人思考总结等

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

精神抖擞王大鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值