首页 > 其他分享 >05-路由-请求之时

05-路由-请求之时

时间:2023-01-20 14:34:11浏览次数:62  
标签:请求 05 对象 request 中间件 视图 方法 路由

这一篇博客我们尝试使用调试,看看整个请求是怎么触发的,也就是从Django接收到请求到调用具体的视图的流程,我们不尝试探讨整条链路,那样太多了。他们将会在后续的章节中讲解。

准备工作

在准备本文的正文之前,有些东西可能需要先捋清一下。WSGI 服务器的启动和 Django 的运行的关系。首先我在主 urls 文件里面打上了一个断点,然后 DEBUG 启动Django,得到如下的代码。

一张图实在放不下,读者注意每一个窗口的上面有对应的文件,下面有对应的文件中的光标选中行所属的代码块,另外大家可以依据行数寻找到对应的代码位置,截图中出现的三个窗口所对应的文件均在右侧的调试器中可以找到。
【1】此处是Django启动的过程中将会执行的一行代码,整个 runserver 怎么启动的,请看前面的博客,这里不再赘述。1 调用了2,需要注意的是,他将 django.core下的 runserver 中的 Command 的方法 inner_run 传递给了 run_with_reloader 方法,并作为 main_func 存在,所以需要时刻记得 main_func 和 inner_run 是同一个东西,后面还会用到的,包括正文也会再次出现。也就是说,这里并没有启动 WSGI 服务器
【2】调用了 start_django 函数,并且传递了 main_func[inner_run]
【3】启动了一个线程,并对这个线程进行了命名,叫作 django-main-thread,也就是说 main_func[inner_run] 跑起来了
【4】又执行了一个 run 方法
【5】run 方法内启动了 WSGI 服务器

结论:WSGI 并不是在主线程中启动的,而是另起了一个线程监听端口,而从 manage.py 这个文件开始所执行的所有代码才是真正的主线程,当我们以多线程的方式启动 WSGI 服务器的时候,WSGI 服务器是守护线程,所以我们使用 CTR + C 中断的其实是 manage.py 这个主线程,WSGI 服务器作为守护线程跟着被杀死了

接下来我们重新调试一遍,这一次我们在下面这两个地方打上断点,去掉其他所有的断点,看看有什么结果。

这个截图展示的是调试第一次停住的位置,所以是先加载了路由,后启动的 WSGI 服务器,上一篇博客中提到过,主进程让子线程停了0.1秒,这样就可以保证 WSGI 服务器启动之后,Django 已经做好了一切准备工作。

再从 serve_forever 看起

【1】一定要注意这里,这里使用type动态构建了一个类,这个类继承自 WSGIServer 和 ThreadingMixIn,这一点很重要。后面调用 serve_forever 启动一个 WSGI 服务器
【2】如果监听到了请求就会触发 _handle_request_noblock 方法
【3】_handle_request_noblock方法内,如果没有异常,是一个安全的请求,将会调用 process_request 方法,这个方法正是继承自 ThreadingMixIn。
【4】process_request 方法内新起一个线程处理请求,并将 process_request_thread 传递给了新线程作为执行方法。
【5】process_request_thread 接收两个参数,request 是和客户端交互的新生成的一个 socket,第二个参数正是客户端的地址。process_request_thread 方法内最主要的调用了两个方法,一个是 finish_request 方法和 shutdown_request,前者处理请求,后者做一些关闭操作,我们主要关注 finish_request 方法

finish_request

finish_request 是 WSGIServer 继承的 BaseServer 方法中的 finish_request 主要任务就是生成一个 RequestHandlerClass 对象,不出意外 RequestHandlerClass 在这里正是 WSGIHandler。前面的博客中已经提到过。

所以在探寻路由的过程中,finish_request 是一个重要的断点

路由寻找

我在 BaseServer 的 finish_request 方法和处理请求的视图的 get 方法打上了断点,然后再浏览器发起了请求,成功定位到了 finish_request 方法,右侧调试器前三个不用看,是 threading 包中的代码,偏底层,我一时半会儿还没搞清楚。联系上面的代码我们知道了 finish_request 方法其实是新起的线程中运行的。查看调试器,确实可以看到 finish_request 方法确实是被 process_request_thread 调用的。

跳过当前代码,使断点定位到 get 方法上,然后我们开始正式分析右侧的调试器部分

BaseRequestHandler.handle

前面的博客说过了,整个请求都是在 RequestHandle 类的初始化中完成,这里调用了子类的 handle 方法

handle & handle_one_request

handle_one_request 方法中已经生成了 ServerHandler 对象,并且调用其 run 方法

ServerHandler.run

这里的 application 是 WSGIHandler 类。

