建立机制的重要性_机制的构成要素

建立机制的重要性_机制的构成要素细细的读读神级代码werkzeug,研究其具体的实现逻辑,以及代码细节。

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

werkzeug使用reloader可以在文件被改变时自动加载更改过的文件,使用方法也很简单,run_simple('localhost', 4000, application,use_reloader=True),ues_reloader=True即可。本文试图去品读一下reloader的实现以及一些小细节。

原理

先概述下整个reloader的原理,看起来会舒服一些。

非reloader的启动很简单,会调用make_server方法,然后调用serve_forever()去循环获取新的请求。

而reloader的机制,会起一个子进程,子进程有两个线程,一个线程会去跑server,一个线程去监控文件是否变动,如果文件发生变动,子进程会退出,并返回返回码3(自定义的返回码,标识因为文件变化而退出)。父进程检测子进程的退出码,并加以判断,如果是3,则重复上面的步骤,去再启动一次子进程,当然,此时加载的文件都会是新的文件了。

代码角度

接下来从代码的角度出发,看下整个流程。

入口

def inner():
    try:
        fd = int(os.environ['WERKZEUG_SERVER_FD'])
    except (LookupError, ValueError):
        fd = None
    srv = make_server(hostname, port, application, threaded,
                      processes, request_handler,
                      passthrough_errors, ssl_context,
                      fd=fd)
    if fd is None:
        log_startup(srv.socket)
    srv.serve_forever()

if use_reloader:
    # If we're not running already in the subprocess that is the
    # reloader we want to open up a socket early to make sure the
    # port is actually available.
    if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
        if port == 0 and not can_open_by_fd:
            raise ValueError('Cannot bind to a random port with enabled '
                             'reloader if the Python interpreter does '
                             'not support socket opening by fd.')

        # Create and destroy a socket so that any exceptions are
        # raised before we spawn a separate Python interpreter and
        # lose this ability.
        address_family = select_ip_version(hostname, port)
        s = socket.socket(address_family, socket.SOCK_STREAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind(get_sockaddr(hostname, port, address_family))
        if hasattr(s, 'set_inheritable'):
            s.set_inheritable(True)

        # If we can open the socket by file descriptor, then we can just
        # reuse this one and our socket will survive the restarts.
        if can_open_by_fd:
            os.environ['WERKZEUG_SERVER_FD'] = str(s.fileno())
            s.listen(LISTEN_QUEUE)
            log_startup(s)
        else:
            s.close()

    # Do not use relative imports, otherwise "python -m werkzeug.serving"
    # breaks.
    from werkzeug._reloader import run_with_reloader
    run_with_reloader(inner, extra_files, reloader_interval,
                      reloader_type)
else:
    inner()

上面就是use_reloader起作用部分的代码。可以看到,使用了use_reloader之后相比较没加做了很多事情(废话 = = )。接下去会挑这几行代码里的需要注意的点讲下。

  • WERKZEUG_RUN_MAIN

    WERKZEUG_RUN_MAIN在这里其实还没赋值,看不太出具体的作用,可以在后面再看。初始肯定是null,第一次执行这几行代码的时候是会进入到if语句的(实际上这几行代码在每次代码更新执行reloader的时候都会重复进入,后面再说)

  • can_open_by_fd

    这个参数是前面定义的,can_open_by_fd = not WIN and hasattr(socket, 'fromfd'),先不管windows系统下的情况,后面的fromdfd方法的解释如下

    create a socket object from an open file descriptor [*]

    即从文件描述符创建一个socket。后面会创建一个socket,并把socket的文件描述符保存起来,方面传递。(实际上会在父进程子进程之间进行传递)

  • socket.SO_REUSEADDR

    允许使用TIME_WAIT的端口。我们知道,TIME_WAIT状态下的端口是无法使用的,加上socket.SO_REUSEADDR参数后使这个socket的端口之后可以重复使用。

  • 为什么直接创建一个socket,而不是在inner中使用make_server去创建?

    因为需要传递fd,在整个程序的入口需要先行创建。在后边我们会看到,子进程回去使用fd去创建socket(或者说是从fd恢复socket)

  • inner

    use_reloader为true的情况下,fd是存在的,会运行一个server,并且使用该fd对应的socket

在处理完是否为WERKZEUG_RUN_MAIN的情况后,程序进入run_with_reloader方法。

run_with_reloader

def run_with_reloader(main_func, extra_files=None, interval=1, reloader_type='auto'):
    """Run the given function in an independent python interpreter."""
    import signal
    reloader = reloader_loops[reloader_type](extra_files, interval)
    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
    try:
        if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
            t = threading.Thread(target=main_func, args=())
            t.setDaemon(True)
            t.start()
            reloader.run()
        else:
            sys.exit(reloader.restart_with_reloader())
    except KeyboardInterrupt:
        pass

先往下看下if语句。同样的,第一次进入,还没赋值WERKZEUG_RUN_MAIN,会进去sys.exit(reloader.restart_with_reloader()),会把reloader.restart_with_reloader()的返回值作为程序的退出码。

reloader_loops是一个监控文件变化的类,有两个实现,分别是StatReloaderLoop以及WatchDogReloaderLoop,二者区别在于监控文件变动的方法不同。

ReloaderLoop

class ReloaderLoop(object):
    name = None
    _sleep = staticmethod(time.sleep)

    def __init__(self, extra_files=None, interval=1):
        # 接受 extra_files 参数,除了监控.py的变化以外,还会监控 extra_files 列表中所有文件的变化
        self.extra_files = set(os.path.abspath(x)
                               for x in extra_files or ())
        self.interval = interval

    def run(self):
        pass

    def restart_with_reloader(self):
        while 1:
            _log('info', ' * Restarting with %s' % self.name)
            # 获取到启动脚本,如['/usr/bin/python','test.py']
            args = _get_args_for_reloading()
            # 把环境变量(包括前面的fd等)
            new_environ = os.environ.copy()
            new_environ['WERKZEUG_RUN_MAIN'] = 'true'

            exit_code = subprocess.call(args, env=new_environ,
                                        close_fds=False)
            if exit_code != 3:
                return exit_code

    def trigger_reload(self, filename):
        self.log_reload(filename)
        sys.exit(3)

    def log_reload(self, filename):
        filename = os.path.abspath(filename)
        _log('info', ' * Detected change in %r, reloading' % filename)

trigger_reload方法是供子类去调用的,子类监控到文件的变化时会去调用trigger_reload,并且使进程退出,退出码为3(3在这里表示这因为文件变化而退出)

可以看到,ReloaderLooprestart_with_reloader方法会去启动一个子进程,并赋予所有的环境变量(包括fd),子进程会去带上WERKZEUG_RUN_MAIN参数重新去跑下前面的run_simple方法。并且会捕获子进程的退出码,如上面讲的,如果返回的是3的话,表示文件变化而倒是子进程退出,直接重启就好了,即继续循环,启动子进程;如果程序是因为其他原因退出的,则返回返回码。

子进程

接下来,我们看看子进程会做些什么。截止到上面的分析,我们知道,子进程相比较原先的父进程,目前唯一的泣别就是环境变量中WERKZEUG_RUN_MAIN为true,而这个字段会在两个地方会用到,一是最开始的if use_reloader:判断中,有这个字段的则不会去创建socket(毕竟父进程已经创建完成且把fd放在了环境变量中),二是run_with_reloader方法中。让我们再看下run_with_reloader

def run_with_reloader(main_func, extra_files=None, interval=1, reloader_type='auto'):
    """Run the given function in an independent python interpreter."""
    import signal
    reloader = reloader_loops[reloader_type](extra_files, interval)
    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
    try:
        if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
            t = threading.Thread(target=main_func, args=())
            t.setDaemon(True)
            t.start()
            reloader.run()
        else:
            sys.exit(reloader.restart_with_reloader())
    except KeyboardInterrupt:
        pass

子进程到达了这个方法,会启动一个线程,运行main_func方法,也就是最开始的inner方法,用来启动一个server,该线程会被设置为deamon线程,即守护线程。守护线程会在其他线程退出后自动退出。

另外,reloader会运行run()方法,作用是监控文件的变化,并调用trigger_reload方法,在文件发生变化时退出,并返回3返回码。

还有一点,signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)),这句看起来很简单,就是捕获signal.SIGTERM信号,也就是捕获kill或者是ctrl + c,并且退出。不过这里我还是有点疑问,为什么需要这个呢?加了信号之后唯一的区别,本来子进程退出会返回一个负数,加上之后会返回0。0代表着命令的成功执行,难道就是为了让程序更加’美丽’?

