使用POE进行应用设计

日复一日,我用Perl编写大型应用程序。告诉你,我被诅咒了。虽然纯Perl编写的大规模、长时间运行的应用程序听起来相当容易,但实际上并非如此。Perl在达到一定的大小和复杂性之后,如果不非常小心,就很难管理。正确选择应用程序框架有助于最大限度地减少这种困难。对于许多应用程序来说,Apache和mod_perl是非常有意义的。这对于用户界面应用程序和数据显示系统来说是一个很好的选择。然而,HTML和万维网对于许多形式的长运行应用程序来说并没有意义,特别是基于网络的服务器。Apache绝对不是syslog监控或边缘主机流量分析的合适选择。

我首选的框架是POE。POE是一个单线程、事件驱动、协作多任务的Perl环境。基本上,POE是一个应用程序框架,其中单个线程的Perl进程等待事件发生,然后相应地执行。这个事件循环是POE进程的核心。

如果POE仅提供事件循环,那么就没有太多可说的了。POE也不会特别突出。CPAN上已经存在几个提供类似功能的循环模块。Event、Coro、IO::Events和IO::Poll都提供了类似的功能。然而,任何有价值的应用程序都需要比简单的动作集合更多。

会话

POE程序从“会话”开始。每个会话代表一个协作的多任务状态机。

    POE::Session->create(
        inline_states => {
            _start => \&start,
            _stop => \&stop,

            do_something => \&do_something,
        },
        heap => {
            'some' => 'data',
        },
    );

会话在某种程度上类似于线程,因为它们有一个独特的运行时上下文和一个半私有数据存储(称为“堆”)。每个会话独立于其他会话运行,从POE内核接收时间片。重要的是要记住,尽管与线程相似,但所有POE会话都在同一个单线程进程中运行。

会话提供了一些简单、易于理解的构建块,可以在此基础上构建更复杂的应用程序。POE提供了一种给会话命名的方法,称为别名,它可以唯一地识别会话本身之外。使用$poe_kernel->alias_set($alias)为当前会话设置别名。然后,进程中的任何POE会话都可以使用命名标识符向该会话发送事件。

    if($door_bell) {
        $poe_kernel->post( $alias => 'pizza' );
    }

远程寻址提供了一种在应用程序内部实现服务模型的机制。不同的会话为应用程序提供不同的服务。一个会话可能提供DNS解析,而另一个可能提供数据存储。使用常见的名称,可能存储在配置文件中,中央应用程序变得更加小巧且易于管理。

组件

POE组件为类似服务的POE会话提供抽象API。而不是每次在会话找到新的用途时都复制会话构建调用和相关子例程,更好的办法是将所有这些代码集成到一个Perl模块中。

    package POE::Component::MyService;

    sub create {
        POE::Session->create(
            # ...
        );
    }

    sub start {
        $poe_kernel->alias_set(__PACKAGE__);
    }


    ####


    #!/usr/bin/perl
    use POE;
    use POE::Component::MyService;

    POE::Component::MyService->create();
    POE::Kernel->run();

POE社区为这些模块创建了一个标准的命名空间POE::Component。通常,它们有一个名为create()spawn()的构造函数,并通过会话为POE应用程序提供一项服务。除了这些简单的规则之外,组件可以自由地做任何必要的事情来履行其目的。POE::Component::Server::Syslog,例如,启动一个UDP监听器,并通过回调提供syslog数据。POE::Component::RSS通过别名接收RSS内容,并通过特殊命名的事件调用交付数据。POE::Component::IRC遵循类似模式。

轮子

对于某些任务,完整的会话是不必要的。有时,改变现有会话的能力以提供所需的功能更有意义。POE有一个特殊的命名空间POE::Wheel,用于修改或改变当前会话的能力以提供某些新功能。

    package POE::Wheel::MyFunction;

    sub new {
        # ...
    }

    ####

    #!/usr/bin/perl
    use POE;
    use POE::Wheel::MyFunction;

    POE::Session->create(
        #...
        foo => \&foo,
    );

    POE::Kernel->run();

    sub start {
        POE::Wheel::MyFunction->new(
            FooState => 'foo'
        );
    }

组件通常像POE::Session一样使用子程序回调,而轮子使用本地事件名来提供功能。在内部,它们围绕对这些事件的调用创建包装器,这些包装器构建了POE事件发生的必要上下文。

创建轮子要复杂得多,这是有充分理由的。轮子与其用户会话共享整个操作上下文,但共享的优美之处却很少。轮子没有自己的堆,也不能为自己创建别名。在许多方面,它们就像一个附着在用户代码侧边的寄生虫。只要它们不妨碍事情,并且提供有用的功能,它们就可以存在。