application 是可以在配置文件中配置的。

WSGIHandler.call

这里正是 WSGI 与 Django 交互的关键代码。
【1】这里默认写了请求对象,也就是我们在使用Django时候的那个 request 对象。前面所遇到的 request 其实是 socket 对象
【2】生成一个 request 对象
【3】获取返回,关键代码,后续的中间件啊,视图代码的都是在这个方法之内运行的,然后得到一个结果
【4】从 response 中获取状态码
【5】设置状态码和返回头
【6】设置文件流,如果有的话
【7】request 中的 GET 方法,他解析问号之后的查询参数,并构造成一个不可修改的字典

WSGIHandler.get_response

【1】执行了一个中间件链函数,向上点击上一面一个文件,发现调用了 get_response 方法,也就是【2】这里
【2】执行了配置文件中配置的第一个中间件类 SecurityMiddleware 的 get_response 方法。

此处有点混乱,我们需要知道这个中间件链是怎么生成的,然后又是如何执行的。以及我们如何自定义中间件。翻看【1】所属的文件可以看到上面有加载中间件的代码。我们进去打上断点,然后重新调试请求。

中间件链

在如图的位置打上断点,我们知道了这个加载顺序如下

【1】inner 方法内尝试获取一个 Handler 对象,于是触发了【2】
【2】触发了初始化方法,也就是 WSGIHandler.init
【3】WSGI.init 接着又调用了 BaseHandler.load_middleware 方法,WSGIHandler 继承自 BaseHandler
【4】从配置文件中加载了中间件列表

下面详细探寻一下 BaseHandler.load_middleware 方法

load_middleware 有点复杂

首先 load_middleware 函数有点长,一个视屏展示不下,所以多出了一部分放到了第二个竖屏的上半部分。
【1】获取返回函数,或许是异步的或许是同步的,依据传入的参数决定
【2】一个将异常转进返回返回对象中的函数。,他初步对 get_response 方法进行了封装,依据执行状态添加异常信息。注意,这里是第一层封装,也就是说底层还是执行的 get_response 代码,可以查看右下角的 convert_exception_to_response 源码
【3】从配置文件中依次获取中间件的包路径,注意这里用了列表反转
【4】对每一个中间件进行了导包
【5】依据各种条件来判断方法是异步执行还是同步执行
【6】是一个函数模式转换器,如果同步执行的函数需要异步执行就将其转换为异步的,反之亦然,就是一个执行模式的转换,这一块儿可以自习看源码,这里不作详细的展开.并且最后将封装过后的 get_response 传给中间件,生成对象
【7】对上一步中生成的对象进行再一次的异常封装。

BaseHandler.load_middleware 方法的最后将终极封装的 handler 赋值给了 _middleware_chain

结论:
【1】中间件是通过 convert_exception_to_response 方法一层一层封装起来的。
【2】封装过后的中间件链被调用,如果完全通过的话,那么其实已经出发了获取返回值的函数,也就是说执行了中间件链就是执行了视图函数,拿到了最终的返回结果
【3】中间件链是通过列表定义的,前面的先被封装,封装到了里层,后面的被封装到了外层。那么执行中间件链获取返回结果的时候,进则是后被封装的中间件先执行,出后被封装的中间件后被执行。我们学习 Django 的使用的时候,对中间件的执行顺序进行了讲述,而这就是为什么。

注意:获取中间件的包路径的时候使用了反转,也就是说,原本应该是进去的时候从下往上执行,回来的时候从上往下执行,由于列表反转,所以进去的时候是从上往下执行,返回的时候从下往上执行。

下面去掉断点,回到讲 BaseHandler.load_middleware 方法

BaseHandler._get_response

前面的中间件链让我们知道了,最终是通过 BaseHandler 的 _get_response 或者 _get_response_async 来获取请求返回,在本次请求中是 _get_response 被调用,而在 _get_response 方法中是通过【2】来获取的,这时注意右边的调试器内,也就是【3】,callback 代表的是 View.as_view 方法,其实这里已经是视图函数了,我的 Django 系列博客里面已经讲到过,View.as_view 是怎么跟真正的视图函数联系起来的,也就是说这个时候其实已经通过路由定位到了具体的视图函数,因此路由解析代码需要去前面寻找。make_view_atomic 方法只是对视图函数做了一层原子封装,这个具体怎么回事我还没搞懂。且看 callback 是怎么怎么来的。因此我们需要进 resolve_request 方法看一下。

resolve_request