再看ReloaderLoop

到了这里,整个流程算是理通了,就是我们一开始的原理。但还有一个问题我们之前一直选择跳过,就是ReloaderLoop的具体实现。我们前面说到,他有两个实现,分别为StatReloaderLoop以及WatchdogReloaderLoop

reloader_loops = {
    'stat': StatReloaderLoop,
    'watchdog': WatchdogReloaderLoop,
}

try:
    __import__('watchdog.observers')
except ImportError:
    reloader_loops['auto'] = reloader_loops['stat']
else:
    reloader_loops['auto'] = reloader_loops['watchdog']

接下来,我们会细致得去看一下具体的实现。

StatReloaderLoop
class StatReloaderLoop(ReloaderLoop):
    name = 'stat'

    def run(self):
        mtimes = {}
        while 1:
            for filename in chain(_iter_module_files(),
                                  self.extra_files):
                try:
                    mtime = os.stat(filename).st_mtime
                except OSError:
                    continue

                old_time = mtimes.get(filename)
                if old_time is None:
                    mtimes[filename] = mtime
                    continue
                elif mtime > old_time:
                    self.trigger_reload(filename)
            self._sleep(self.interval)

StatReloaderLoop的实现很简单,就是挨个去看文件的上次修改时间来确认文件是否发生改变,需要注意的是,如果interval比较小而文件又比较多的情况下,这个方法会很耗资源(显而易见),剩下的没啥好说的…

WatchdogReloaderLoop

