一、What Is POE, And Why Should I Use It? 什么是POE? 为什么要使用POE?
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(Perl对象环境)的出发点。 Perl是用来构建对外部数据,如网络连接或用户交互,生成响应的程序的构架。 POE的程序完全是非线性的。 你可以设置一系列的小程序,然后定义它们是如何调用的,POE将会自动选择它们来处理程序的输入输出。 如果你一直是过程编程方式,开始时会有一些困难,但经过一些实践就会很快习惯
二、POE Design【POE设计】 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: 基于它自己的内核,进程,进程间通信,驱动等来说,POE是一个Perl写的小于OS并不为过。 但实际上,它可以归结为一个简单的组合状态机系统。 下面是组成POE环境的每个部分的简单介绍:
1. 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. 状态是POE程序的基本构建模块,它是当某些事件发生时,用来执行的代码段--例如,当数据到达, 或sessino运行结束时,或session发送一个消息给另一个时。 POE中所有事情可以归类为接收和处理这些事件。
2. 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. POE的内核与OS的内核很相似: 它会跟踪后台的所有进程和数据,并调度运行的代码。 可以用内核来设置POE进程的闹钟,将要运行的状态排除,和执行多种低级服务, 但很多时候并不需要直接和内核进行交互。
3. 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. session等同于OS中的进程。session是从一个状态切换到另一个状态时运行的POE程序。 它可以创建子session, 发送POE事件给其它session等。 每个session都可以在一个叫做head的哈希表中存储自己的数据,且该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. POE有一个非常简单的多任务协作模型; 所有的session都是在同个OS进程中执行,且没有线程或进程创建。 因此,在POE编程中要很小心地使用阻塞式的系统调用。
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: 下面是一些基本的POE组成块
4. 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. Driver是POE的I/O层的最低层。现在,POE的发布版本中只有一个driver -- POE::Driver::SysRW. 它可以从文件句柄中读写数据 -- 因此不详做介绍。有可能你根本不会直接用到它。
5. 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). 从另一方面来说,filter非常有用。 filter是将格式化的数据块转换成另一种格式的简单接口。例如, POE::Filter::HTTPD将HTTP 1.0的请求转换成HTTP::Request对象并返回; POE::Filter::Line将原始数据流转换成多行(就像Perl的<>操作)。
6. 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. Wheels包含有完成日常任务的高级逻辑的可重用代码片断。它们以POE的方式将有用的代码做了封装。 在POE中,通常使用wheel做的事情包括处理事件驱动的输入输出,易用的网络连接。 Wheel通常使用Filter和Driver来通知和发送数据。 这么说可能不是很明白,可以具体地看下面的代码
7. 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. Component是一个session,用来被别的session控制。 你的session可以发送命令给它,也可以从它那接收事件,很像OS中的使用IPC进行进程通信。 Component的例子包括, POE::Component::IRC, 用来创建基于POE的IRC客户端的接口; POE::Component::HTTP, Perl中由事件驱动的用户代码。
三、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? 在本例中,我们创建一个服务后台,它接收客户端的TCP连接,并将客户端简单算术问题的答案返回给客户端。 当有客户端连接到31008端口时,它将会返回给客户端"Hello, client!"; 然后,客户端就可以发送计算表达式给它,并以换行符结束(如"6 + 3\n" or "50 / (7 - 2)\n"); 服务端将会发送回结果. 很简单吧!
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. 要写这样的一个POE程序和写一个传统的Unix后台程序有很大的不同。 我们有一个服务端的session来监听31008端口的TCP连接。 每当有连接到达时,它就创建一个新的子session来处理这个连接, 子session将用来和用户交互,并且当连接关闭时子session会安静退出。 上面这些动作,只需要74行的代码就完成了:
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: 在这儿,我们导入了要用的模块和函数,并定义了监听端口常量。 qw()声明是POE用来一次性加载多个POE模块的简写方式,它和下面的代码等同: use POE; use POE::Wheel::SocketFactory; use POE::Wheel:ReadWrite; use POE::Filter::Line; use POE::Driver::SysRW;
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;". 这就是整个程序!我们设置了主要的服务端session,告诉POE内核开始处理事件,当它完成后退出。 (当没有session需要管理时,内核就被认为是完成了。但我们将服务端session设置为无限循环, 所以它是不会退出的。) 当声明了"use POE"后,POE将自动导致变量$poe_kernel到你的命名空间。
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. 这个新POE::Session调用需要做些解释。 当创建session时,就给内核创建了一个它要接受的事件列表。 在上面的代码,告诉了内核新session要调用&server_start和&server_stop函数来处理_start和_stop事件。 任何其它的事件对于本session来说都将被忽略。 _start和_stop是POE session中的特殊事件: _start状态是session创建时执行的第一个事件, 在session被内核销毁时,session将会进入到_stop状态。 基本上,它们就是创建者和销毁者。
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: 接着再来写当session运行时用来处理状态的代码。 先从&server_start开始,它在session创建时被调用:
13 sub server_start { 14 $_[HEAP]->{listener} = new POE::Wheel::SocketFactory->new 15 ( BindPort => PORT, 16 Reuse => 'yes', 17 SuccessEvent => 'accept_new_client', 18 FailureEvent => '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'. 这是一个很好的POE状态示例代码。 在开始之前,先看$_[HEAP]调用: POE有一个传递参数的特别方式: 数组@_封装了很多额外的参数 -- 当前内核和session的引用,状态名,Heap的引用,以及其它; 要访问它们,可以使用数组@_加下标KERNEL, SESSION, STATE, HEAP, ARG0 ~ ARG9。 和POE的大多数设计一样,这样能最大化的提高速度。 上面的这个例子存储了以'listener'为key的SocketFactory wheel.
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! POE::Wheel::SocketFactor wheel是POE中最酷的东西。 可以用它来创建任何流socket而不用关注细节。 上面的状态声明创建了一个SocketFactory来监听指定的TCP端口(使用了SO_REUSE选项); 当连接建立了时,它将调用&accept_new_client状态来传输新的客户端socket。 如果出错,将调用&accept_failed状态来处理错误。 这就是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: 将wheel存储在heap中可以让Perl进行异常时的自动垃圾收集。 再来看&server_stop状态:
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. 代码很简单,session中也可以没有_stop状态。 这儿是为了说明和调试方便。
Here's where we create new sessions to handle each incoming connection: 下面是对每个新到的连接创建一个新的session:
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. 当和客户端成功建立连接后,POE::Wheel::SocketFactory将会调用这个例程。 我们将socket地址转换成可读的IP地址,然后创建一个新的session和这个客户端进行交互。 这个和前面的POE::Session创建者很像,但有两个地方需要解释: @_[ARG0 .. ARG2]是($_[ARG0], $_[ARG1], $_[ARG2])的缩写;
What does line 31 mean? It's not like any other 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: 31行是事件名等于状态名的一种缩写,将其展开将如下: 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. 当状态名和事件名相同时,这种方式很方便 -- 只需要传输包名或对象名作为key,以及完整子例程或方法名 引用数组,POE就会做正确的事。 更多的事可以看POE::Session文档。
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. 最后,第32行是POE::Session创建者的参数列表,它是我们打算在session的_start状态中手动支持的 参数列表。
If the POE::Wheel::SocketFactory had problems creating the listening socket or accepting a connection, this happens: 如果在创建监听socket或接收连接异常时,则调用下面的方法处理:
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. 输出出错信息通常就足够了。 此处从heap中删除SocketFactory的原因是为了POE管理session资源。 只要能生成或接收事件,session就被认为是活的。 如果它没有wheel,或别名,POE内核就认为这个session死亡并回收它。 本服务端的session只能从SocketFactory wheel获得事件 -- 如果它被销毁, POE内核将会等待直到它的子session完成,然后回收这个session。 在本例中,因为没有别的session在执行,POE将做完并退出。
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: 因此,基本上,这是清除不想要的POE session的常用方式: 处理所有session的资源并让内核回收它。 下面是子session的细节:
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. 每次调用,将会创建一个新的子session来处理新的客户端的连接。 在这,我们将引入一个新的POE wheel: ReadWrite, 它以事件驱动方式的处理I/O任务。 传输文件句柄,用来I/O调用的驱动,和转换输入输出数据的filter。 另外,这个wheel将会给这个session一个child_input事件,当文件句柄的新数据到达时, 且出错时发送child_error事件
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. 我们立即使用新的wheel输出字符串"Hello, client!"给socket。 最后,存储客户端的IP和端口进heap,并输出成功消息
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! 下面是正餐: child_input 状态
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! 当客户端发送了一行新数据时,先检查它,并发送结果或出错信息给客户端。 通常,将不可信的数据直接传给eval()是很危险的,因此要去掉非数据字符。 子session将会保持接收新数据直到客户端断开连接。
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. child_done 和 child_error 状态只用来声明下就行啦
四、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!
五、验证过的最终的代码 1. server.pl
#!/usr/bin/perl -w
#
# This program listen to port and compute arthmatic expression
#
use strict;
use Socket qw(inet_ntoa);
use POE qw(Wheel::SocketFactory Wheel::ReadWrite Filter::Line Driver::SysRW);
use constant ADDR=>192.168.1.73;
use constant PORT=>31008;
###############################################################################
# Sub
###############################################################################
#
# Server
#
sub server_start{
my $heap = $_[HEAP];
print "= L = Listener birth\n" if $debug;
$heap->{listener} = POE::Wheel::SocketFactory->new
(
BindAddress => ADDR,
BindPort => PORT,
Reuse => 'yes',
SuccessEvent => 'accept_new_client',
FailureEvent => 'accept_failed',
);
print "SERVER: Started listening on port ", PORT, ".\n";
}
sub server_stop{
my $heap = $_[HEAP];
delete $heap->{listener};
delete $heap->{session};
print "= L = Listener death\n" if $debug;
print "SERVER: Stopped.\n";
}
2. client.pl
#!/usr/bin/perl -W
# chatclient - client for the chat server
use IO::Multiplex;
use IO::Socket;
use strict;
## Main process
my %srv_info =(
"srv_ip" => "192.168.1.73",
"srv_port"=> "31008",);
my $srv_addr = $srv_info{"srv_ip"};
my $srv_port = $srv_info{"srv_port"};
my $sock = IO::Socket::INET->new(
PeerAddr => "$srv_addr",
PeerPort => "$srv_port",
Type => SOCK_STREAM,
ReuseAddr=> SO_REUSEADDR,
Reuse => 1,
Proto => "tcp")
or die "Can not create socket connect. $!, $@";