前言:
【Django的那些事】系列将会分三个方面系统性的分析django框架的源码结构:django runserver 全生命周期、django请求到响应全过程、django rest framework框架及应用分别对应服务如何启动、如何处理client请求和响应、django CBV视图
序言:
django中通过python manange.py runserver ip+port命令便会启动django的服务,服务端就可以收到用户request然后respond,那么这个runserver是如何启动的,启动的是什么,下面的启动输出语句又是在哪里打印出来的,带着这些问题,我们依次分析整个runserver全生命周期。
一、django中的web服务器
django中使用的uWSGI服务器,那么uWSGI、WSGI、uwsgi这三者之间又是什么关系呢。
WSGI(Web Server Gateway Interface)是一种通信协议,wsgi server (比如uWSGI) 要和 wsgi application(比如django )交互,uWSGI需要将过来的请求转给django 处理,那么uWSGI 和 django的交互和调用就需要一个统一的规范,这个规范就是WSGI WSGI
uwsgi是一种线路协议而不是通信协议,在此常用于在uWSGI服务器与其他网络服务器的数据通信。
uWSGI是实现了uwsgi和WSGI两种协议的Web服务器,当然还包括http协议。
拓展:
提到通信则会想起最开始学习C语言的7种进程通信方式有名管道(pipe)、无名管道(fifo)、信号(signal)、消息队列(message queue)、共享内存(shared memory)、信号量(semaphore)、套接字(socket)。
uWSGI内部使用的是socket通信,taiga项目中还涉及到signal通信,socket的通信原理在这里就不再概述了,主要是客户端和服务端连接完成,服务端一直监听客户端请求。
二、django runserver 全生命周期源码分析
接下来会根据下图,从python manage.py runserver 到一直监听用户请求,整个过程结合源码进行分析【django 版本2.1.1】。
1、manage.py文件
def main():
"""
django服务入口函数:
1、设置项目配置文件
2、执行runserver命令
"""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'taiga_master.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
"""
1、sys.argv:获取当前文件外部执行传递的参数列表
argv = ["...manager.py","runserver","ip:port"]
2、启动服务
"""
execute_from_command_line(sys.argv)
这个是python manage.py 的入口函数,功能:
1、将项目的配置文件路径添加到系统环境变量中;
2、sys.argv捕获manage.py后面跟的命令参数:runserver 等,通过python manage.py 可以查看其它命令,比如 startproject、 startapp等众多功能。
2、django\core\management_init_.py
def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv)
utility.execute()
def execute(self):
"""
Given the command-line arguments, figure out which subcommand is being
run, create a parser appropriate to that command, and run it.
"""
try:
subcommand = self.argv[1]
except IndexError:
subcommand = 'help' # Display help if no arguments were given.
# Preprocess options to extract --settings and --pythonpath.
# These options could affect the commands that are available, so they
# must be processed early.
parser = CommandParser(usage='%(prog)s subcommand [options] [args]', add_help=False, allow_abbrev=False)
parser.add_argument('--settings')
parser.add_argument('--pythonpath')
parser.add_argument('args', nargs='*') # catch-all
try:
options, args = parser.parse_known_args(self.argv[2:])
handle_default_options(options)
except CommandError:
pass # Ignore any option errors at this point.
try:
"获取settings里面的app列表,但是还未加载app"
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:
"在django.setup里面真正加载并检测app格式"
autoreload.check_errors(django.setup)()
except Exception:
# The exception will be raised later in the child process
# started by the autoreloader. Pretend it didn't happen by
# loading an empty list of applications.
apps.all_models = defaultdict(OrderedDict)
apps.app_configs = OrderedDict()
apps.apps_ready = apps.models_ready = apps.ready = True
# Remove options not compatible with the built-in runserver
# (e.g. options for the contrib.staticfiles' runserver).
# Changes here require manually testing as described in
# #27522.
_parser = self.fetch_command('runserver').create_parser('django', 'runserver')
_options, _args = _parser.parse_known_args(self.argv[2:])
for _arg in _args:
self.argv.remove(_arg)
# In all other cases, django.setup() is required to succeed.
else:
django.setup()
self.autocomplete()
if subcommand == 'help':
if '--commands' in args:
sys.stdout.write(self.main_help_text(commands_only=True) + '\n')
elif not options.args:
sys.stdout.write(self.main_help_text() + '\n')
else:
self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0])
# Special-cases: We want 'django-admin --version' and
# 'django-admin --help' to work, for backwards compatibility.
elif subcommand == 'version' or self.argv[1:] == ['--version']:
sys.stdout.write(django.get_version() + '\n')
elif self.argv[1:] in (['--help'], ['-h']):
sys.stdout.write(self.main_help_text() + '\n')
else:
"""
1、获取命令,即获取是runserver还是startproject或者migrate等众多命令;
2、传递参数并运行此命令。
"""
self.fetch_command(subcommand).run_from_argv(self.argv)
这个文件主要看execute函数中
settings.INSTALLED_APPS、
autoreload.check_errors(django.setup)()、
self.fetch_command(subcommand).run_from_argv(self.argv)三个函数的功能:
(1)、settings.INSTALLED_APPS 位于django\conf_init_.py文件
# 懒加载
settings = LazySettings()
class LazySettings(LazyObject):
"""
A lazy proxy for either global Django settings or a custom settings object.
The user can manually configure settings prior to using them. Otherwise,
Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE.
"""
def _setup(self, name=None):
"""
Load the settings module pointed to by the environment variable. This
is used the first time settings are needed, if the user hasn't
configured settings manually.
"""
settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
if not settings_module:
desc = ("setting %s" % name) if name else "settings"
raise ImproperlyConfigured(
"Requested %s, but settings are not configured. "
"You must either define the environment variable %s "
"or call settings.configure() before accessing settings."
% (desc, ENVIRONMENT_VARIABLE))
self._wrapped = Settings(settings_module)
def __repr__(self):
# Hardcode the class name as otherwise it yields 'Settings'.
if self._wrapped is empty:
return '<LazySettings [Unevaluated]>'
return '<LazySettings "%(settings_module)s">' % {
'settings_module': self._wrapped.SETTINGS_MODULE,
}
def __getattr__(self, name):
"""Return the value of a setting and cache it in self.__dict__."""
if self._wrapped is empty:
self._setup(name)
val = getattr(self._wrapped, name)
self.__dict__[name] = val
return val
def __setattr__(self, name, value):
"""
Set the value of setting. Clear all cached values if _wrapped changes
(@override_settings does this) or clear single values when set.
"""
if name == '_wrapped':
self.__dict__.clear()
else:
self.__dict__.pop(name, None)
super().__setattr__(name, value)
def __delattr__(self, name):
"""Delete a setting and clear it from cache if needed."""
super().__delattr__(name)
self.__dict__.pop(name, None)
def configure(self, default_settings=global_settings, **options):
"""
Called to manually configure the settings. The 'default_settings'
parameter sets where to retrieve any unspecified values from (its
argument must support attribute access (__getattr__)).
"""
if self._wrapped is not empty:
raise RuntimeError('Settings already configured.')
holder = UserSettingsHolder(default_settings)
for name, value in options.items():
setattr(holder, name, value)
self._wrapped = holder
@property
def configured(self):
"""Return True if the settings have already been configured."""
return self._wrapped is not empty
@property
def DEFAULT_CONTENT_TYPE(self):
stack = traceback.extract_stack()
# Show a warning if the setting is used outside of Django.
# Stack index: -1 this line, -2 the caller.
filename, _line_number, _function_name, _text = stack[-2]
if not filename.startswith(os.path.dirname(django.__file__)):
warnings.warn(
DEFAULT_CONTENT_TYPE_DEPRECATED_MSG,
RemovedInDjango30Warning,
stacklevel=2,
)
return self.__getattr__('DEFAULT_CONTENT_TYPE')
@property
def FILE_CHARSET(self):
stack = traceback.extract_stack()
# Show a warning if the setting is used outside of Django.
# Stack index: -1 this line, -2 the caller.
filename, _line_number, _function_name, _text = stack[-2]
if not filename.startswith(os.path.dirname(django.__file__)):
warnings.warn(
FILE_CHARSET_DEPRECATED_MSG,
RemovedInDjango31Warning,
stacklevel=2,
)
return self.__getattr__('FILE_CHARSET')
def __init__(self):
# Note: if a subclass overrides __init__(), it will likely need to
# override __copy__() and __deepcopy__() as well.
self._wrapped = empty
class Settings:
def __init__(self, settings_module):
# update this dict from global settings (but only for ALL_CAPS settings)
"加载系统的setting配置文件"
for setting in dir(global_settings):
if setting.isupper():
setattr(self, setting, getattr(global_settings, setting))
# store the settings module in case someone later cares
self.SETTINGS_MODULE = settings_module
mod = importlib.import_module(self.SETTINGS_MODULE)
tuple_settings = (
"INSTALLED_APPS",
"TEMPLATE_DIRS",
"LOCALE_PATHS",
)
self._explicit_settings = set()
"加载用户setting配置文件"
for setting in dir(mod):
if setting.isupper():
setting_value = getattr(mod, setting)
if (setting in tuple_settings and
not isinstance(setting_value, (list, tuple))):
raise ImproperlyConfigured("The %s setting must be a list or a tuple. " % setting)
setattr(self, setting, setting_value)
self._explicit_settings.add(setting)
if not self.SECRET_KEY:
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
if self.is_overridden('DEFAULT_CONTENT_TYPE'):
warnings.warn(DEFAULT_CONTENT_TYPE_DEPRECATED_MSG, RemovedInDjango30Warning)
if self.is_overridden('FILE_CHARSET'):
warnings.warn(FILE_CHARSET_DEPRECATED_MSG, RemovedInDjango31Warning)
if hasattr(time, 'tzset') and self.TIME_ZONE:
# When we can, attempt to validate the timezone. If we can't find
# this file, no check happens and it's harmless.
zoneinfo_root = Path('/usr/share/zoneinfo')
zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split('/'))
if zoneinfo_root.exists() and not zone_info_file.exists():
raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE)
# Move the time zone info into os.environ. See ticket #2315 for why
# we don't do this unconditionally (breaks Windows).
os.environ['TZ'] = self.TIME_ZONE
time.tzset()
def is_overridden(self, setting):
return setting in self._explicit_settings
def __repr__(self):
return '<%(cls)s "%(settings_module)s">' % {
'cls': self.__class__.__name__,
'settings_module': self.SETTINGS_MODULE,
}
settings.INSTALLED_APPS主要功能是:加载系统配置和用户配置。这里把代码贴进来主要是因为这个模块使用了懒加载和很多魔法方法。
1、懒加载: 又叫延迟加载,这种方式确实推迟了加载的时间点。
比如:settings.INSTALLED_APPS 实例化settings对象,该对象内部没有属性INSTALLED_APPS,此时启动 魔法方法__getattr__
,在该方法进入_setup
方法里,最终在_setup方法里实例化Settings类并在__init__
函数里加载系统配置和用户配置。
2、魔法方法: python类的魔法方法包含几个类别:
比如构造初始化(__init__
,__new__
),
属性访问控制(__getattr__
,__setattr__
,__deleteattr__
,__getattribute__
),
描述器对象(__get__
,__set__
,__delete__
)以及构造自定义的容器。
在这里我们仅仅阐述我们用到的属性访问控制里面的__getattr__
,__setattr__
;
__getattr__
:实例化某个类,然后调用类的某个属性时会进入该函数;
__setattr__
:实例化某个类,在内部(包含__init__
,但不限于该方法)和外部对类的属性赋值时候先进入该方法,因此不要再该方法内部实现 self.xx = xx要用self.__dict__
[name] = xx,避免无限循环。
(2)、autoreload.check_errors(django.setup)()位于django\conf_init_.py文件
def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect of accessing the
first setting), configure logging and populate the app registry.
Set the thread-local urlresolvers script prefix if `set_prefix` is True.
"""
from django.apps import apps
from django.conf import settings
from django.urls import set_script_prefix
from django.utils.log import configure_logging
configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
if set_prefix:
set_script_prefix(
'/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME
)
"加载INSTALLED_APPS内app"
apps.populate(settings.INSTALLED_APPS)
def populate(self, installed_apps=None):
"""
Load application configurations and models.
Import each application module and then each model module.
It is thread-safe and idempotent, but not reentrant.
"""
if self.ready:
return
# populate() might be called by two threads in parallel on servers
# that create threads before initializing the WSGI callable.
with self._lock:
if self.ready:
return
# An RLock prevents other threads from entering this section. The
# compare and set operation below is atomic.
if self.loading:
# Prevent reentrant calls to avoid running AppConfig.ready()
# methods twice.
raise RuntimeError("populate() isn't reentrant")
self.loading = True
# Phase 1: initialize app configs and import app modules.
for entry in installed_apps:
if isinstance(entry, AppConfig):
app_config = entry
else:
app_config = AppConfig.create(entry)
if app_config.label in self.app_configs:
raise ImproperlyConfigured(
"Application labels aren't unique, "
"duplicates: %s" % app_config.label)
self.app_configs[app_config.label] = app_config
app_config.apps = self
# Check for duplicate app names.
counts = Counter(
app_config.name for app_config in self.app_configs.values())
duplicates = [
name for name, count in counts.most_common() if count > 1]
if duplicates:
raise ImproperlyConfigured(
"Application names aren't unique, "
"duplicates: %s" % ", ".join(duplicates))
self.apps_ready = True
# Phase 2: import models modules.
for app_config in self.app_configs.values():
app_config.import_models()
self.clear_cache()
self.models_ready = True
# Phase 3: run ready() methods of app configs.
for app_config in self.get_app_configs():
app_config.ready()
self.ready = True
self.ready_event.set()
在该段代码里面主要完成对导入app以及检查app名字等操作,在这里不做详细概述,主要介绍第三个也是最重要的函数。
(3)、self.fetch_command(subcommand).run_from_argv(self.argv)位于django\conf_init_.py文件
def fetch_command(self, subcommand):
"""
根据不同的manage.py命令生成不同的命令对象。
"""
# Get commands outside of try block to prevent swallowing exceptions
commands = get_commands()
try:
app_name = commands[subcommand]
except KeyError:
if os.environ.get('DJANGO_SETTINGS_MODULE'):
# If `subcommand` is missing due to misconfigured settings, the
# following line will retrigger an ImproperlyConfigured exception
# (get_commands() swallows the original one) so the user is
# informed about it.
settings.INSTALLED_APPS
else:
sys.stderr.write("No Django settings specified.\n")
possible_matches = get_close_matches(subcommand, commands)
sys.stderr.write('Unknown command: %r' % subcommand)
if possible_matches:
sys.stderr.write('. Did you mean %s?' % possible_matches[0])
sys.stderr.write("\nType '%s help' for usage.\n" % self.prog_name)
sys.exit(1)
if isinstance(app_name, BaseCommand):
# If the command is already loaded, use it directly.
klass = app_name
else:
"生成runserver命令对应的对象"
klass = load_command_class(app_name, subcommand)
return klass
def run_from_argv(self, argv):
"""
Set up any environment changes requested (e.g., Python path
and Django settings), then run this command. If the
command raises a ``CommandError``, intercept it and print it sensibly
to stderr. If the ``--traceback`` option is present or the raised
``Exception`` is not ``CommandError``, raise it.
"""
self._called_from_command_line = True
parser = self.create_parser(argv[0], argv[1])
options = parser.parse_args(argv[2:])
cmd_options = vars(options)
# Move positional args out of options to mimic legacy optparse
args = cmd_options.pop('args', ())
handle_default_options(options)
try:
self.execute(*args, **cmd_options)
except Exception as e:
if options.traceback or not isinstance(e, CommandError):
raise
# SystemCheckError takes care of its own formatting.
if isinstance(e, SystemCheckError):
self.stderr.write(str(e), lambda x: x)
else:
self.stderr.write('%s: %s' % (e.__class__.__name__, e))
sys.exit(1)
finally:
try:
connections.close_all()
except ImproperlyConfigured:
# Ignore if connections aren't setup at this point (e.g. no
# configured settings).
pass
def execute(self, *args, **options):
"""
Try to execute this command, performing system checks if needed (as
controlled by the ``requires_system_checks`` attribute, except if
force-skipped).
"""
if options['force_color'] and options['no_color']:
raise CommandError("The --no-color and --force-color options can't be used together.")
if options['force_color']:
self.style = color_style(force_color=True)
elif options['no_color']:
self.style = no_style()
self.stderr.style_func = None
if options.get('stdout'):
self.stdout = OutputWrapper(options['stdout'])
if options.get('stderr'):
self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func)
if self.requires_system_checks and not options.get('skip_checks'):
self.check()
if self.requires_migrations_checks:
self.check_migrations()
"self.handle跳转到django\core\management\commands\runserver.py的handdle方法"
output = self.handle(*args, **options)
if output:
if self.output_transaction:
connection = connections[options.get('database', DEFAULT_DB_ALIAS)]
output = '%s\n%s\n%s' % (
self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
output,
self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
)
self.stdout.write(output)
return output
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:
"ip地址、端口号re匹配"
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']
"根据runserver 后面是否有--noreload选在自动加载或者不加载"
if use_reloader:
autoreload.run_with_reloader(self.inner_run, **options)
else:
self.inner_run(None, **options)
def run_with_reloader(main_func, *args, **kwargs):
"进入自动加载模式"
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
try:
if os.environ.get(DJANGO_AUTORELOAD_ENV) == 'true':
reloader = get_reloader()
logger.info('Watching for file changes with %s', reloader.__class__.__name__)
start_django(reloader, main_func, *args, **kwargs)
else:
exit_code = restart_with_reloader()
sys.exit(exit_code)
except KeyboardInterrupt:
pass
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)
def get_handler(self, *args, **options):
"""Return the default WSGI handler for the runner."""
return get_internal_wsgi_application()
def get_internal_wsgi_application():
"""
Load and return the WSGI application as configured by the user in
``settings.WSGI_APPLICATION``. With the default ``startproject`` layout,
this will be the ``application`` object in ``projectname/wsgi.py``.
This function, and the ``WSGI_APPLICATION`` setting itself, are only useful
for Django's internal server (runserver); external WSGI servers should just
be configured to point to the correct application object directly.
If settings.WSGI_APPLICATION is not set (is ``None``), return
whatever ``django.core.wsgi.get_wsgi_application`` returns.
"""
from django.conf import settings
app_path = getattr(settings, 'WSGI_APPLICATION')
if app_path is None:
return get_wsgi_application()
try:
return import_string(app_path)
except ImportError as err:
raise ImproperlyConfigured(
"WSGI application '%s' could not be loaded; "
"Error importing module." % app_path
) from err
def get_wsgi_application():
"""
The public interface to Django's WSGI support. Return a WSGI callable.
Avoids making django.core.handlers.WSGIHandler a public API, in case the
internal WSGI implementation changes or moves in the future.
"""
django.setup(set_prefix=False)
"返回WSGIHandle对象"
return WSGIHandler()
def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer):
server_address = (addr, port)
if threading:
httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {})
else:
httpd_cls = server_cls
httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
if threading:
# ThreadingMixIn.daemon_threads indicates how threads will behave on an
# abrupt shutdown; like quitting the server by the user or restarting
# by the auto-reloader. True means the server will not wait for thread
# termination before it quits. This will make auto-reloader faster
# and will prevent the need to kill the server manually if a thread
# isn't terminating correctly.
httpd.daemon_threads = True
httpd.set_app(wsgi_handler)
httpd.serve_forever()
def serve_forever(self, poll_interval=0.5):
"""Handle one request at a time until shutdown.
Polls for shutdown every poll_interval seconds. Ignores
self.timeout. If you need to do periodic tasks, do them in
another thread.
"""
self.__is_shut_down.clear()
try:
# XXX: Consider using another file descriptor or connecting to the
# socket to wake this up instead of polling. Polling reduces our
# responsiveness to a shutdown request and wastes cpu at all other
# times.
with _ServerSelector() as selector:
selector.register(self, selectors.EVENT_READ)
while not self.__shutdown_request:
ready = selector.select(poll_interval)
if ready:
self._handle_request_noblock()
self.service_actions()
finally:
self.__shutdown_request = False
self.__is_shut_down.set()
这段代码最长,也是djang runserver最重要的,下面就结合源码详细阐述每个函数【上面源码中重要的地方添加了注释】
1、fetch_command(self, subcommand)
这个函数是根据python manage.py后面跟随的命令实例化命令的对象,跟runserver则实例化runserver对象,跟随startapp则实例化startapp对象。
2、run_from_argv(self, argv)
上面实例化对象以后就调用运行传入参数的命令函数,该函数里面再次设置了os.environ[‘DJANGO_SETTINGS_MODULE’] = options.settings,并且将python路径添加到系统环境中。sys.path.insert(0, options.pythonpath),最重要的是调用self.execute(*args, **cmd_options)函数。
3、self.execute(*
args,**
cmd_options)
该函数做了必要的检查,系统检查、migrations检查等,也就是在这里可能会报平常你没有进行django数据库的migrate操作,但是该函数最重要的还是self.handle(*args, **options),该方法被runserver.py文件中重写了。
4、handle(self,*
args,**
options)
该函数做了ip以及端口的检查,然后调用self.run(**options)。
5、run(self,**
options)
该函数主要是根据python manage.py runserver 后面是否有–noreload来选择是否开启新的进程,以便监听项目中文件的改动,从而自动的重新加载,这也就是你操作了项目文件后,看到终端自动重启服务的由来,如果默认没有传递noreload则会进入run_with_reloader()函数。
6、run_with_reloader(main_func,*
args,**
kwargs)
该函数会在执行的第一次进入restart_with_reloader()函数分支,并在其中调用subprocess.call(args, env=new_environ, close_fds=False)完成新进程的启动。该函数会在执行第二次进入start_django(reloader, main_func, *args, **kwargs)函数分支,并在其中启动新线程。
7、start_django(reloader, main_func,*
args,**
kwargs)
该函数启动新线程,并且启动django,然后调用reloader.run(django_main_thread)–>get_resolver().urlconf_module–>import_module(self.urlconf_name)完成对url的导入,也就是关联到settings里面设置的ROOT_URLCONF。
8、self.inner_run(None,**
options)
无论是否启动noreload都会进入inner_run(self, *args, **options)函数,在该函数中会检测是否配置use_threading,进而选择不同的httpd_cls类,该函数同样做了一些基础检查,并且输出下列信息:
也就是服务启动的信息,信息输出了但是函数还未执行完毕,self.get_handler(*args, **options)和run(self.addr, int(self.port), handler,
ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls)函数
9、get_handler(self,*
args,**
options)
该方法返回了get_internal_wsgi_application()函数。
10、get_internal_wsgi_application()
该函数返回了get_wsgi_application()函数,也就是项目自动生成的wsgi.py文件里面的函数,然后返回了WSGIHandler()对象。
11、run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer)
该函数根据前面传递的use_threading,选择不同的httpd_cls,然后执行httpd.serve_forever()方法,字面意思:永久服务也就是永久监听。到这里整个runserver启动流程也该结束了,进去最后一个方法。
12、serve_forever(self, poll_interval=0.5)
在该函数里面每隔0.5秒轮询查看用户请求,在 self._handle_request_noblock()函数中调用self.process_request(request, client_address),这个时候看process_request函数的参数,用户请求和client_adderss,这个是在接收到用户的请求后,开始处理请求以及响应。
三、总结
好了,从python manage.py runserver到加载系统、用户配置,然后初始化 加载app,然后到启动新进程监听文件改动自动加载、启动新线程url路由加载、系统检查、migarte检查、ip+端口号检查,实例化WSGIHandler对象、轮询监听用户请求。整个runserver生命周期到此也告一段落,下面的文章将会介绍一次完整的用户请求到返回响应。
本文也是通过对django项目进行百十次的断点调试才慢慢的找到其整个流程,如果有不对的地方,希望可以提出来,共同进步。
题外
推荐一个很好的django开源前后端分离项目:taiga
前端:
Augular JS:
1、MVC设计模式
2、双向数据绑定(MVVM)
3、依赖注入
4、采用模块化设计
后端:
django rest framework
1、基于类的视图 CBV
2、认证、权限、节流…