WatchdogReloaderLoop依赖了第三方的库watchdog,这是一个可以监控文件变化的库,跨平台,运维用的比较多。他允许自定义监控一系列文件的变化,并在变化时调用相应的handler。

class WatchdogReloaderLoop(ReloaderLoop):

    def __init__(self, *args, **kwargs):
        ReloaderLoop.__init__(self, *args, **kwargs)
        from watchdog.observers import Observer
        from watchdog.events import FileSystemEventHandler
        self.observable_paths = set()

        # 根据发生变化的文件名,确定是否需要重启(如果变动了一个不重要的小文件就没必要重启了)
        def _check_modification(filename):
            if filename in self.extra_files:
                self.trigger_reload(filename)
            dirname = os.path.dirname(filename)
            if dirname.startswith(tuple(self.observable_paths)):
                if filename.endswith(('.pyc', '.pyo', '.py')):
                    self.trigger_reload(filename)

        # 定义一个处理器类,分别处理不懂改变时要做的事(都是调用_check_modification方法)
        class _CustomHandler(FileSystemEventHandler):
            def on_created(self, event): _check_modification(event.src_path)
            def on_modified(self, event):  ...         
            def on_moved(self, event): ...
            def on_deleted(self, event): ...

        self.observer_class = Observer
        self.event_handler = _CustomHandler()
        self.should_reload = False

    def trigger_reload(self, filename):
        # 调用会发生在handler中,退出没什么卵用,所以覆写了这个方法,让run循环自动退出
        self.should_reload = True
        self.log_reload(filename)

    def run(self):
        watches = {}
        observer = self.observer_class()
        observer.start()

        try:
            while not self.should_reload:
                # 使用watchdog去检查文件是否发生变化,并使用handler去处理。
        finally:
           # observer是一个线程,让observer也正常退出
            observer.stop()
            observer.join()
        # 返回3,标识文件发生变化
        sys.exit(3)

这部分代码很长,我把不太重要的省略掉了。代码比较简单,注释都卸载里边了,简单的说就是使用watchdog的方式去调用处理文件变化的事件,并按正常流程退出。

小结

werkzeug的代码真的很神,很多可以看的地方,比如父进程通过环境变量给子进程传递信息,父进程创建socket并获取其fd,子进程通过fd去创建socket,即便在重启的过程中也不至于connection refused,再比如使用退出码让子进程给父进程传递信息,再比如清晰的逻辑,各个环节的划分,reloader具体实现类的抽象等,都很值得学习。

我在看这代码之前想了很久,如果我来做reloader机制会如何去做,反正我做能实现功能就不错了…希望自己的代码有一天能这么好看吧。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/185166.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • idea配置svn地址

    idea配置svn地址idea会报如下错误:说明要有svn客户端面命令,可是一般个人开发本地不需要安装svn,所以,进行如下操作:file->settings将红圈处的对勾取消

  • jax java,用JAX WS实现java调用webServic「建议收藏」

    jax java,用JAX WS实现java调用webServic「建议收藏」来个简单点的:1.建个具体的服务实现:packagecom.webservice;@WebServicepublicclassWarehouse{privateMapprices;publicWarehouse(){prices=newHashMap();prices.put(“BlackwellToaster”,24.95);prices.put(“ZapXpress…

  • matlab输出语句fprintf例子_matlab中compose函数

    matlab输出语句fprintf例子_matlab中compose函数golang需要什么基础?_后端开发golang需要的基础是:首先初学Go语言要弄懂基础语法和概念;然后掌握文件操作、网络编程、锁、协程、对象序列化和反序列化,以及各种数据格式的封装等;最后接触数据库等,就可以模块化开发。matlab中fprintf函数的用法详解:fprintf函数可以将数据按指定格式写入到文本文件中。其调用格式为:数据的格式化输出:fprintf(fid,format,v…

  • 女生学java_Java Server Pages

    女生学java_Java Server Pages/*身份证号码的结构和表示形式<br>1、号码的结构<br>公民身份号码是特征组合码,由十七位数字本体码和一位校验码组成。排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。<br>2、地址码<br>表示编码对象常住户口所在县(市、旗、区)的行政区划代码,按GB/T2260的规定执行。<br>3、出生日期码&

  • Python–csv文件处理建议收藏

    CSV(Comma-SeparatorValues)逗号分割值,由于是纯文本文件,任何编辑器都可以打开。下面用csv和pandas两种方式进行csv文件操作原始csv文件内容1.csv包操作c

    2021年12月18日
  • 数据分析之数据预处理、分析建模、可视化[通俗易懂]

    概述:简介、思路、发展历程、应用领域、开发流程;数据类型:结构化与非结构化、定性与定量、截面数据与时间序列数据;数据来源:外部来源、内部来源;数据预处理:数据清洗、数据集成、数据规约、数据变换;数据分析模型:对比分析、漏斗分析、留存分析、A/B测试、用户行为路径分析、用户分群、用户画像分析等;数据分析方法:描述统计、假设检验、信度分析、相关分析、方差分析、回归分析、聚类分析、判别分析、主成分分析、因子分析、时间序列分析等;数据可视化:Excel、PowerBI、Tableau、Python;

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号