文章目录
- 为什么需要web框架
- web做了什么工作
- Http协议的方法
- get方法
- post方法
- HTTP响应码
- web要发送什么内容给浏览器
- Web 应用
- 一个简单的web应用
- Web框架
- 路由
- 模板
- WSGI
- WSGI
- flask 实例
- wsgi_app定义
为什么需要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。
- 从 flask 程序开始,要运行 web 应用,必须要有 web server,使用 run() 方法运行应用时创建了 WSGIServer,继承自 werkzeug 中的 BaseServer。然后一直被继承,不断被重写;
- WSGI 看成中间件,WSGI 中有一个非常重要的概念:每个 python web 应用都是一个可调用(callable)的对象(类定义时写了__call__ 方法);
- Flask 类中的__call__ 方法会调用 wsgi_app()方法,wsgi_app()的作用就是找到各种请求处理函数,调用这些函数,将处理结果返回到服务器;
有了上面的知识,从最简单的这个 flask 程序来看 WSGI 的实现。
- 使用app.run()方法来启动 flask 应用(app.run()代码隐藏着创建一个服务器),app 应用本身会作为参数传递到 WSGI 服务器中。
- 在客户端(这里就是浏览器)输入网址(发送一个请求),服务器使用 WSGI 中间件来处理这个请求。
- WSGI 处理请求对应着wsgi_app(self, environ, start_response)方法,self参数对应着app,即 flask 程序;environ和 start_response由服务器提供。
- 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的内部流程:
- 生成request请求对象,并请求上下文环境;
ctx = self.request_context(environ)
这里涉及到了flask使用请求上下文和应用上下文的概念,结构为栈结构。
这里将其理解为:作用是生成了一个request请求对象,以及包含请求信息在内的request context。
- 请求进入预处理、错误处理以及请求转发到响应的过程;
进入到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参数,将最终响应对象返回给服务器。
作者计算机硕士,从事大数据方向,公众号致力于技术专栏,主要内容包括:算法,大数据,个人思考总结等