03-Django命令 python manage.py runserver
使用 Django 开发,一定离不开这条命令,python manage.py runserver [127.0.0.1:8000]
,这条命令是启动Django, 接下来就可以通过浏览器发起请求了。Django 肯定也会遵守 WSGI 协议,不出意外,我们开发的Django与WSGI服务器是完全解耦的,通过前面两篇博客,我们知道,WSGI 协议必定有一处地方启动 WSGI 服务器,然后可能会接收一个 Handler,并且WSGI服务器接收到请求的时候,生成一个Handler对象,然后Handler对象里面执行了一个方法【也可以有其它的形式, WSGI 协议的内容,后面会讲】,传入 environ 和 app,也就是WSGI协议的内容,服务器与APP的交互。
接下来我们通过断点调试寻找Django中的WSGI。
设置Django调试环境
从 manage.py 文件开始
可以看到 manage.py 中的main函数做了两件事,一件是导入了Django的配置文件,另外一件是将命令行的参数传入 execute_from_command_line 函数中
execute_from_command_line
execute_from_command_line 函数仅仅是生成了一个 ManagementUtility 对象,并且传入了命令行参数,然后执行了 ManagementUtility.execute()就结束了
所以我们需要进入 ManagementUtility 中一看究竟
ManagementUtility
ManagementUtility的初始化函数仅仅是完成赋值操作,没有任何奇特的地方。所以有必要看看 execute 方法。
ManagementUtility.execute
重点是我打断点的地方,前面的几乎都是依据选择做出配置项,看不出跟运行服务器相关的地方
由于 subcommand 的值是 runserver, 所以一定会走我打上断点的位置。看这段代码的字面意思,self.fetch_command(subcommand).run_from_argv(self.argv) 抓取一个 command,然后执行它的 run_from_argv 方法,抓取的 command 肯定是跟 runserver 相关啊,run_from_argv从参数开始运行。
这里我需要说明一下 Django 的目录结构,Django的每一个应用目录下都会有这么一个文件夹 management/commands 里面存放的是 Django 的脚本文件,我们按照固定的格式写脚本,然后使用 python manage.py xxx 来执行这个 xxx 脚本,而且并不需要完全的路径,Django 自己就会定位到具体的应用下的固定的脚本文件。而runserver也是一个脚本文件,她在哪里?她在 django包下的 core/management/commands 下。这些都是后话,后面的代码将会展示manage.py 是如何依据命令找到对应的脚本文件。这个方法就是 fetch_command,看名字也是他。
ManagementUtility.fetch_command
fetch_command 方法通过调用 get_commands 抓取所有的 commands,get_commands 非常暴力,它把整个Django系统中所有的 脚本文件都抓取出来了,而且我们也可以发现 runserver 的身影。不过我们先看看 get_commands
get_commands
@functools.lru_cache(maxsize=None)
是一个缓存机制,可以缓存函数的结果,暂时不予理会,有兴趣的可以去百度一下。
此处,先看【1】,path
每一个python包都有一个 __path__
变量,这是个内置变量,只在当前包中的__init__.py
文件中存在并且生效,各位可以去试一下,我们这里的代码正好处于 __init__.py
文件内,可以调用
于是 command_dir = os.path.join(management_dir, 'commands')
这一行代码等于是获取了 django.core.management.commands这个目录,接着 pkgutil.iter_modules
获取了所有的包,于是 find_commands
返回了所有的核心包下的所有脚本名
再看下面这段
这张图需要好好看看,我在实际调试过程中不是这个样子的,右侧调试器是没有那么多东西的,看代码我看不出 app_configs
是什么时候加载的,到目前为止,既然看不出来,那一定是我之前调试的时候跳过了某一步,也就是说 app_configs
早就被加载了,只是不知道什么时候,于是我这样打了断点
接着重新调试,没走几步就出现了前面那张复杂的图,在调试器中从下往上一层一层的看,原来是这里调用的。
上面这张图很复杂,不方便详细展开,读者可以自己调试看看,app_configs 早就加载了
于是这里通过同样的手段,得到了 settings 文件中注册的 APP,中提取了他们的所有的 commands 下的脚本文件。所以不光是 python manage.py runserver,其它的任意的脚本都是通过这种手段找到的。
load_command_class
load_command_class
意为加载 Command 类
import_module
做了一个导入动作,通过字符串拼接的手段,直接导入了 runserver 包,最后在返回的时候返回了一个 Command 对象,这个对象就是runserver 脚本文件中的 Command 类,熟悉Django的朋友应该知道这是个啥玩意儿,就不赘述了。但是需要注意的是,这个runserver可不是 core 下的runserver,而是 django.contrib.staticfiles.management.commands.runserver
这是怎么回事
django.contrib.staticfiles.management.commands.runserver
看到没,这个runserver并没有 command 方法,并且他继承自 django.core 下的runserver, 说明执行了它本质上还是执行了 django.core 下的runserver。
run_from_argv
可以看到 run_from_argv
中调用了 execute
, 而 execute
中调用了我们自己写脚本的时候的 handle 方法,这个方法是需要自定义的,也是功能代码的主体。其余的细节读者可以自己深挖一下。
django.core 的 runserver
前面的代码其实只是为了讲述为什么 python manage.py runserver 可以调用 runserver 脚本。在这里我们将会详细阐述 runserver 脚本是如何启动一个 WSGI 服务器,以及跟 Django 应用交互的入口。在这之前,既然前面的代码我已经全部梳理通了,就需要去掉所有的断点,然后从 runserver 中开始打断点,探索 Django 中的 WSGI 的源码。
runserver.Command
可以看到重新调试,确实进入了 django.core 的 runserver 中,handle 的方法前面都是设置参数,关键是最后一个 run 方法,而 run 方法内,不管怎样,最后都会执行 inner_run 方法
inner_run
inner_run 方法太长,所以分页展示为两块。
【1】校验迁移文件
【2,3】打印了我们每次启动 Django 开发测试的时候的一些信息
【4】获取了一个handler控制器。WSGI 服务器有以下几个部分:服务器[负责监听端口,转发请求], 控制器[负责解析 HTTP 请求头,并且使用服务器返回的 socket 与客户端进行交互], app[需要我们自定义的部分,真正的处理前端请求的代码]
【5】看起来就是启动了一个 WSGI 服务器
get_handler
【1】这个 get_handler 是 django.contrib.staticfiles 下的 runserver 里的,但是 django.contrib.staticfiles 下的 runserver 继承了 django.core 下的 runserve
【2】又回过头调用了父类 django.core 下的 runserve 中的 get_handler
【3】调用了 get_internal_wsgi_application
【4】尝试获取配置文件中配置的 WSGI_APPLICATION
,在我的项目中它是有值的,经过一系列寻找,他最后定位到了 django.core.handlers.wsgi.WSGIHandler
读者可以自己去查找一下,中间还要跳转几个文件,还涉及到我公司的项目,不方便展示,但是是可以找到的。
handler 已经找到了,接下来应该去看一下 run 方法了
run
先看左侧
【1】生成一个服务器对象
【2】设置app
【3】启动服务器
Django 也得遵守 WSGI 协议,在看右侧
【4】对应了服务器对象类
【5】处理请求的 Handler 类
【6】对应app,也就是那个 demo_app,但是这个名字取得很让人误会
到目前为止我们已经知道了 python manage.py runserver 确实开启了一个服务器,但是WSGI如何跟Django交互的我们还是没搞清楚,这将在下一篇博客继续探寻