Flask源码阅读以及WSGI理解

首先要明白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后面发生了什么呢?

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 每一层的功能都很明确,具体要在那一层进行任务只关心它的下一层就好了。还有很主要的一点,不管是
一个概念的出现,还是一个新技术的出现都是前人发现现有的技术无法满足需求,有更好的方法来解决问题,从而出现
了新技术。大部分东西看其本质还是那些。理解好了最根基的那些东西,再出来什么新的东西不过是到了一定时间点正好
该出来的东西。有了根基 无非就是看看就能够开始使用。

博客 https://www.97up.cn/post/148

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容