POE学习笔记



QUOTE:
原帖由 福瑞哈哥 于 2007-3-14 13:44 发表

查了资料好像是不支持,它使用select监控终端。而win32的select只能用在socket上。

因此很讨厌,我打算有空了写个 Win32 版的键盘输入轮子。


QUOTE:
原帖由 flw 于 2007-3-14 13:53 发表

因此很讨厌,我打算有空了写个 Win32 版的键盘输入轮子。

支持。


QUOTE:
原帖由 flw 于 2007-3-14 10:31 发表
POE 里有好多模块都是动态加载的,而 PDK 没法检测到动态加载的模块并包含进去,因此需要解决一下这个问题。
我的解决方法就是,PDK 打包以后,运行时如果缺什么模块,就把那个模块显式地 use 一下。这样问题就解 ...

也这么干过,只不过此路不通!在某些程序上会导致一些意想不到的情况。

我现在的方法是将整个POE放到程序的运行目录里面,在编译的时候干脆将POE模块Trim掉,然后把一些要用到的PM文件加到SCANNED SCRIPTS当中,让PDK把所需的相关模块自己扫描出来,整个过程比较复杂,有时候要反复编译很多次才能搞定。
Filter:对数据进行包装和拆解

凡是通讯都要涉及到通讯协议的制订,在POE框架中通过Filter层完成对数据的包装和拆解。最简单的Filter是Filter::Stream,它什么也不做只是简单传递原始数据,应用程序需要在事件处理函数中对消息格式进行处理。Wheel ReadWrite缺省使用Filter::Line,这是一个行消息协议处理器,凡是发送的数据都会被自动在末尾加上行分隔符,接收数据时则按进行分割。这就是上面的聊天室程序在没有设置任何消息格式却能正常通讯的原因。

我们可以设计自己的Filter,来对数据进行封装,只需要在创建Wheel ReadWrwite时设置InputFilter和OutputFilter参数即可。设计一个Filter,最重要是实现get_one_start、get_one和put三个函数。对于输出Filer,put函数接收一个数组引用的参数,这个数组里面是应用程序发送的数据包队列,需要返回一个数组引用,数组里面是封装好的数据报队列。对于输入Filter,get_one_start接收一个数组引用,数组里面包含未经处理的远程数据包队列,这个函数需要把这个数据包队列保存起来,然后其他组件可以反复调用get_one得到按照通讯协议分割好的数据包。

下面是经过修改后的聊天室客户端程序,里面包含了一个WhoSaidFilter的类。客户端在启动时接收一个命令行参数作为客户端的名字$name,当用户在终端输入消息后,它会经过WhoSaidFilter把消息$message变成"$name said: $message, IMHO\n"发送给服务端。这个通讯协议兼容Line通讯协议(都用行分割符作为数据包的结束标志),因此服务端不需要更换它的InputFilter。

[Copy to clipboard] [ - ]
CODE:
use warnings;
use strict;

use IO::Socket;
use POE qw /Wheel::SocketFactory Wheel::ReadWrite Wheel::ReadLine/;

my $name = shift;
$name = "Anonymous" unless $name;

# 设计自己Filter
@WhoSaidFilter::ISA = qw (POE::Filter);

sub WhoSaidFilter::new {
        my $type = shift;
        my %params = @_;
        my $name = delete $params{Name};
        return bless { name => $name }, $type;
}

# 重载put函数
sub WhoSaidFilter::put {
  my ($self, $bufs) = @_;

  my @raw;
  foreach (@$bufs) {
    push @raw, "$self->{name} Said: $_, IMHO\n";
  }

  \@raw;
}

POE::Session->create
        ( inline_states =>
                { _start => \&start_chat,
                  connected => \&connected,
                  connect_fail => \&connect_fail,
                  server_input => \&server_input,
                  user_input => \&user_input,
                }
        );
       
POE::Kernel->run;

sub start_chat {
        my $wheel = POE::Wheel::SocketFactory->new
                ( RemoteAddress => 'localhost',
                  RemotePort => 8000,
                  SuccessEvent => "connected",
                  FailureEvent => "connect_fail",
                );
        $_[HEAP]->{server} = $wheel;
}       

sub connected {
        my ($kernel, $heap, $socket) = @_[KERNEL, HEAP, ARG0];
        my $wheel = POE::Wheel::ReadWrite->new
                ( Handle => $socket,
                  InputEvent => "server_input",
                  ErrorEvent => "error_happened",
                  # 使用输出Filter
                  OutputFilter => WhoSaidFilter->new( Name => $name ),
                );
        # 下面这一句同时把以前的那个SocketFactory的轮子删掉
        $heap->{server} = $wheel;
       
        my $console = POE::Wheel::ReadLine->new
                ( InputEvent => 'user_input'
                );
        # 告诉ReadLine监控终端
        $console->get( 'input your message, bye to quit: ');
        $heap->{console} = $console;
}

sub connect_fail {
        delete $_[HEAP]->{server};
}

sub server_input {
        my ($heap, $input) = @_[HEAP, ARG0];
        # 如果使用print "$input\n"会搞乱终端
        $heap->{console}->put( $input );
}

sub user_input {
        my ($heap, $input) = @_[HEAP, ARG0];
        if ($input =~ /(quit)|(exit)|(bye)/i) {
                delete $heap->{server};
                delete $heap->{console};
                return;
        }
        # 发送到服务端
        $heap->{server}->put( $input );
        # 继续监控终端
        $heap->{console}->get( 'input your message, bye to quit: ');
}

