Django框架浅析(一)
Django是一个十分重量级的框架,所以只能每次写一部分,没有办法一次性写完,更新频率要看心情😀
先来说说django常用的几个功能的实现:
runserver
首先入口点,肯定是manage.py了
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings") # 设置配置文件路径到环境变量
try:
from django.core.management import execute_from_command_line # 导包 命令行解析函数
except ImportError:
try:
import django # 导入django,验证环境中是否有django模块
except ImportError:
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?"
)
raise
execute_from_command_line(sys.argv) # 把命令行参数传入命令行解析函数
# 命令行解析函数
def execute_from_command_line(argv=None):
"""
A simple method that runs a ManagementUtility.
"""
utility = ManagementUtility(argv) # 这个类主要实现了命令行解析和处理,导入django配置文件,导包的模块加载等等,其实基本上django初始化过程应该都在这里了,这个类太长了这里就不放全部源码了
utility.execute() # ManagementUtility的execute方法主要实现了上述的命令行解析处理,导入django配置文件等等
# execute,删减了其中大部分代码
def execute(self):
try:
subcommand = self.argv[1]
except IndexError:
subcommand = 'help' # Display help if no arguments were given.
try:
settings.INSTALLED_APPS
except ImproperlyConfigured as exc:
self.settings_exception = exc
if settings.configured:
if subcommand == 'runserver' and '--noreload' not in self.argv: # 这里可以看出如果使用了runserver,就会调用以下的autoreload.check_errors方法,这个方法无可厚非,看名字也知道是个查错的,其实这就是个收集异常的闭包,这里不展开说了,django.setup这个函数参数在下方代码块,其实就是在配置日志和app
try:
autoreload.check_errors(django.setup)()
except Exception:
else:
django.setup()
self.autocomplete() # 初始化配置的,有兴趣可以看看,这里不说了。
if subcommand == 'help': # 这里判断是否是查询帮助,如果不是则正常启动
........
else:
self.fetch_command(subcommand).run_from_argv(self.argv) # 函数在下下方代码块
# django.setup
# 这里的注释可以看出,直接了当说明这函数干嘛的,配置setting,log和注册app
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.encoding import force_text
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 force_text(settings.FORCE_SCRIPT_NAME)
)
apps.populate(settings.INSTALLED_APPS) # 初始化app(注册app)这个不展开说了
# self.fetch_command(subcommand)
def fetch_command(self, subcommand):
"""
Tries to fetch the given subcommand, printing a message with the
appropriate command called from the command line (usually
"django-admin" or "manage.py") if it can't be found.
"""
# Get commands outside of try block to prevent swallowing exceptions
commands = get_commands()
"""
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)})
# 这里粘出get_commands关键代码,commands这个字典里面name是输入命令的名称例如runserver什么的,value是app_config.name,这个是刚才django.setup注册的模块的名称,不知道大家记得不记得django注册app的时候,django会在对应app的apps.py中写入
class APPsConfig(AppConfig):
name = 'apps'
这个name就是上面app_config.name取出来的,注意这个推导式的两个name不一样的,第一个name是遍历出来的,第二个app_config.name这个是直接取出来的,和遍历没关系。key-name是一个命令名字,而value-app_config.name是一个属性,不知道什么是命令的小伙伴可以查一下python的命令是什么,关于命令这里不再展开
最后返回的就是这么个字典
"""
try:
app_name = commands[subcommand]
except KeyError:
.... # 这里抛出异常忽略
if isinstance(app_name, BaseCommand):
# If the command is already loaded, use it directly.
klass = app_name
else:
klass = load_command_class(app_name, subcommand) # 这个我不展开了,从上下文可以看出来,作用就是把这个取出来的属性转化成BaseCommand
return klass # 返回的其实就是个BaseCommand对象
# self.fetch_command(subcommand).run_from_argv(self.argv)
# 上面我们知道了self.fetch_command(subcommand)返回的是个BaseCommand命令类对象,而run_from_argv是BaseCommand类的一个方法,那么run_from_argv(self.argv)我们来看看
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:
........
finally:
try:
connections.close_all()
except ImproperlyConfigured:
# Ignore if connections aren't setup at this point (e.g. no
# configured settings).
pass
# execute函数这里进行system检查,比如有无新的迁移文件,这里是承接上面函数的,也是BaseCommand的一个方法
"""
Performing system checks...
System check identified no issues (0 silenced).
May 26, 2020 - 08:28:53
Django version 1.11.11, using settings 'Apps.settings'
Starting development server at http://0.0.0.0:12345/
Quit the server with CONTROL-C.
这几句大家应该比较清楚,运行runserver时候出现的,这里的第二句就是这里输出的,其它的在下一个模块会介绍到,这一块的作用是捕获异常,如果是严重问题则不允许执行,Error级别以下的问题都会打印在std
"""
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).
"""
saved_locale = None
try:
if self.requires_system_checks and not options.get('skip_checks'):
self.check()
if self.requires_migrations_checks:
self.check_migrations()
output = self.handle(*args, **options) # 这里的handle作用是一个分发器,根据BaseCommand子类的重写实现不同的方法,下面的代码块是最后一个这次要看的模块,就是runserver.Command类,继承自BaseCommand
if output: # 有输出的话会进行数据库的操作,盲猜就迁移什么的handle有返回值吧😂
if self.output_transaction:
connection = connections[options.get('database', DEFAULT_DB_ALIAS)]
finally:
if saved_locale is not None:
translation.activate(saved_locale)
return output
# 为了方便查找我列出来路径:django.core.management.commands.runserver
class Command(BaseCommand):
help = "Starts a lightweight Web server for development." # 这个help很清晰了,这个类就是创建dev下的轻量webserver的
# Validation is called explicitly each time the server is reloaded.
requires_system_checks = False
leave_locale_alone = True
default_port = '8000'
protocol = 'http' # 不支持https,这里反手推荐一个包模块sslserver😉
server_cls = WSGIServer
def add_arguments(self, parser):
parser.add_argument(
'addrport', nargs='?',
help='Optional port number, or ipaddr:port'
)
parser.add_argument(
'--ipv6', '-6', action='store_true', dest='use_ipv6', default=False,
help='Tells Django to use an IPv6 address.',
)
parser.add_argument(
'--nothreading', action='store_false', dest='use_threading', default=True,
help='Tells Django to NOT use threading.',
)
parser.add_argument(
'--noreload', action='store_false', dest='use_reloader', default=True,
help='Tells Django to NOT use the auto-reloader.',
)
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[str("DJANGO_COLORS")] = str("nocolor")
super(Command, self).execute(*args, **options)
def get_handler(self, *args, **options):
"""
Returns the default WSGI handler for the runner.
"""
return get_internal_wsgi_application()
def handle(self, *args, **options):
from django.conf import settings
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 = '::1' if self.use_ipv6 else '127.0.0.1' # 是支持ipv6的
self._raw_ipv6 = self.use_ipv6
self.run(**options)
def run(self, **options):
"""
Runs the server, using the autoreloader if needed
"""
use_reloader = options['use_reloader']
if use_reloader:
autoreload.main(self.inner_run, None, 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')
if six.PY2:
now = now.decode(get_system_encoding())
self.stdout.write(now)
self.stdout.write((
# 往下看可以看出来,上一个代码块里面写的runserver启动时候的输出信息在这个模块都可以看到
"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光从参数也大致可以看出来,就是在启动server了,本来到这里我想结束了,因为再往下就是创建server的过程,其实已经不属于django内部操作了,但是突然发现了一个有意思的建类的写法,所以接下来我也来说说,见下方代码块
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 = force_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)
# 这里的run是basehttp.py的函数,路径django.core.servers.basehttp
def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer):
server_address = (addr, port)
if threading:
httpd_cls = type(str('WSGIServer'), (socketserver.ThreadingMixIn, server_cls), {}) # 没错我说的就是这句,刚开始看的我一愣,没遇到过这种用法,其实就是声明了类名WSGIServer,继承了(socketserver.ThreadingMixIn, server_cls)两个类,现在开始 我就用过了😁
'''
def __init__(cls, what, bases=None, dict=None): # 这个是type的init说明,可以看一看,下面很清晰的说明就是目前用的方法
"""
type(object_or_name, bases, dict)
type(object) -> the object's type
type(name, bases, dict) -> a new type
# (copied from class doc)
"""
pass
'''
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) # 对接application
httpd.serve_forever() # 启动server,这个不再追了,其实也追到底了,下面就是socketserver.py,有兴趣的进去看一看,TCPServer(BaseServer) UDPServer(TCPServer) ThreadingMixIn(这个眼熟吧,就上面建类用的,而建类继承的另一个类有兴趣追进去看看,追到最后其实就是继承的socketserver.TCPServer,也就是前面刚写的TCPServer(BaseServer))
'''
TCPServer(BaseServer):
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
"""Constructor. May be extended, do not override."""
BaseServer.__init__(self, server_address, RequestHandlerClass)
self.socket = socket.socket(self.address_family,
self.socket_type)
if bind_and_activate:
try:
self.server_bind()
self.server_activate()
except:
self.server_close()
raise
到这傻子也能看出来干啥的😜
'''
最后补充一点:
为什么说runserver不适合生产环境?
有些人说单线程运行,这个肯定是瞎扯,上面最后一个代码块中的ThreadingMixIn是来干嘛的?跑龙套吗?其次看下面的django doc:
--nothreading
The development server is multithreaded by default. Use the --nothreading option to disable the use of threading in the development server.
这个参数直接就说了,人家本身是个多线程的,当然Cpython多线程跟个屁一样这里就不说了
说单进程确实是对的,不过我先质疑一下,可以看到最后调用的是socketserver模块,这个模块可以说很基础了,想在上面二次开发个并发服务器应该没有问题,但是可惜的是依然是Python语言,运行速度和效率方面肯定不如c写的uwsgi和nginx了
还有一个原因其实在wsgiref.simple_server.py中已经阐述,那就是安全性,这个文件中定义了django所使用的WSGIServer这个类,其实是在HTTPServer上面封装的,这个再往上一层就是socketserver.TCPServe
"""BaseHTTPServer that implements the Python WSGI protocol (PEP 3333)
This is both an example of how WSGI can be implemented, and a basis for running
simple web applications on a local machine, such as might be done when testing
or debugging an application. It has not been reviewed for security issues,
however, and we strongly recommend that you use a "real" web server for
production use.
For example usage, see the 'if __name__=="__main__"' block at the end of the
module. See also the BaseHTTPServer module docs for other API information.
"""
如上,作者直接就说了,我就没有做安全审查,感觉就是个免责声明😄
对Django runserver的启动流程简单介绍到这里了,因为这个流程涉及到了很多的引用,不少引用的深度还挺深的,而我只是用空闲时间随便看看,没办法每个引用去寻根溯源,所以这里代码简化后可能有很多问题,描述可能也有很多问题,这里只是提出了我的一个思路,并不是什么权威解读,所以如果有错误的地方欢迎大家指正:)
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
有任何问题请随时交流~
Email: araise1@163.com