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的启动流程简单介绍到这里了,因为这个流程涉及到了很多的引用,不少引用的深度还挺深的,而我只是用空闲时间随便看看,没办法每个引用去寻根溯源,所以这里代码简化后可能有很多问题,描述可能也有很多问题,这里只是提出了我的一个思路,并不是什么权威解读,所以如果有错误的地方欢迎大家指正:)

posted @ 2020-05-26 09:57  seas  阅读(275)  评论(0编辑  收藏  举报