A Beginner's Introduction to POE

By Dennis Taylor, with Jeff Goff

What Is POE, And Why Should I Use It?

Table of Contents
POE Design

A Simple Example

That's All For Today

Related Links

Most of the programs we write every day have the same basic blueprint: they start up, they perform a series of actions, and then they exit. This works fine for programs that don't need much interaction with their users or their data, but for more complicated tasks, you need a more expressive program structure.

That's where POE (Perl Object Environment) comes in. POE is a framework for building Perl programs that lends itself naturally to tasks which involve reacting to external data, such as network communications or user interfaces. Programs written in POE are completely non-linear; you set up a bunch of small subroutines and define how they all call each other, and POE will automatically switch between them while it's handling your program's input and output. It can be confusing at first, if you're used to procedural programming, but with a little practice it becomes second nature.

POE Design

It's not much of an exaggeration to say that POE is a small operating system written in Perl, with its own kernel, processes, interprocess communication (IPC), drivers, and so on. In practice, however, it just boils down to a simple system for assembling state machines. Here's a brief description of each of the pieces that make up the POE environment:

States
The basic building block of the POE program is the state, which is a piece of code that gets executed when some event occurs -- when incoming data arrives, for instance, or when a session runs out of things to do, or when one session sends a message to another. Everything in POE is based around receiving and handling these events.
The Kernel
POE's kernel is much like an operating system's kernel: it keeps track of all your processes and data behind the scenes, and schedules when each piece of your code gets to run. You can use the kernel to set alarms for your POE processes, queue up states that you want to run, and perform various other low-level services, but most of the time you don't interact with it directly.
Sessions
Sessions are the POE equivalent to processes in a real operating system. A session is just a POE program which switches from state to state as it runs. It can create ``child'' sessions, send POE events to other sessions, and so on. Each session can store session-specific data in a hash called the heap, which is accessible from every state in that session.

POE has a very simple cooperative multitasking model; every session executes in the same OS process without threads or forking. For this reason, you should beware of using blocking system calls in POE programs.

Those are the basic pieces of the Perl Object Environment, although there are a few slightly more advanced parts that we ought to explain before we go on to the actual code:

Drivers
Drivers are the lowest level of POE's I/O layer. Currently, there's only one driver included with the POE distribution -- POE::Driver::SysRW, which reads and writes data from a filehandle -- so there's not much to say about them. You'll never actually use a driver directly, anyhow.
Filters
Filters, on the other hand, are inordinately useful. A filter is a simple interface for converting chunks of formatted data into another format. For example, POE::Filter::HTTPD converts HTTP 1.0 requests into HTTP::Request objects and back, and POE::Filter::Line converts a raw stream of data into a series of lines (much like Perl's <> operator).
Wheels
Wheels contain reusable pieces of high-level logic for accomplishing everyday tasks. They're the POE way to encapsulate useful code. Common things you'll do with wheels in POE include handling event-driven input and output and easily creating network connections. Wheels often use Filters and Drivers to massage and send off data. I know this is a vague description, but the code below will provide some concrete examples.
Components
A Component is a session that's designed to be controlled by other sessions. Your sessions can issue commands to and receive events from them, much like processes communicating via IPC in a real operating system. Some examples of Components include POE::Component::IRC, an interface for creating POE-based IRC clients, or POE::Component::Client::HTTP, an event-driven HTTP user agent in Perl. We won't be using any Components in this article, but they're a very useful part of POE nevertheless.

A Simple Example

For this simple example, we're going to make a server daemon which accepts TCP connections and prints the answers to simple arithmetic problems posed by its clients. When someone connects to it on port 31008, it will print ``Hello, client!''. The client can then send it an arithmetic expression, terminated by a newline (such as ``6 + 3\n'' or ``50 / (7 - 2)\n'', and the server will send back the answer. Easy enough, right?

Writing such a program in POE isn't terribly different from the traditional method of writing daemons in Unix. We'll have a server session which listens for incoming TCP connections on port 31008. Each time a connection arrives, it'll create a new child session to handle the connection. Each child session will interact with the user, and then quietly die when the connection is closed. And best of all, it'll only take 74 lines of modular, simple Perl.

The program begins innocently enough:

   1  #!/usr/bin/perl -w
   2  use strict;
   3  use Socket qw(inet_ntoa);
   4  use POE qw( Wheel::SocketFactory  Wheel::ReadWrite
   5              Filter::Line          Driver::SysRW );
   6  use constant PORT => 31008;

Here, we import the modules and functions which the script will use, and define a constant value for the listening port. The odd-looking qw() statement after the ``use POE'' is just POE's shorthand way for pulling in a lot of POE:: modules at once. It's equivalent to the more verbose:

        use POE;
        use POE::Wheel::SocketFactory;
        use POE::Wheel:ReadWrite;
        use POE::Filter::Line;
        use POE::Driver::SysRW;

Now for a truly cool part:

   7  new POE::Session (
   8      _start => \&server_start,
   9      _stop  => \&server_stop,
  10  );
  11  $poe_kernel->run();
  12  exit;

That's the entire program! We set up the main server session, tell the POE kernel to start processing events, and then exit when it's done. (The kernel is considered ``done'' when it has no more sessions left to manage, but since we're going to put the server session in an infinite loop, it'll never actually exit that way in this script.) POE automatically exports the $poe_kernel variable into your namespace when you write ``use POE;''.

The new POE::Session call needs a word of explanation. When you create a session, you give the kernel a list of the events it will accept. In the code above, we're saying that the new session will handle the _start and _stop events by calling the &server_start and &server_stop functions. Any other events which this session receives will be ignored. _start and _stop are special events to a POE session: the _start state is the first thing the session executes when it's created, and the session is put into the _stop state by the kernel when it's about to be destroyed. Basically, they're a constructor and a destructor.

Now that we've written the entire program, we have to write the code for the states which our sessions will execute while it runs. Let's start with (appropriately enough) &server_start, which is called when the main server session is created at the beginning of the program:

  13  sub server_start {
  14      $_[HEAP]->{listener} = new POE::Wheel::SocketFactory
  15        ( BindPort     => PORT,
  16          Reuse        => 'yes',
  17          SuccessState => \&accept_new_client,
  18          FailureState => \&accept_failed
  19        );
  20      print "SERVER: Started listening on port ", PORT, ".\n";
  21  }

This is a good example of a POE state. First things first: Note the variable called $_[HEAP]? POE has a special way of passing arguments around. The @_ array is packed with lots of extra arguments -- a reference to the current kernel and session, the state name, a reference to the heap, and other goodies. To access them, you index the @_ array with various special constants which POE exports, such as HEAP, SESSION, KERNEL, STATE, and ARG0 through ARG9 to access the state's user-supplied arguments. Like most design decisions in POE, the point of this scheme is to maximize backwards compatibility without sacrificing speed. The example above is storing a SocketFactory wheel in the heap under the key 'listener'.

The POE::Wheel::SocketFactory wheel is one of the coolest things about POE. You can use it to create any sort of stream socket (sorry, no UDP sockets yet) without worrying about the details. The statement above will create a SocketFactory that listens on the specified TCP port (with the SO_REUSE option set) for new connections. When a connection is established, it will call the &accept_new_client state to pass on the new client socket; if something goes wrong, it'll call the &accept_failed state instead to let us handle the error. That's all there is to networking in POE!

We store the wheel in the heap to keep Perl from accidentally garbage-collecting it at the end of the state -- this way, it's persistent across all states in this session. Now, onto the &server_stop state:

  22  sub server_stop {
  23      print "SERVER: Stopped.\n";
  24  }

Not much to it. I just put this state here to illustrate the flow of the program when you run it. We could just as easily have had no _stop state for the session at all, but it's more instructive (and easier to debug) this way.

Here's where we create new sessions to handle each incoming connection:

  25  sub accept_new_client {
  26      my ($socket, $peeraddr, $peerport) = @_[ARG0 .. ARG2];
  27      $peeraddr = inet_ntoa($peeraddr);
  28      new POE::Session (
  29          _start => \&child_start,
  30          _stop  => \&child_stop,
  31          main   => [ 'child_input', 'child_done', 'child_error' ],
  32          [ $socket, $peeraddr, $peerport ]
  33      );
  34      print "SERVER: Got connection from $peeraddr:$peerport.\n";
  35  }

Our POE::Wheel::SocketFactory will call this subroutine whenever it successfully establishes a connection to a client. We convert the socket's address into a human-readable IP address (line 27) and then set up a new session which will talk to the client. It's somewhat similar to the previous POE::Session constructor we've seen, but a couple things bear explaining:

@_[ARG0 .. ARG2] is shorthand for ($_[ARG0], $_[ARG1], $_[ARG2]). You'll see array slices used like this a lot in POE programs.

What does line 31 mean? It's not like any other event_name = state> pair that we've seen yet. Actually, it's another clever abbreviation. If we were to write it out the long way, it would be:

  new POE::Session (
      ...
      child_input => &main::child_input,
      child_done  => &main::child_done,
      child_error => &main::child_error,
      ...
  );

It's a handy way to write out a lot of state names when the state name is the same as the event name -- you just pass a package name or object as the key, and an array reference full of subroutine or method names, and POE will just do the right thing. See the POE::Session docs for more useful tricks like that.

Finally, the array reference at the end of the POE::Session constructor's argument list (on line 32) is the list of arguments which we're going to manually supply to the session's _start state.

If the POE::Wheel::SocketFactory had problems creating the listening socket or accepting a connection, this happens:

  36  sub accept_failed {
  37      my ($function, $error) = @_[ARG0, ARG2];
  38      delete $_[HEAP]->{listener};
  39      print "SERVER: call to $function() failed: $error.\n";
  40  }

Printing the error message is normal enough, but why do we delete the SocketFactory wheel from the heap? The answer lies in the way POE manages session resources. Each session is considered ``alive'' so long as it has some way of generating or receiving events. If it has no wheels and no aliases (a nifty POE feature which we won't cover in this article), the POE kernel realizes that the session is dead and garbage-collects it. The only way the server session can get events is from its SocketFactory wheel -- if that's destroyed, the POE kernel will wait until all its child sessions have finished, and then garbage-collect the session. At this point, since there are no remaining sessions to execute, the POE kernel will run out of things to do and exit.

So, basically, this is just the normal way of getting rid of unwanted POE sessions: dispose of all the session's resources and let the kernel clean up. Now, onto the details of the child sessions:

  41  sub child_start {
  42      my ($heap, $socket) = @_[HEAP, ARG0];
  43      $heap->{readwrite} = new POE::Wheel::ReadWrite
  44        ( Handle => $socket,
  45          Driver => new POE::Driver::SysRW (),
  46          Filter => new POE::Filter::Line (),
  47          InputState => 'child_input',
  48          ErrorState => 'child_error',
  49        );
  50      $heap->{readwrite}->put( "Hello, client!" );
  51      $heap->{peername} = join ':', @_[ARG1, ARG2];
  52      print "CHILD: Connected to $heap->{peername}.\n";
  53  }

This gets called every time a new child session is created to handle a newly connected client. We'll introduce a new sort of POE wheel here: the ReadWrite wheel, which is an event-driven way to handle I/O tasks. We pass it a filehandle, a driver which it'll use for I/O calls, and a filter that it'll munge incoming and outgoing data with (in this case, turning a raw stream of socket data into separate lines and vice versa). In return, the wheel will send this session a child_input event whenever new data arrives on the filehandle, and a child_error event if any errors occur.

We immediately use the new wheel to output the string ``Hello, client!'' to the socket. (When you try out the code, note that the POE::Filter::Line filter takes care of adding a line terminator to the string for us.) Finally, we store the address and port of the client in the heap, and print a success message.

We will omit discussion of the child_stop state, since it's only one line long. Now for the real meat of the program: the child_input state!

  57  sub child_input {
  58      my $data = $_[ARG0];
  59      $data =~ tr{0-9+*/()-}{}cd;
  60      return unless length $data;
  61      my $result = eval $data;
  62      chomp $@;
  63      $_[HEAP]->{readwrite}->put( $@ || $result );
  64      print "CHILD: Got input from peer: \"$data\" = $result.\n";
  65  }

When the client sends us a line of data, we strip it down to a simple arithmetic expression and eval it, sending either the result or an error message back to the client. Normally, passing untrusted user data straight to eval() is a horribly dangerous thing to do, so we have to make sure we remove every non-arithmetic character from the string before it's evaled (line 59). The child session will happily keep accepting new data until the client closes the connection. Run the code yourself and give it a try!

The child_done and child_error states should be fairly self-explanatory by now -- they each delete the child session's ReadWrite wheel, thus causing the session to be garbage-collected, and print an expository message explaining what happened. Easy enough.

That's All For Today

And that's all there is to it! The longest subroutine in the entire program is only 12 lines, and all the complicated parts of the server-witing process have been offloaded to POE. Now, you could make the argument that it could be done more easily as a procedural-style program, like the examples in man perlipc. For a simple example program like this, that would probably be true. But the beauty of POE is that, as your program scales, it stays easy to modify. It's easier to organize your program into discrete elements, and POE will provide all the features you would otherwise have had to hackishly reinvent yourself when the need arose.

So give POE a try on your next project. Anything that would ordinarily use an event loop would be a good place to start using POE. Have fun!

Source Listing....

Related Links

http://poe.perl.org/
The POE home page. All good things stem from here.

Visit the home of the Perl programming language: Perl.org

Sponsored by

Monthly Archives

Powered by Movable Type 5.13-en