然而,开发开销通过损失内部POE开销来弥补。会话需要一定的维护来保持运行。POE检查会话是否还有工作要做,是否有待处理的计时器或闹钟,是否应该进行垃圾回收等。系统中的会话越多,这种开销就越大。这种开销在时间敏感的应用程序中尤其明显。轮子没有任何这种开销。它们骑在用户会话之上,因此,除了它们在正常操作中可能触发的任何事件外,使用轮子本身就没有固有的内部POE开销。

过滤器

许多轮子处理传入和传出数据。它们存在是为了帮助用户将来自奇怪来源(例如HTTP)的数据转换为用户可以分析或以Perl方式拆分的格式。《code>POE::Wheel::SocketFactory,例如,处理非阻塞套接字创建和维护的所有可怕事情。然而,对于大多数人来说,SocketFactory还不够。我不想担心打包调用或HTTP头部或其他任何必要的废话,以从线路上取下一个事务并使其变得美味。在《code>POE::Filter命名空间中的特殊模块处理这种苦差事。

    package POE::Filter::MyData;

    sub new {
        # ...
    }

    sub put {
        # ...
    }

    sub get {
        # ...
    }

过滤器是非常简单的数据解析模块。大多数POE过滤器足够有限,可以在POE环境中使用。它们对POE或运行中的POE环境一无所知。标准接口需要三个方法:《code>new(),构造函数;《code>get(),输入解析器;《code>put(),输出生成器。《code>get()接收数据流并返回解析记录,这些记录可能是散列、数组、对象或任何其他可能需要的东西。《code>put()接收用户生成的记录并将它们转换为原始数据。

设计

有了这四个简单的构建块,POE应用程序可以扩展以满足几乎任何需求,同时仍然易于维护。关键是将应用程序拆分成小块。这有两个主要好处:1)各个块更容易被新员工或六个月后查看代码的人理解。2)较小的代码块花费的时间更少……嗯,阻塞。

如上所述,POE应用程序是一个单线程进程,通过称为协作多任务的技术来假装执行异步操作。在任何给定时间,POE应用程序中只有一个子例程正在执行。如果该子例程内部有《code>sleep 60;,则整个应用程序将休眠60秒。不会触发任何警报;不会发生任何操作。较小的代码块意味着POE有机会执行其他操作,如清理正在关闭的会话或执行另一个事件。

即使是长时间运行的for循环也可以分解成小的POE事件。

    while(@data) {
        # ... process, process
    }

可以成为

    $poe_kernel->yield('process_data' => $_) for @data;

这为 POE 提供了在每次处理时间间隔中从套接字读取、执行内部清理等工作的时间。然而,如果 @data 足够大,这种方法可能会导致资源耗尽——处理 5000 个 @data 事件可能完成工作并允许 POE 执行清理,但这意味着在接下来的 5000 个事件调用中,POE 除了处理那个数组外什么也不做。

POE 的事件队列是一个先进先出(FIFO)。事件按照它们被调用的顺序进行处理。这里有两个主要例外。信号可以触发立即事件处理,使用 call() 而不是 yield()post() 也会导致立即事件处理。除了这两个例外,所有事件总是按照顺序发生。

在上面的例子中,我们要求 POE 在队列上推送大量事件。虽然 POE 仍然可以在那些 yield 之间从我们获取数据的任何套接字中读取,但由套接字读取触发的事件将在我们处理完那个大型数组之后才会被调用。我们可以非常容易地打破这种模式。

如果我们不需要以任何及时的方式处理 @data,我们可以进一步分散处理。

    $poe_kernel->delay_add('process_data' => $hence++ => $_) for @data;

这将每秒钟处理一块 @data。效率不高,也不及时,但其他事件可以在调用之间发生。一秒钟绝不是 delay_add() 接受的最小时间值。使用 Time::HiRes 允许使用微秒延迟值。

    use Time::HiRes;
    use POE;

在导入 POE 之前使用 Time::HiRes 会导致 POE 使用 Time::HiRestime() 而不是 Perl 内置的 time()。虽然 Time::HiRes 在时间值上有更高的分辨率,但它可能或可能不是您特定平台上的最准确的时间守护者。做你的作业,选择最适合你情况和需求的选择。

结论

POE 是一个灵活的应用程序框架,适用于长期运行的庞大 Perl 应用程序。它提供了任务抽象的标准接口,并迫使程序员以更小、更易于维护的块来考虑他们的软件。

POE 可在 CPAN 上找到,并有一个丰富、社区维护的网站 (http://poe.perl.org)。

标签

反馈

这篇文章有什么问题吗?请通过在 GitHub 上打开问题或拉取请求来帮助我们。