Filter的功能很强,从简单的行协议通讯、加数据包长度前缀的通讯,到各种网际协议(HTTP、FTP、Jabber、MSN)都可以设计出相应的Filter来处理。POE/Filter下面的各种各样的Filter,可以拿来学习使用。简单一点的通讯协议可以使用Filter Block,会自动在数据包前面加上长度或者根据长度读取数据。这些Filter又都有丰富的选项,可以改变处理细节。比如Filter Line可以设置行分割符,Filter Block可以设置为固定包长度通讯,或者提供自己的数据包前缀处理函数。
执行常规任务

除了执行IO操作的Session(携带一个或者多个Wheel),应用程序肯定也需要一些执行低IO延迟的工作,比如统计运行数据、记录日志等。你可以把POE::Kernel->run那一句替换为如下代码:

while (not $done) {
        POE::Kernel->run_one_timeslice;
        # 如果运行到一定时间,统计数据
        # 如果运行到一定时间,记录日志
}

也可以创建一个Session,使用POE Kernel的alarm、delay等函数设置定期执行任务。下面的代码实现了这样一个组件,它接收一个代码块引用,以及一个时间间隔参数,当到达时间间隔后,它就会执行那个代码块,如果执行结果返回真值,它则继续设置下一个定时,否则就结束自己的工作。事实上还可以重新设计,使代码块返回下一次被触发执行的时间间隔。也可以不设置Interval,这样常规任务就会在每一次有任何事件发生时被执行。

[Copy to clipboard] [ - ]
CODE:
use warnings;
use strict;

package Component::TimerRoutine;

use Carp qw /croak/;
use POE;

# 启动一个Session,执行定时任务
sub spawn {
        my $type = shift;
        my %params = @_;
        my $interval = delete $params{Interval};
        my $workhorse = delete $params{Workhorse};
        croak "Workhorse param must be a subroutine reference"
                unless ref($workhorse) eq 'CODE';
        POE::Session->create
                ( inline_states => {
                        _start => \&timer_routine_start,
                        dowork => \&dowork,
                  },
                  # 在创建一个Session时,可以通过heap参数设定Session私有数据
                  heap => {
                          interval => $interval,
                          workhorse => $workhorse,
                  }
                       
                );
        undef;
}

sub timer_routine_start {
        my ($kernel, $heap) = @_[KERNEL, HEAP];
        if ($heap->{interval}) {
                $kernel->delay_set( 'dowork', $heap->{interval} );
        } else {
                $kernel->yield( 'dowork' );
        }
}

sub dowork {
        my ($kernel, $heap) = @_[KERNEL, HEAP];
        return unless $heap->{workhorse}->();
        if ($heap->{interval}) {
                $kernel->delay_set( 'dowork', $heap->{interval} );
        } else {
                $kernel->yield( 'dowork' );
        }
}


package main;

use POE;

sub work {
        print time, "\n";
        return 1;
}

Component::TimerRoutine->spawn
        ( Interval => 1,
          Workhorse => \&work,
        );

POE::Kernel->run;

除了在使用Wheel的情况下Session的状态被自动设置外,应用程序也可以使用POE::Kernel的函数post、yield、alarm、delay来设置某一个Session的状态。还可以使用Kernel::call直接调用某一个状态的处理函数,而不用通过消息分发机制。在对Session操作时,一个通常的做法时使用Session的别名而不是使用POE::Session->create的返回值。别名可以在创建Session后通过POE::Kernel::alias_set设定。
Component: 组合高级应用

如果来总结一个POE Server应用程序的编写,会发现程序基本上是先建立一个Session,然后创建一个监听Socket,并把这个Socket挂载到一个ListenAccept的Wheel上来监控连接事件,在有新连接到达时,新建一个Wheel ReadWrite跟客户端通讯。如果把这些常规动作再封装起来就产生了组件。


下面的聊天室服务器程序使用Component::Server::TCP重写,从编写轮子代码的重复中解脱了出来。这个组件会为每一个新连接的客户端建立一个单独的Session,这是跟上面的服务器程序不同的地方,不过这都是在组件内实现的。

[Copy to clipboard] [ - ]
CODE:
use warnings;
use strict;

use IO::Socket;
use POE qw /Component::Server::TCP/;

# 客户连接表
my %conntab = ();

POE::Component::Server::TCP->new
        ( Port => 8000,
          ClientConnected => sub {
                  # 登记客户
                  $conntab{ $_[HEAP]->{client}->ID } = $_[HEAP]->{client};
          },
          ClientDisconnected => sub {
                  # 注销客户
                  delete $conntab{ $_[HEAP]->{client}->ID };
          },
          ClientInput => \&client_input,
        );

POE::Kernel->run;

sub client_input {
        my ($heap, $input, $wid) = @_[HEAP, ARG0, ARG1];
        # 广播数据。如果愿意,可以屏蔽掉$wid,即发送消息的客户端
        map { $conntab{$_}->put( $input ) } keys %conntab;
}

# 是的,这些代码实现了一个聊天室服务器

当你要开发一个新的程序时,可以先去查看是否有满足你要求的组件,如果没有再从编写Wheel开始。cpan上POE组件从Syslog、Dirwatch到TCP、HTTP、FTP、IRC、NTTP、SOAP、Jabber、MSN,从服务器到客户端,每一个都优雅地运行POE的平台上,给进一步的编程提供了坚实的基础。说不定将来也有qq的组件。
暂告一段落吧,码字,测试,累的吐血。
敢问FLW老大:无人喝彩,难道就不能成为原创精华吗?


QUOTE:
原帖由 福瑞哈哥 于 2007-3-16 17:17 发表
敢问FLW老大:无人喝彩,难道就不能成为原创精华吗?

晕~已经是精华了。我给你改改,改成原创精华。


QUOTE:
原帖由 flw 于 2007-3-14 13:53 发表

因此很讨厌,我打算有空了写个 Win32 版的键盘输入轮子。

市长姐姐恕我孤陋,键盘输入轮子 是什么