django源码分析——本地runserver分析

Posted thinheader

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了django源码分析——本地runserver分析相关的知识,希望对你有一定的参考价值。

本文环境python3.5.2,django1.10.x系列

1.根据上一篇文章分析了,django-admin startproject与startapp的分析流程后,根据django的官方实例此时编写好了基本的路由和相应的处理函数,此时需要调试我们写的接口此时本地调试,django框架提供了python manage.py runserver 命令来本地调试。
2.runserver的特点是启用多线程处理请求,并可以监控当文件修改后自动重启服务,以达到服务重启极大方便了本地的调试,根据官方文档该命令只用于本地调试,不简易部署到生成环境。
3.当生成项目后,找到manage.py文件,其实代码与django-admin代码一样,都是调用django注册的方法。
我们直接分析runserver的命令位于django/core/management/commands/runserver.py,由于上一篇博文已经分析过具体的调用过程,我们直接就看其中的Command定义:

 

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  
    leave_locale_alone = True

    default_port = ‘8000‘                                                               # 默认启动服务监听的端口

    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:                                   # 检查是否是debug模式,如果不是则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:                                               # 检查输入参数中是否是ipv6格式,检查当前python是否支持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‘])                                       # 检查匹配的ip格式
            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:                                                                       # 检查解析出的地址是否合法的ipv6地址
                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:                                                                       # 如果没有输入ip地址则使用默认的地址
            self.addr = ‘::1‘ if self.use_ipv6 else ‘127.0.0.1‘
            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‘]                                                  # 根据配置是否自动加载,如果没有输入则default=True                                  

        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...

")                                    # 想标准输出输出数据
        self.check(display_num_errors=True)                                                     # 检查
        # Need to check migrations here, so can‘t use the
        # requires_migrations_check attribute.
        self.check_migrations()                                                                 # 检查是否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((
            "Django version %(version)s, using settings %(settings)r
"
            "Starting development server at http://%(addr)s:%(port)s/
"
            "Quit the server with %(quit_command)s.
"
        ) % {
            "version": self.get_version(),
            "settings": settings.SETTINGS_MODULE,
            "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)                                        # 获取信息处理的handler,默认返回wsgi
            run(self.addr, int(self.port), handler,
                ipv6=self.use_ipv6, threading=threading)                                        # 调用运行函数
        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)

 

该command主要进行了检查端口是否正确,是否多线程开启,是否开启了文件监控自动重启功能,如果开启了自动重启功能则,

   autoreload.main(self.inner_run, None, options) 

 

调用django/utils/autoreload.py中的mian函数处理,如下所示:

def main(main_func, args=None, kwargs=None):
    if args is None:
        args = ()
    if kwargs is None:
        kwargs = {}
    if sys.platform.startswith(‘java‘):                         # 获取当前reloader的处理函数
        reloader = jython_reloader
    else:
        reloader = python_reloader  

    wrapped_main_func = check_errors(main_func)                 # 添加对man_func的出错处理方法
    reloader(wrapped_main_func, args, kwargs)                   # 运行重启导入函数

 

目前电脑运行的环境reloader为python_reloader,查看代码为:

def python_reloader(main_func, args, kwargs):
    if os.environ.get("RUN_MAIN") == "true":                           # 获取环境变量是RUN_MAIN是否为"true"
        thread.start_new_thread(main_func, args, kwargs)               # 开启子线程运行服务程序
        try:
            reloader_thread()                                          # 调用监控函数
        except KeyboardInterrupt:
            pass
    else:
        try:
            exit_code = restart_with_reloader()                        # 调用重启函数
            if exit_code < 0:
                os.kill(os.getpid(), -exit_code)
            else:
                sys.exit(exit_code)
        except KeyboardInterrupt:
            pass

 

第一次运行时,RUN_MAIN没有设置,此时会运行restart_with_reloader函数

