首先要明白web服务就是一个解析请求 然后返回响应的过程。
然后明白wsgi是什么?
一套接口标准,用来实现在服务器和python程序之间的转换。使得服务器和python程序之间能够交互。
常见的python 的web程序的架构一般是这样子
Nginx-->(wsgi)gunicorn-->framework-->application
wsgi工作在wsgi服务器和web服务器的中间,一般是使用nginx来进行反向代理。然后使用gunicorn来
当wsgi服务器。
wsgi的接口是这样子的:
wsgi_app(environ, start_response)
environ 包含符合wsgi 标准的一个字典 如下所示:
{'wsgi.url_scheme': 'http', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'SCRIPT_NAME': '', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'HTTP_CONNECTION': 'keep-alive', 'REQUEST_METHOD': 'GET', 'SERVER_SOFTWARE': 'Werkzeug/0.12.1', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'wsgi.multiprocess': False, 'REMOTE_ADDR': '127.0.0.1', 'QUERY_STRING': 'next=%2F', 'wsgi.run_once': False, 'SERVER_NAME': '127.0.0.1', 'werkzeug.server.shutdown': <function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x000000198E7241E0>, 'wsgi.input': <_io.BufferedReader name=828>, 'wsgi.version': (1, 0), 'HTTP_COOKIE': 'UM_distinctid=15c7b3a049547-08fef361539c6f-323f5e0f-144000-15c7b3a049666a; CNZZDATA1262121305=568492071-1496716091-%7C1496716211; csrftoken=ovtMsXELMG8ISgFhdDzqcYJKVParC8ajkCr8Kza6RKQ9reVO3wOiwXuJigTw6Iaa; __wzdbd8a16e4306732acca12=1504512971|1acb4ba715b7; test=tests', 'SERVER_PORT': '5000', 'PATH_INFO': '/login', 'HTTP_ACCEPT_LANGUAGE': 'zh-CN,zh;q=0.8', 'REMOTE_PORT': 64941, 'HTTP_HOST': '127.0.0.1:5000', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36', 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, 'werkzeug.request': <BaseRequest 'http://127.0.0.1:5000/login?next=%2F' [GET]>, 'HTTP_CACHE_CONTROL': 'max-age=0', 'wsgi.multithread': False}
这里的environ就是给了一个request 定义了一个标准。
start_response 是一个返回响应的函数,通过request来产生响应。
那么wsgi服务器具体做了什么 ,它究竟是干什么的呢?
其实主要就是将数据转化成wsgi格式的 。具体整个wsgi后面发生了什么呢?
下面来阅读整个flask框架。首先要明白的是flask的框架不过是应用和wsgi 之间的一层抽象。
主要提供的功能就是路由绑定 即将路由和具体的执行函数绑定在一起。
def dispatch_request(self):
try:
endpoint, values = self.match_request()
return self.view_functions[endpoint](**values)
except HTTPException, e:
handler = self.error_handlers.get(e.code)
if handler is None:
return e
return handler(e)
except Exception, e:
handler = self.error_handlers.get(500)
if self.debug or handler is None:
raise
return handler(e)
这个函数根据相应的路由返回对应的响应。
那么在这之前就需要一个在程序启动一开始就把函数和路由绑定的方法。
def route(self, rule, **options):
def decorator(f):
self.add_url_rule(rule, f.__name__, **options)
self.view_functions[f.__name__] = f
return f
return decorator
这就是flask中的 @app.route 的route,使用装饰器,将函数名字和路由绑定在一起。
比如
@app.route('/test/<int:id>')
def test(id):
do something
这里将这个路由和 test这个函数绑定在一起。这里还有动态参数这个概念。即 <>中的部分。这个依靠
正则表达式来获取值。
整个流程就是这样子的:
例: 浏览器--> 请求/test/32--> 经过wsgi服务器,environ中有一个path,上面的那个dispatch-request函数来
从路由字典中寻找 发现 test函数对应的是 这个类型的路由。 解析出 动态参数,传给test函数。
还有一个问题 有一些任务是需要在整个流程前执行的,那么需要有这么一种机制来进行执行。
flask 中有这么一个变量
self.before_request_funcs
用来保存需要先执行的函数。
def preprocess_request(self):
for func in self.before_request_funcs:
rv = func()
if rv is not None:
return rv
def wsgi_app(self, environ, start_response):
with self.request_context(environ):
rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
response = self.make_response(rv)
response = self.process_response(response)
return response(environ, start_response)
可以看到首先是执行before_request_funcs这个字典中的函数。同理如果有需要在每次请求之后执行的
函数也是这样子,变动的是 变成了在每次返回响应之前执行,即执行完具体的路由函数后。
这应该是框架最主要的功能。现在使用这个就可以写web程序了。
但是还有一个问题,http是无状态的协议,如果有多个用户登录网站,如何判断那个是那个呢?
这时就需要session 来实现了。 用session来维持会话,给每个请求一个cookie标识,服务端通过这个
cookie来判断那个是那个用户。flask自带的session都是存在客户端的,考虑到安全,使用了加密。
在服务端保存一个密钥,每次需要这个密钥来进行信息读取。
flask_login 这个扩展模块使用了这个机制来进行登录管理,通过保存用户的id, 可以很清楚的知道具体是那个
用户在登录,对一些必须登录的界面也可以很方便的进行管理。使用装饰器这个语法糖 写起来是这样子的:
@app.route('/mypost')
@login_reqired
def mypost():
do something
这样子就可以对一些页面实现访问控制了。
最后还有一个比较理解的地方,即flask中的上下文。
Flask中分为请求上下文 requestcontext 和 应用上下文 appcontext
这背后的主要实现是靠:
LocalStack(object):
LocalProxy(object):
这种上下文像threadlocal,各个线程的对象 面向其他线程是隔离的。
为什么要这样子呢?
因为在并发的时候如果是多个线程共享一个上下文,很容易出现混乱。
这就是对一些全局的变量进行了隔离。
应用上下文的作用
首先 代码在执行的时候处于两种状态,一种是已经压入上下文的一种是没压入的。
所以在执行一些操作时经常遇到 不在上下文这样子的错误。
上面使用栈实现这种结构的原因是因为Flask是支持在一个python进程中有多个应用的。
下面来看这几类的实现
首先是声明两个实例 一个代表请求上下文 一个代表应用上下文
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
然后来翻看werkzeug的源码 发现这几个类实现的很巧妙。
这里面自己实现了类似 threadlocal 的东西。
如果有greenlet存在则首先使用greenlet
实现这种隔离就是通过不同的线程id 不同实现隔离。
__slots__ = ('__storage__', '__ident_func__')
def __setattr__(self, name, value):
storage[ident][name] = value
最主要的就是通过上面几行代码来实现的。
通过不同的线程id 和 来保存不同的函数名字,和值对应起来。
LocalStack 使用local来实现栈
def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, 'stack', None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
local 和 lcoalstack方法都实现了 __call__ 调用localproxy。这里是一个代理模式的最佳实践。
ls = Localstack()
ls.push(10) # 还是localstack对象
proxy = ls() # 变成了localproxy 对象
几乎全部的操作都被代理。
使用
_get_current_object 获取真正的对象。
现在最基本的就差不多完了。但是又出现了一个问题。当一个程序开始变得比较大,功能开始繁杂。开始多个人写同一个app,
使用之前的方式在一个文件中进行开发是很痛苦的事情。这个时候就需要一些方式来对其进行解耦。使用一些手段来进行拆分。
这时蓝图就出现了,以一种模块的方式来将代码进行解耦。
官方文档是这样说明蓝图的应用场景的
* 把一个应用分解为一个蓝图的集合。这对大型应用是理想的。一个项目可以实例化一个应用对象,初始化几个扩展,并注册一集合的蓝图。
* 以 URL 前缀和/或子域名,在应用上注册一个蓝图。 URL 前缀/子域名中的参数即成为这个蓝图下的所有视图函数的共同的视图参数(默认情况下)。
* 在一个应用中用不同的 URL 规则多次注册一个蓝图。
* 通过蓝图提供模板过滤器、静态文件、模板和其它功能。一个蓝图不一定要实现应用或者视图函数。
* 初始化一个 Flask 扩展时,在这些情况中注册一个蓝图。
比如一个网站 一般包含 -->登录认证 api 主页面 。。。各种功能
不使用蓝图的话就是一堆代码耦合在一起,对控制复杂度很不利。 使用蓝图的话
认证模块 使用auth api就使用api 主页面使用main 开头的方法。各自模块的代码放在
各自的python文件中,不同模块的静态文件也放在不同的文件中,管理起来很方便。
最简单的实现蓝图的方法就是对注册的蓝图直接加上前缀
大概如下
main = register(prefix,blueprint_folder)
main.add_url_route(prefix+rule, ...)。。。
总结 :
从计算机网络中学习到的很重要的一个概念就是分层。使用合理的层次来把任务进行划分。各层间保存相互独立。
通过一个叫SAP(服务访问点)的东西来实现各层间的访问。如上图,从最下层的Baseserver 到 tcpserver 到httpserver
到wsgiserver 每一层的功能都很明确,具体要在那一层进行任务只关心它的下一层就好了。还有很主要的一点,不管是
一个概念的出现,还是一个新技术的出现都是前人发现现有的技术无法满足需求,有更好的方法来解决问题,从而出现
了新技术。大部分东西看其本质还是那些。理解好了最根基的那些东西,再出来什么新的东西不过是到了一定时间点正好
该出来的东西。有了根基 无非就是看看就能够开始使用。