开发环境中我们通过‘python manage.py runserver ip:port’启动一个django服务,下面我们通过manage.py这个入口脚本,逐步解析django的启动过程。
# 创建日志目录
os.makedirs('../logs')
# 设置环境变量
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
# 解析传递给manage.py的参数,执行具体操作
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
optional arguments:
-h, --help show this help message and exit
--ipv6, -6 Tells Django to use an IPv6 address. # IPV6,默认不开
--nothreading Tells Django to NOT use threading. # 多线程,默认开启
--noreload Tells Django to NOT use the auto-reloader. # 自动重载,默认开启
--noasgi Run the old WSGI-based runserver rather than the ASGI-based one
--http_timeout HTTP_TIMEOUT
Specify the daphne http_timeout interval in seconds (default: no timeout)
--websocket_handshake_timeout WEBSOCKET_HANDSHAKE_TIMEOUT
Specify the daphne websocket_handshake_timeout interval in seconds (default: 5)
--version show program's version number and exit
-v {0,1,2,3}, --verbosity {0,1,2,3}
Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output
--settings SETTINGS The Python path to a settings module, e.g. "myproject.settings.main". If this isn't provided, the DJANGO_SETTINGS_MODULE environment variable will be used.
--pythonpath PYTHONPATH
A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
--traceback Raise on CommandError exceptions
--no-color Don't colorize the command output.
--force-color Force colorization of the command output.
上面代码可以知道,启动参数交给了‘django.core.management.init.execute_from_command_line’进行处理。
def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
# 初始化ManagementUtility类
utility = ManagementUtility(argv)
# 解析传递仅来的参数,以[‘manage.py’, 'runserver','0.0.0.0:8000',...]为例,执行具体命令
utility.execute()
上面可以了解到,访问参数继续交给了‘django.core.management.init.ManagementUtility’实例的‘execute’方法进行处理。
# 非关键代码省略
...
# 获得操作命令,runserver
subcommand = self.argv[1]
...
# 解析参数中传递的环境设置,如--settings和--pythonpath
options, args = parser.parse_known_args(self.argv[2:])
# 设置环境变量
handle_default_options(options)
....
# 检查配置中是否存在INSTALLED_APPS,不存在设置异常内容
try:
settings.INSTALLED_APPS
except ImproperlyConfigured as exc:
self.settings_exception = exc
except ImportError as exc:
self.settings_exception = exc
# 检查是否存在配置
if settings.configured:
# Start the auto-reloading dev server even if the code is broken.
# The hardcoded condition is a code smell but we can't rely on a
# flag on the command class because we haven't located it yet.
#
if subcommand == 'runserver' and '--noreload' not in self.argv:
try:
# 如果启动没有添加--noreload,则代码变动时自动重载
autoreload.check_errors(django.setup)()
except Exception:
...
# In all other cases, django.setup() is required to succeed.
else:
django.setup()
...
# self.fetch_command(subcommand)通过get_command和argv[1]获得具体模块的方法
# 通过‘if isinstance(app_name, BaseCommand):klass = app_name;’过滤返回BaseCommand类型的类
# 然后执行BaseCommand类型方法的run_from_argv方法,这里获得了django.contrib.staticfiles.management.commands.runserver.Command
self.fetch_command(subcommand).run_from_argv(self.argv)
上面代码了解到‘fetch_command’通过‘django.core.management.init.ManagementUtility.get_commands()’解析出运行的具体命令,
然后通过命令的run_from_argv启动服务。
# 装饰器作用,暂时缓存get_commands结果
@functools.lru_cache(maxsize=None)
def get_commands():
"""
Return a dictionary mapping command names to their callback applications.
Look for a management.commands package in django.core, and in each
installed application -- if a commands package exists, register all
commands in that package.
Core commands are always included. If a settings module has been
specified, also include user-defined commands.
The dictionary is in the format {command_name: app_name}. Key-value
pairs from this dictionary can then be used in calls to
load_command_class(app_name, command_name)
If a specific version of a command must be loaded (e.g., with the
startapp command), the instantiated module can be placed in the
dictionary in place of the application name.
The dictionary is cached on the first call and reused on subsequent
calls.
"""
# find_commands找到该目录下的commands目录中所有模块的类
# 这里找的是django.core.management.commands下的所有命令模块中的类
commands = {
name: 'django.core' for name in find_commands(__path__[0])}
# 如果没有额外配置了,则返回commands
if not settings.configured:
return commands
# 这里从配置中找到INSTALLED_APPS, 然后加载所有APP目录的management.commands目录下的命令模块中的类
# 如果App中模块与前边的同名,会覆盖之前的
# 由于'django.contrib.staticfiles'的management.commands目录下包含了runserver.py模块
# 这里会覆盖 django.core.management.commands下的runserver.py模块
for app_config in reversed(list(apps.get_app_configs())):
path = os.path.join(app_config.path, 'management')
commands.update({
name: app_config.name for name in find_commands(path)})
return commands
上面代码了解到,django首先去‘django.core.management.commands’获取命令模块,
然后通过注册的app下面的management.commands目录下方获取命令模块,并且覆盖前面获取到的同名模块。
实际上runserver执行的就是注册的staticfiles这个应用下面的runserer模块,下面对该模块进行解读。
from django.conf import settings
from django.contrib.staticfiles.handlers import StaticFilesHandler
from django.core.management.commands.runserver import (
Command as RunserverCommand,
)
class Command(RunserverCommand):
help = "Starts a lightweight Web server for development and also serves static files."
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
'--nostatic', action="store_false", dest='use_static_handler',
help='Tells Django to NOT automatically serve static files at STATIC_URL.',
)
parser.add_argument(
'--insecure', action="store_true", dest='insecure_serving',
help='Allows serving static files even if DEBUG is False.',
)
def get_handler(self, *args, **options):
"""
Return the static files serving handler wrapping the default handler,
if static files should be served. Otherwise return the default handler.
"""
handler = super().get_handler(*args, **options)
use_static_handler = options['use_static_handler']
insecure_serving = options['insecure_serving']
if use_static_handler and (settings.DEBUG or insecure_serving):
return StaticFilesHandler(handler)
return handler
该命令继承自‘django.core.management.commands.runserver.Command’,没有run_from_argv方法,继续向下解读。
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
help = "Starts a lightweight Web server for development."
# Validation is called explicitly each time the server is reloaded.
requires_system_checks = False
stealth_options = ('shutdown_message',)
default_addr = '127.0.0.1'
default_addr_ipv6 = '::1'
default_port = '8000'
protocol = 'http'
server_cls = WSGIServer
def add_arguments(self, parser):
# 这里使用的是django.contrib.staticfiles.management.commands.runserver.Command.add_arguments
...
def execute(self, *args, **options):
if options['no_color']:
# We rely on the environment because it's currently the only
# way to reach WSGIRequestHandler. This seems an acceptable
# compromise considering `runserver` runs indefinitely.
os.environ["DJANGO_COLORS"] = "nocolor"
super().execute(*args, **options)
def get_handler(self, *args, **options):
# 这里使用的是django.contrib.staticfiles.management.commands.runserver.Command.get_handler
...
def handle(self, *args, **options):
if not settings.DEBUG and not settings.ALLOWED_HOSTS:
raise CommandError('You must set settings.ALLOWED_HOSTS if DEBUG is False.')
self.use_ipv6 = options['use_ipv6']
if self.use_ipv6 and not socket.has_ipv6:
raise CommandError('Your Python does not support IPv6.')
self._raw_ipv6 = False
if not options['addrport']:
self.addr = ''
self.port = self.default_port
else:
m = re.match(naiveip_re, options['addrport'])
if m is None:
raise CommandError('"%s" is not a valid port number '
'or address:port pair.' % options['addrport'])
self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
if not self.port.isdigit():
raise CommandError("%r is not a valid port number." % self.port)
if self.addr:
if _ipv6:
self.addr = self.addr[1:-1]
self.use_ipv6 = True
self._raw_ipv6 = True
elif self.use_ipv6 and not _fqdn:
raise CommandError('"%s" is not a valid IPv6 address.' % self.addr)
if not self.addr:
self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr
self._raw_ipv6 = self.use_ipv6
self.run(**options)
def run(self, **options):
"""Run the server, using the autoreloader if needed."""
use_reloader = options['use_reloader']
if use_reloader:
autoreload.run_with_reloader(self.inner_run, **options)
else:
self.inner_run(None, **options)
def inner_run(self, *args, **options):
# If an exception was silenced in ManagementUtility.execute in order
# to be raised in the child process, raise it now.
autoreload.raise_last_exception()
threading = options['use_threading']
# 'shutdown_message' is a stealth option.
shutdown_message = options.get('shutdown_message', '')
quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
self.stdout.write("Performing system checks...\n\n")
self.check(display_num_errors=True)
# Need to check migrations here, so can't use the
# requires_migrations_check attribute.
self.check_migrations()
now = datetime.now().strftime('%B %d, %Y - %X')
self.stdout.write(now)
self.stdout.write((
"Django version %(version)s, using settings %(settings)r\n"
"Starting development server at %(protocol)s://%(addr)s:%(port)s/\n"
"Quit the server with %(quit_command)s.\n"
) % {
"version": self.get_version(),
"settings": settings.SETTINGS_MODULE,
"protocol": self.protocol,
"addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr,
"port": self.port,
"quit_command": quit_command,
})
try:
handler = self.get_handler(*args, **options)
run(self.addr, int(self.port), handler,
ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls)
except socket.error as e:
# Use helpful error messages instead of ugly tracebacks.
ERRORS = {
errno.EACCES: "You don't have permission to access that port.",
errno.EADDRINUSE: "That port is already in use.",
errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",
}
try:
error_text = ERRORS[e.errno]
except KeyError:
error_text = e
self.stderr.write("Error: %s" % error_text)
# Need to use an OS exit because sys.exit doesn't work in a thread
os._exit(1)
except KeyboardInterrupt:
if shutdown_message:
self.stdout.write(shutdown_message)
sys.exit(0)
django.contrib.staticfiles.management.commands.runserver继承自django.core.management.commands.runserver
django.core.management.commands.runserver继承自django.core.management.base.BaseCommand
下面从run_from_argv方法继续解析剩下的启动过程
...
# 进行一系列环境准备后,正式开始启动
# 首先执行django.core.management.commands.runserver.Command.execute,在这里判断是否设置‘DJANGO_COLORS’这个环境变量
# 然后执行自身的execute
self.execute(*args, **cmd_options)
# 这里尝试关闭数据库引擎连接
connections.close_all()
启动参数交给了‘django.core.management.base.BaseCommand.execute’继续处理。
...
# 执行系统检查
# 这里因为django.core.management.commands.runserver.Command.requires_system_checks=False,因此不执行
if self.requires_system_checks and not options.get('skip_checks'):
self.check()
# 检查数据库迁移,这里好像也没有执行
if self.requires_migrations_checks:
self.check_migrations()
# 重点来了,这里调用django.core.management.commands.runserver.Command.handle正式进入启动流程
output = self.handle(*args, **options)
...
启动参数交给了‘django.core.management.commands.runserver.Command.handle’继续处理。
下一篇,我们将从‘django.core.management.commands.runserver.Command.handle’继续剖析django的启动源码