def restart_with_reloader():
    while True:
        args = [sys.executable] + [‘-W%s‘ % o for o in sys.warnoptions] + sys.argv    # 获取当前运行程序的执行文件
        if sys.platform == "win32":
            args = [‘"%s"‘ % arg for arg in args] 
        new_environ = os.environ.copy()                                               # 拷贝当前的系统环境变量
        new_environ["RUN_MAIN"] = ‘true‘                                              # 设置RUN_MAIN为true
        exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ)          # 启动新进程执行当前代码,如果进程主动退出返回退出状态吗
        if exit_code != 3:                                                            # 如果返回状态码等于3则是因为监控到文件有变化退出,否则是其他错误,就结束循环退出
            return exit_code

 

该函数主要是对当前执行的可执行文件进行重启,并且设置环境变量RUN_MAIN为true,此时再重新运行该程序时,此时再python_reloader中执行:

    if os.environ.get("RUN_MAIN") == "true":                           # 获取环境变量是RUN_MAIN是否为"true"
        thread.start_new_thread(main_func, args, kwargs)               # 开启子线程运行服务程序
        try:
            reloader_thread()                                          # 调用监控函数
        except KeyboardInterrupt:
            pass

 

开启一个子线程执行运行服务的主函数,然后重启进程执行reloader_thread函数:

def reloader_thread():
    ensure_echo_on()
    if USE_INOTIFY:                                                         # 如果能够导入pyinotify模块
        fn = inotify_code_changed                                           # 使用基于pyinotify的文件监控机制
    else:
        fn = code_changed                                                   # 使用基于对所有文件修改时间的判断来判断是否进行文件的更新
    while RUN_RELOADER:
        change = fn()                                                       # 获取监控返回值
        if change == FILE_MODIFIED:                                         # 如果监控到文件修改则重启服务运行进程
            sys.exit(3)  # force reload
        elif change == I18N_MODIFIED:                                       # 监控是否是本地字符集的修改
            reset_translations()
        time.sleep(1)                                                       # 休眠1秒钟

 

此时主进程会一直循环执行该函数,间隔一秒调用代码变化监控函数执行,如果安装了pyinotify,则使用该模块监控代码是否变化,否则就使用django自己实现的文件监控,在这里就分析一下django自己实现的代码是否变化检测函数:

def code_changed():
    global _mtimes, _win
    for filename in gen_filenames():
        stat = os.stat(filename)                                                        # 获取每个文件的状态属性
        mtime = stat.st_mtime                                                           # 获取数据最后的修改时间
        if _win:
            mtime -= stat.st_ctime
        if filename not in _mtimes:
            _mtimes[filename] = mtime                                                   # 如果全局变量中没有改文件则存入,该文件的最后修改时间
            continue
        if mtime != _mtimes[filename]:                                                  # 如果已经存入的文件的最后修改时间与当前获取文件的最后修改时间不一致则重置保存最后修改时间变量
            _mtimes = {}
            try:
                del _error_files[_error_files.index(filename)]
            except ValueError:
                pass
            return I18N_MODIFIED if filename.endswith(‘.mo‘) else FILE_MODIFIED         # 如果修改的文件是.mo结尾则是local模块更改,否则就是项目文件修改需要重启服务
    return False

 

首先,调用gen_filenames函数,该函数主要是获取要监控项目的所有文件,然后将所有文件的最后编辑时间保存起来,当第二次检查时比较是否有新文件添加,旧文件的最后编辑时间是否已经改变,如果改变则重新加载:

def gen_filenames(only_new=False):
    """
    Returns a list of filenames referenced in sys.modules and translation
    files.
    """
    # N.B. ``list(...)`` is needed, because this runs in parallel with
    # application code which might be mutating ``sys.modules``, and this will
    # fail with RuntimeError: cannot mutate dictionary while iterating
    global _cached_modules, _cached_filenames
    module_values = set(sys.modules.values())                                       # 获取模块的所有文件路径
    _cached_filenames = clean_files(_cached_filenames)                              # 检查缓存的文件列表
    if _cached_modules == module_values:                                            # 判断所有模块是否与缓存一致
        # No changes in module list, short-circuit the function
        if only_new:
            return []
        else:
            return _cached_filenames + clean_files(_error_files)

    new_modules = module_values - _cached_modules
    new_filenames = clean_files(
        [filename.__file__ for filename in new_modules
         if hasattr(filename, ‘__file__‘)])                                         # 检查获取的文件是否存在,如果存在就添加到文件中

    if not _cached_filenames and settings.USE_I18N:
        # Add the names of the .mo files that can be generated
        # by compilemessages management command to the list of files watched.
        basedirs = [os.path.join(os.path.dirname(os.path.dirname(__file__)),
                                 ‘conf‘, ‘locale‘),
                    ‘locale‘]
        for app_config in reversed(list(apps.get_app_configs())):
            basedirs.append(os.path.join(npath(app_config.path), ‘locale‘))         # 添加项目目录下的locale
        basedirs.extend(settings.LOCALE_PATHS)
        basedirs = [os.path.abspath(basedir) for basedir in basedirs
                    if os.path.isdir(basedir)]                                      # 如果现在的文件都是文件夹,将文件夹添加到路径中去
        for basedir in basedirs:
            for dirpath, dirnames, locale_filenames in os.walk(basedir):
                for filename in locale_filenames:
                    if filename.endswith(‘.mo‘):
                        new_filenames.append(os.path.join(dirpath, filename))

    _cached_modules = _cached_modules.union(new_modules)                            # 添加新增的模块文件
    _cached_filenames += new_filenames                                              # 将新增的文件添加到缓存文件中
    if only_new:
        return new_filenames + clean_files(_error_files)
    else:
        return _cached_filenames + clean_files(_error_files)


def clean_files(filelist):
    filenames = []
    for filename in filelist:                                                       # 所有文件的全路径集合
        if not filename:                                                            
            continue
        if filename.endswith(".pyc") or filename.endswith(".pyo"):                  # 监控新的文件名
            filename = filename[:-1]
        if filename.endswith("$py.class"): 
            filename = filename[:-9] + ".py"
        if os.path.exists(filename):                                                # 检查文件是否存在,如果存在就添加到列表中
            filenames.append(filename)
    return filenames 

 

至此,django框架的自动加载功能介绍完成,主要实现思路是, 
1.第一次启动时,执行到restart_with_reloader时,自动重头执行加载 
2.第二次执行时,会执行python_reloader中的RUN_MAIN为true的代码 
3.此时开启一个子线程执行服务运行程序,主进程进行循环,监控文件是否发生变化,如果发生变化则sys.exit(3),此时循环程序会继续重启,依次重复步骤2 
4.如果进程的退出code不为3,则终止整个循环

当监控运行完成后继续执行self.inner_run函数,当执行到

            handler = self.get_handler(*args, **options)                                        # 获取信息处理的handler,默认返回wsgi
            run(self.addr, int(self.port), handler,
                ipv6=self.use_ipv6, threading=threading)                                        # 调用运行函数

 

获取wsgi处理handler,然后调用django/core/servers/basshttp.py中的run方法

def run(addr, port, wsgi_handler, ipv6=False, threading=False):
    server_address = (addr, port)                                                               # 服务监听的地址和端口
    if threading:                                                                               # 如果是多线程运行
        httpd_cls = type(str(‘WSGIServer‘), (socketserver.ThreadingMixIn, WSGIServer), {} )     # 生成一个继承自socketserver.ThreadingMixIn, WSGIServer的类
    else:
        httpd_cls = WSGIServer
    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)                                                                 # 设置服务类的处理handler
    httpd.serve_forever()  

 

调用标准库中的wsgi处理,django标准库的wsgi处理再次就不再赘述(在gunicorn源码分析中已经分析过),所以本地调试的server依赖于标准库,django并没有提供高性能的server端来处理连接,所以不建议使用该命令部署到生产环境中。





以上是关于django源码分析——本地runserver分析的主要内容,如果未能解决你的问题,请参考以下文章

django python manage.py runserver 流程

Django扩展自定义manage命令

Django扩展自定义manage命令

Win7环境下Apache+mod_wsgi本地部署Django

Django源码分析1:创建项目和应用分析

Django学习——forms组件源码分析