【1】尝试从请求中获取 url_conf,当然本次请求中是没有这个值得,于是会走【2】
【2】调用 get_resolver 函数获取一个 URLResolver 对象。他将会调用 _get_cached_resolver 函数。
【3】尝试从缓存中获取 URLResolver 对象
【4】在Django系统启动之时已经被加载了,所以这里是不会执行的。详情见 路由-启动之时

这里需要回顾一下 URLResolver
上一篇的博客,路由-启动之时中讲到了,re_path[path] 函数,他接收两种值。

  • 一种是已经关联到了对应视图函数的路由,这个时候返回的是一个 URLPattern 对象。
  • 一种接收到的是一个路由列表,返回的是一个 URLResolver 对象。一般来说主路由文件内会引用子应用中的路由文件,这个时候返回的就是 URLResolver 对象

也就是说 URLResolver 对象其实就是为了跳到下一层路由,URLPattern 对象正是直接触发对应的视图函数。根路由就是一个 URLResolver 对象。他在加载的时候已经被缓存了,所以遇到了请求的时候直接从缓存中那这个根 URLResolver 对象。

继续讲本段代码。
【5】这里需要我们分析一下这段代码。

当前是根路由 URLResolver ,也就是这段代码生成的 URLResolver(RegexPattern(r"^/"), urlconf),所以当前的 self.pattern 就是 RegexPattern(r"^/") 对象,于是我们需要去看一下 RegexPattern 源码。

重点是这个 match 方法,当前传进来的 path 是 /api/ip2region2/ ,也就是说想用 r"^/" 来从 /api/ip2region2/ 提取值。注意 r"^/" 其实是一个正则字符串,在这里经过了包装变成了一个正则对象,而非普通的字符串。
所以 match = self.regex.search(path)
于是整个 match 方法的结果如下

于是接下来要走 for 循环

第一个匹配的就是我圈出来的,这个返回值是一个 URLPattern 的对象,下面我们要好好看看 URLPattern 源码

我把 URLPattern.match 源码部分拿出来运行,可以看到 match 为空,所以匹配失败

触发不了 if sub_match: 下面的代码,于是继续匹配第二个 URLResolver 的对象,在我这里必然也是匹配失败的。大家可以进入源码看一下,如何失败的。因为第三个同样也是 URLResolver 对象,所以我们拿他举例

第三个 URLResolver 对象的开头是 api,表示需要进入下一层的路由,于是又进入了 URLResolver.resolve 方法内,于是得到了 new_path = ip2region2/ 每进入一层路由都会去掉前面的部分前缀,就是这么实现的。

这段代码成功返回了 match,有值,于是需要执行下面这段代码

这段代码中,parttern 正是根路由中的第三个元素,他是一个 URLResolver 对象,于是又执行了一次 resolve 方法,可以看做是一个递归,进入了内部的 resolve 方法,这时,传递进去的 new_path 为 api/ip2regions2/。于是就有了下面的截图。

在根路由中第三个 URLResolver.resolve 方法内执行了下面这段代码之后,得到的 match 为三元组,其中新的到的 new_path 正是 ip2region/ ,也就是大截图中的【2】,于是需要执行一下 for 循环,提取 api/urls 中的列表的每一个元素。

这里需要回顾一下:
【1】是 上一篇的博客,路由-启动之时 ,也就是上一个大截图中 【1】这里,把 api/urls 传给了 URLResolver。

这里使用了装饰器,所以可以直接拿取这个列表,url_patterns 方法内部使用了反射获取了列表。这些列表的元素都是 URLPattern

于是 URLPattern 中尝试匹配 ip2region/,这里我不尝试去用人脑匹配每一个,看每一行代码怎么走,大家一步一步调试会看到这面这幅截图,这一步

这里返回了一个 ResolverMatch 对象,这个对象中的 self.callback 正式当初定义到路由中的视图。到了这里终于获取到了具体的视图函数

匹配到之后,会不断地向外返回 ResolverMatch 对象,并且不断完善信息

路由的终极返回

路由查找最终返回了一个 ResolverMatch 对象,并且将这个对象设置为了 request 对象的属性

回到 _get_response

resolve_request 返回的本是一个 ResolverMatch 对象,这个对象怎么会像元组或者列表一样解包,正式靠【4】

【1】尝试解包,第一个元素是视图函数,第二个元素是从url中提取的参数,第三个元素是从url中提取的字典这样的参数
【2】这里需要在调用真正的视图函数之前,尝试调用中间件中的在视图函数之前调用的函数
【3】调用真正的视图函数,这里返回的是 Django 中的 Response 对象

后续的代码暂时不需要深究,后面就按照调试器返回。

这就是完整的请求。

标签:请求,05,对象,request,中间件,视图,方法,路由
From: https://www.cnblogs.com/yaowy001/p/17060178.html

相关文章