POE::Tutorial - A beginers guide to POE
Using POE for the first time can be daunting. POE is a large collection of complex modules. POE is also a completely different way of thinking about solving a problem.
This document is an attempt to help beginers get quickly up to speed with POE. It is roughly structured in 3 sections: understanding the event-oriented programming, an overview of POE and details of specific POE modules.
POE uses an event loop to implement cooperative multi-tasking. To understand what an event loop is, we need to know what an event is:
An event is something that happened and the details of what happened.
Something happened is purposefully very vague. It could be data is now available over the network, an output buffer has been flushed to the serial port, the user has interacted with an applications user interface, a lapse of time is over. It could also be a more complex event: a new HTTP request has arrived, a new user has joined an IRC client, one part of the application wishes to notify another part of an application.
Details of what happened is also vague: it really depends on the type of event:
The details could be the data itself, the socket that the data arrived on, and so on.
The details could be the UI element in question, the type of interaction (mouse click, key press, etc) and so on
The details could be some arbitrary data associated with the timer
The details could be the socket, a the HTTP::Request manpage object, and so on.
The details could be a new database record, the contents of a user session.
Once an event has been noticed, POE will route the event to one or more destinations, known as event handlers. (For historical reasons event handlers are also called states and state handlers in POE.) An event handler is just some Perl code that has been associated with the event. Its job is to react to the event:
Log the data to a database.
Update some element in the UI.
Rotate a log file.
Calculate a fibonacci sequence and send this back to the browser.
Write current status of an operation to a check-point file.
Events could come from outside the application (network, filesystem, OS signals) or from within the application.
the POE::Kernel manpage deals with many external event sources. Others are detected by themultitude of POE::Wheel modules. Wheels can also do some processing on the data with POE::Filters. At higher level of abstraction, the POE::Session manpages and the POE::Component manpages can detect or create events. But this is getting a bit ahead of it all.
Once an event is detected, a message is given to the POE kernel to be routed to an event handler. This is called posting an event. Confusingly, both the something that happened and the message that gets posted are both called events.
The above is pretty vague: an event is detected, a message (aka event) is posted, the POE kernel routes it to an event handler that reacts accordingly. POE applications spend their entier time doing this: event->reaction, event->reaction, event->reaction. Sort of like going around in circles; it's an event loop.
This is important to remember. Important enough that I'm going to include an ugly diagram to draw attention to it
/---> detect event | | | \|/ | post event | | | \|/ | route event | | | \|/ | handle event | | \________/
The POE kernel is the master of ceremoies in a POE application; it handles interaction with the outside world, it routes events to their handlers. It also makes sure that something is always happening. If nothing is happening, it checks to see if something could happen. If nothing could happen ever again, then obiously the application has finished its task and the kernel exits.
Queue
There is only one POE kernel within an application. It is available as the
$poe_kernel
object, exported by POE.
use POE; $poe_kernel->run();
Most action within a POE application is mediated by the kernel. Most interaction with other POE components happens via the kernel.
While the POE kernel can detect that something has happened (for instance: network data is availabe), most of the time you will want a higher level of abstraction (for instance: reading the data). But why reinvent the wheel? Code for reading files, sockets, handling sub-processes and more already exists in the form of the the POE::Wheel manpage library of modules.
You can also think of these modules as the wheels that POE runs on; where the rubber hits the pavement, as it were.
As an example, take the POE::Wheel::ReadWrite manpage. It can read from a socket or file handle, write to a socket or file handle. It can also detect errors. All these things are then followed by an event being posted.
Or the POE::Wheel::ReadLine manpage, a wrapper around the Term::ReadLine manpage.
What happens if the data that Wheel is incomplete? Maybe the data being sent was bigger then a network packet and is arriving in pieces. Your application is only really interested in the complete data, not the individual pieces as they arrive. The the POE::Filter manpage collection of modules will handle each piece until it has everything nessary. Only then will it tell the wheel to send a message.
Filters work 2 ways; raw data from the ``outside'' is split into messages, and messages from the application are combined into raw data when sent to the ``outside''
Examples:
Waits until it sees an end-of-line condition (configurable), then post the entire line as an event. If sending data, makes sure it is end-of-line terminated. See the POE::Fitler::Line manpage for more details.
Seperates data into blocks of X bytes, then posts those bytes as an event. Can also handle variable length data structures. Variable-length blocks are also possible. See the POE::Filter::Block manpage for more details.
When sending, complex Perl structures are serialised before being sent, for example with Storable/nfreeze. When receiving, the raw data is converted back to a Perl structure, for example with Storable/thaw.
Because the first populare generalised serialisation module was called FreezeThaw, these operations are reffered to as freezing and thawing.
All event handlers in a POE application are collected into the POE::Session manpages. Sessions could be thought of as threads or processes, but remember that POE is cooperative multitasking; only one event handler in one session can be running at any given time. If that event handler never exits, no other event handler can be run.
A session maintains the list of event handlers. When the POE kernel routes an event to its handler, it is the session that calls the handler's code.
The modules in the POE::Component can be pretty much anything. Like
DBIx, it is where code that uses POE or extends POE lives. There are a
few conventions on how a component should behave, but there are no hard
rules. In general, they are implemented as POE::Sessions, and started with
either ->spawn
or ->create
.
Example components are the popular POE::Component::IRC, an IRC client, POE::Component::Server::SimpleHTTP, a web server and POE::Component::DBIAgent for interacting with a database.
Event handlers are created by POE::Session in 3 flavours: inline, package and object. All 3 can be used in the same session. Multiple objects or packages can be used by the same session:
POE::Session->create( inline_states => { _stop => sub { warn "Shutdown" }, ctrl_C => \&ctrl_C }, object_states => [ $obj1 => [ '_start' ], $obj2 => { load => 'file_load', save => 'file_save' } ], package_states => [ My::Package => [ '_child', '_default' ] ], );
This sets up 7 event handlers:
An anonymous coderef that is called when the POE kernel believes the session has nothing to do.
A subroutine that is called when the ctrl_C
event is posted.
When the session is first created, the _start
method of $obj1
will be
called.
These event handlers are mapped to object methods of $obj2
that have a
different name. The load
event will call $obj2->file_load
and the
save
event, $obj2->file_save
.
This event handler is called when this session gains or looses a child session. Discussion of child sessions is beyond the scope of this document.
This event handler is called when there is no other event handler for an event. You might be thinking ``I know, I'll install a _default handler. If I have all my event handlers installed already set up, I'll be able to catch typos in mistyped event names.'' The problem with this is that there is a back-ground noise of events that POE kernel posts, dealing with various house-keeping issues.
It is also possible to add and remove event handlers during application execution:
$poe_kernel->state( $new_event => $object => $method ); $poe_kernel->state( $old_event );
This adds a new event handler for $new_event
, which will call $method
of $object
. It also removes the event handler for $old_event
.
It is worth pointing out that a event's name is not limited to Perl identifiers. Then can contain spaces, punctuation, UTF8, anything. This is why you can map event handlers to methods of a different name.
When the POE kernel decides an event must
the POE::Session manpage will call your handler with the arguement list (@_
)
populated with all the details of an event. Individual details are fetched
via constant offsets into this array. So your code will start to look like
this:
sub my_handler { my( $session, $sender, $caller_file ) = @_[ SESSION, SENDER, CALLER_FILE ]; my( $id, $data ) = @_[ ARG0, ARG1 ]; # .... }
SESSION
is the current session, SENDER
is the session that posted an
event (which could also be the POE kernel). CALLER_FILE
is the source
code file the event was posted from. There are others, consult
the POE::Session manpage.
ARG0
and ARG1
are details specific to this event. If an event can be
thought of as a subroutine (which it shouldn't), ARG0
, ARG1
are the
subroutine's parameters.
If like me you find this to be cumbersome there are other ways of managing those details. See the POE::Sugar::Args manpage and the POE::Session::PlainCall manpage.
If your application wishes to post an event, it does so via the POE kernel:
$poe_kernel->post( $session1 => $event1, @args ); $poe_kernel->yield( $event2, @args ); my @B = $poe_kernel->call( $session3, $event3, @args );
The first line will post an event called $event1
to the session reffered
to in $session1
. The event's handler will not be called immediatly; the
event is placed on an event queue by the POE kernel and will called after
the current event handler exits. This is called an asynchronous event.
The second line will post an event called $event2
to the currently active
session. This is also asynchronous event. yield
can be thought of as
syntatic sugar for:
$poe_kernel->post( $_[SESSION], $event );
The second line will call the envent handler for $event3
in $session3
right away, and the event handler's return will be placed into @B
. This
is a synchronous event. They should be used sparingly, because they
break POE's cooperative nature.
In all 3 lines passed @args
to the POE kernel. These are the details of
a given event. They will be available to the event handler as $_[ARG0]
,
$_[ARG1]
...
Most of the time you will be using POE::Wheels to create and maintain sockets.
There are 2 kinds of signals: external, OS signals, and internal ``fake'' signals.
External signals are detected by the kernel, and routed sessions that have registered an interest in them.
$poe_kernel->sig( INT => 'ctrl_C' );
Some signals are terminal; if your application doesn't handle them and tell the kernel to continue, your application will exit. See Signals in the POE::Kernel manpage.
The kernel also uses signals to send details of the life of the application.
You can also use signals to multicast a message to all interested sessions:
$poe_kernel->signal( $poe_kernel, 'my-signal', @details );
To catch that signal, you would use
$poe_kernel->sig( 'my-signal' => 'sig_handler' );
There are 3 ways to address a session: with a session object, with a session ID, or with a session alias.
create in the POE::Session manpage returns a session object. You can use this object post events to the session. However, you would be ill advised to keep a reference to a session object; this prevents POE's garbage collection from doing its job.
Each session has a unique ID. This ID is avaible via ID in the POE::Session manpage and is guaranteed unique within a given POE kernel's lifetime. And instead of keeping session object, you can use it's ID.
my $id = POE::Session->create(); # ... $poe_kernel->post( $id => 'event' );
Using session IDs requires that you tell all sessions the IDs of all the sessions they wish to interact with. This can be cumbersom and can lead to chicken-and-egg problems.
Sessions can also be given one or more aliases: strings that will also uniquely identify the session.
$poe_kernel->alias_set( 'honk' ); # ... $poe_kernel->post( honk => 'event' );
What's more, a simple number is not very informative, however. Text like ``posting to session 3'' in debug output doesn't tell much about session 3, compared to ``posting to session 'honk'''
When will an application exit? is a common question. The answer is when there are no more sessions.
When does session stop, then?. A session will be garbage collected when the POE kernel notices that session is not doing anything and that it can't do anything in the future.
Not doing anything means that it has no events in the queue, that it hasn't sent an event that is still in the queue, no timers set.
Can't do anything means that it doesn't have any files or sockets open (future external events), and that it doesn't have an alias set (future internal events).
So, to get a session to go away, you close all its sockets (generaly by getting rid of its wheels), remove all pending timers, and remove its alias.
What's more, if all the remaining sessions are kept around solely because they have aliases, the POE kernel will deem the sessions as having finished their task and will exit.
Here, in less then 100 lines of code, is a primitive pure-perl chat server.
use POE;
Chatter->spawn( port => 4321 ); $poe_kernel->run;
package Chatter; use strict; use warnings; use POE; use POE::Component::Server::TCP;
sub spawn { my( $package, @args ) = @_; my $self = bless {@args}, $package; return POE::Session->create( object_states => [ $self => [ qw( _start connect disconnect error said sig_INT ) ] ] ); }
The spawn class method is very simple: we create an object, then create a
session that uses object methods as event handlers. One common gotcha is to
create a new method but forget to add it to the object_states
list.
the POE::Declare manpage can help you avoid this.
While create
is running, the _start
event is posted. This means that
a POE event handler is running even before $poe_kernel->run was called.
sub _start { my( $self ) = @_; $self->{alias} = "Chat server"; $poe_kernel->alias_set( $self->{alias} ); $self->{server} = POE::Wheel::SocketFactory->new( BindPort => $self->{port} SuccessEvent => 'connect', FailureEvent => 'error', ); $poe_kernel->sig( INT => 'sig_INT' ); }
Again, very simple to follow: create an alias for the session, create a wheel that listens on the given port, and look for SIGINT (aka Control-C).
sub connect { my( $self, $socket ) = @_[ OBJECT, ARG0 ]; my $wheel = POE::Wheel::ReadWrite->new( Handle => $socket, InputEvent => 'said', ErrorEvent => 'disconnect' ); $wheel->put( "Greetings" ); $self->{clients}{ $wheel->ID } = $wheel; }
When a client connects to the server, we receive a connect
event from the
wheel. We create another wheel to handle interaction with the new client,
send it a greeting banner and keep ahold of the wheel.
sub error { my ($operation, $errnum, $errstr) = @_[ OBJECT, ARG0, ARG1, ARG2]; warn "Server $operation error $errnum: $errstr\n"; delete $_[HEAP]{server}; }
If the server wheel encoutered an error, for example the port was already in use, this event handler is called.
sub disconnect { my( $self, $op, $errnum, $errstr, $wid ) = @_[ OBJECT, ARG0, ARG1, ARG2, ARG3 ]; delete $self->{clients}{ $wid }; if( $op eq 'read' and $errnum == 0 ) { $poe_kernel->yield( said => "*CARRIER LOST*", $wid ); } return; }
If a client wheel encounters an error the disconnect
event is posted from
the wheel. We react by getting rid of the wheel. $op eq 'read' and
$errnum == 0
is how we detect a normal disconnect event. We only send a
message to other clients on normal disconnect. Otherwise we risk sending
messages to client wheels that are in the process of shuting down, which
would provoke more error events, which in turn would post more messages.
sub said { my( $self, $input, $wid ) = @_[ OBJECT, ARG0, ARG1 ]; foreach my $id ( keys %{ $self->{clients} } ) { next if $id == $wid; $self->{clients}{ $id }->put( "$wid> $input" ); } }
When a user types something into their client, the client wheel filter out
the end-of-line, then posts the said
event. We send the message to all
the other client wheels.
sub sig_INT { my( $self ) = @_[ OBJECT ]; delete $self->{clients}; delete $self->{server}; $poe_kernel->alias_set( delete $self->{alias} ) if $self->{alias}; }
Philip Gwyn