gyn

Win32下的Perl,无用的select,停滞的Tk,结束吧....

对Django中sqlite数据库无法建立外键的解析

是参照《Learning Website Developing with Django》里面的django_bookmarks一例照搬照抄下来的,问题出现在学习到为bookmarks应用建立数据库表的时候。这里涉及到三张表,第一张是User,用来存储用户信息;第二张是Link,用来存储网页的url;第三张是bookmarks,用来存储书签,书签除了id和书签名之外,还有两个指向User和Link的外键。按照书里面的指示在booksmark.models里插入以下内容:

from django import models

from django.contrib.auth.models import User

class Link(models.Model):

       url = URLField(unique = True)

class Bookmarks(models.Model):

       title = CharField(maxlength = 200)

       user = ForeignKey(User)

       link = ForeignKey(Link)

之后,“manage.py syncdb”将以上程序同步到数据库中,再使用“manage.py sqlall bookmarks”查看一下数据库中关于的bookmarks的表的建立情况。实际的输出与书本上的内容有一些差异,就是对于Link建立的外键,显示的结果只是一个非Null的Integer字段,没有“references”。

这里让人奇怪的一点是,既然没有建立外键,那么这个Integer的类型是怎么决定下来的?也许一种默认行为。这样的话,就很wired和 magic了。还有一种可能就是manag.py对与数据库构成的揭示是存在bug的,也就是说事实上这个外键是已经建立了,只不过没有被显示出来而已。所以使用标准sqlite3.exe,分析数据库bookmarksdb。

sqlite3 bookmarksdb

sqlie> .schema

在得到的结果中,果不其然,有

“link_id” integer NOT NULL REFERENCES “bookmarks_link” (“id”)


posted @ 2008-07-10 19:49 gyn_tadao 阅读(1438) | 评论 (0)编辑 收藏

我对pysqlite的一些了解

大约是一年半或者是两年前的样子,在那段时间里,我似乎特别痴迷于各种数据库以及相关的技术。其间看了不少书,可能是平常工作接触的多是 sqlserver 的缘故,我本人对其反而不是特别的感冒,所以看的多是关于 postgresql oracle 的,尤其是前者。记得当时用的是一台 celeron1.7G 、内存 256MB 的方正电脑,安装这些软件让电脑变得非常的慢,终于在一次忍无可忍的僵死之后,我卸载了所有这些庞然大物。

之后台里引进了一台用于上下班记录的打卡机,为了方便统计迟到早退,我用 perl/tk 写了一个采集整理程序,后台用到了 sqlite ,这也是我和 sqlite 的初次邂逅。

至于对 python ,不好意思地说,我一直抱着骑墙的态度。如果 perl 可以解决的,我是断然不会用 python 的,这一切直到我对 poe 的幻想破灭为止。原先我是期待用 poe 来作为多线程的替代,可事实还是向着相反的方向发展。

sqlite 是一个嵌入式的数据库,也就是说没有独立的维护进程,所有的维护都来自于程序本身。麻雀虽小,五脏俱全, sqlite 实现了多数 sql-92 的标准,比如说 transaction trigger 和复杂的查询等。

有很多介绍的书籍,记得有一本 apress 出版的《 The difinitive guide to sqlite 》,有毛 500 面的样子,用来介绍一个嵌入式数据库,着实有点长了,当然对于初学者是有其存在的价值的。

pysqlite 则是一个 sqlite python 提供的 api 接口,它让一切对于 sqlite 的操作都变得异常简单。 userguide 并不长,但是基本上都说到点子上了,不过这里还是要对其提一下几个要点。事实上我个人认为,理解了这几点就完全足够了。

from pysqlite2 import dbapi2 as sqlite ”,   这就是一切的起源。

下面是一个涉及到我即将介绍的几点的示例程序,内容很简单,就是将一个 Point 对象存入 sqlite 建立的内存数据库中,然后原封不动地返回出来。

from pysqlite2 import dbapi2 as sqlite

class Point(object):

   def __init__(self, x, y):

      self.x, self.y = x, y

   def __conform__(self, protocol):

      if protocol is sqlite.PrepareProtocol:

          return "%d;%d" % (self.x, self.y)

 

def adapt_converter(point):

   return Point(*[int(x) for x in point.split(";")])

sqlite.register_converter('point', adapt_converter)

con=sqlite.connect(":memory:",detect_types= sqlite.PARSE_COLNAMES)

cur = con.cursor()

cur.execute('select ? as "a [point]"', (Point(2, 3), ))

print type(cur.fetchone()[0])

首先要做一些准备工作,因为 sqlite 不是什么 python 对象都能够接受的 , 为此需要一些必要的转换。这个例子里, sqlite 就像是一个汽车维修站。在站里,为了维修,汽车被拆成了个个零件,等出站时又被组装成了一辆汽车。

第一步是声明一个 Point 类。这个类除了初始化函数 __init__ 之外,还有一个两参量的函数 __conform__ sqlite 在检查存入数据库对象的时候会检查是否存在该函数,如果存在,则将该函数返回的值替代对象本身存入数据库。可以说这是一个转换器,将 python 对象转换为 sqlite 允许接纳对象,即 Null Integer Real Text Blob 其中之一。在这里,显然 sqlite 是无法接纳 Point 这个类所建立的 Point(2,3) 对象的,所以通过 __conform__ ,实际将存入内容为“ 2;3 ”的文本。

使用“ sqlite.register_adapter(Point, lambda p = point : “%d;%d” % p.x, p.y) ”,在 sqlite 中注册一个适配器也可以完成转换的功能,但是我个人认为这样比较麻烦,不如写在类里来得清楚。

在处理完了 Point 类之后,为了使文本“ 2;3 ”在从数据库取出时能转换为 Point 对象,还需要用“ sqlite.register_converter ”注册一个叫“ adapt_converter ”的转换器。

这样一条拆卸组装的流水线就完成了。

实际使用到的只有两样东西,一是数据库连接对象 con ,还有一个就是用来执行命令并返回结果(有必要的话)的游标对象 cur ,它是由 con 对象产生的。

为了使 sqlite 意识到它正在处理的表中的某一列是已注册需要转换的“ phone ”,我们需要在建立表的时候声明该列的属性,或者在查询的时候显示地表明该列的属性。对于前者,建立 con 对象是应将 detect_types 设置为 sqlite.PARSE_DELTYPES ;后者,则是设置为 sqlite.PARSE_COLNAMES 。这里,因为使用的是一个临时表,所以只能使用后者的设置办法。

事实上,“ select ? as …… ”这句包含了两个方面的内容,首先它将 Point(2, 3) 存入了临时表中,这涉及到了 Phone 对象到文本的转换。之后,再将文本数据从临时表中取出,又用到了文本到 Phone 的反转换。

下面是一种浅显而罗嗦的写法:

cur.execute("create table temp(t)")

cur.execute("insert into temp values (?)", (Point(2, 3),))

cur.execute('select ? as "a [point]" from temp')

最后,打印从 sqlite 中取出的对象类型,内容为“ <class '__main__.Point'> ”。

以上这些,可以总结为两点:有必要的话,做好转换;建立 con cur 。理解了这些,差不多就能用 pysqlite 了。当然如果要做工程、写程序还需要很多的其他知识,至少数据库设计这方面的内容是要看一下的。

posted @ 2008-07-04 18:26 gyn_tadao 阅读(3339) | 评论 (2)编辑 收藏

对于pil--handboook中反转图片函数的一点疑问

函数的意思很简单,给定一个Image对象和一个delta参数,然后根据这个delta参数将Image对象割成左右两块提取出来,之后互换两块的位置。
大致看了一下程序之后,自己写了一个,然后对照范例,除了变量名不一样之外基本上是吻合的,但是运行之后的结果却很不容乐观。图像不是左右互换,而是仿佛还有第三块夹在中间,这就不是需要的效果了。
为了找出原因,又仔细查了一遍函数,之后加了两条显示被分割出来的图片的命令。再次运行的结果却一切正常了,但注释掉这两句显示命令之后,毛病又复现。
显然在同一幅图片里进行分割黏贴的操作,pil是存在问题的。所以新建了一个Image对象,用于返回反转之后的图片,问题也得到了顺利的解决。
至于为什么会出现这个错误的现象,暂时不是很清楚。 
原图:


错误的图:


正确的图:


import Image

import ImageTk

def roll(im, delta):

    """Roll an image sideways"""

    

    x, y = im.size

    

    if delta % x == 0:

        return image

        

    im_l = im.crop((0, 0, delta, y))

    im_r = im.crop((delta, 0, x, y))

    

    # im_l.show()

    # im_r.show()

    

    imi = Image.new(im.mode, im.size)

    

    imi.paste(im_r, (0, 0, x - delta, y))

    imi.paste(im_l, (x - delta, 0, x, y))

    

    return imi

    

try:

    image = Image.open(r'd:\s\python\pil\tu\js.jpg')

    im = roll(image, 250)

    if im is not None:

        im.save(r'd:\s\python\pil\tu\js_r.jpg')

except IOError:

    print "Can not open file"


posted @ 2008-07-02 21:03 gyn_tadao 阅读(404) | 评论 (0)编辑 收藏

检测系统可用磁盘

只有一行代码。如下:

for ('a'..'z') { chdir "$_:" && print "$_:\n"}

posted @ 2008-06-14 22:37 gyn_tadao 阅读(408) | 评论 (0)编辑 收藏

IKC初探

     摘要: What is IKC not KFC Component的存在使得我们可以将大多数的精力放在业务逻辑上,但我认为这样同时也减低了程序员对代码的信任度。因为对于像IKC这样一个高级的应用,我们很难确切地理解其中的原理。很多时候我们不得不根据文档亦步亦趋,但始终还是存在那么一些难以定位的逻辑...  阅读全文

posted @ 2008-05-31 17:02 gyn_tadao 阅读(746) | 评论 (3)编辑 收藏

回声服务器的轮子代码解析

Tell me why
为什么要写这个有关轮子代码的解析呢?如果是用 IO::Socket配合select_read等写的代码,一来需要自己处理客户socket的连接,二来还要整理传入的数据,如此这般使得代码量变得 很巨大,对其解析可能会需要很大的工作量,事实上我也大致写过很长的一个。而且这种具有普遍意义的代码任放在哪个平台上其中的内容大多是不会发生变化的, 因而很多地方都可整合起来。在另一个极端上来说,如果使用像POE::Component::Server::TCP的组件,虽然可以将代码量控制在一个 很小的范围,但是同时也隐藏了绝大多数的细节,对于刚刚接触POE的初学者,这无异于一个神奇的盒子。所以要用夹在中间的轮子代码,先有一个大致的概念, 之后再去深究其中的细节。

一拖n的结构
还是老样子,先把代码的大致结构讲一下。和大多数底层的POE程序差不多,一个session拖一屁股事件句柄。

POE :: Session -> create(
    inline_states  =>  {
        _start  =>   \& server_start ,
        server_accepted  =>   \& server_accepted ,
        client_input  =>   \& client_input ,
        client_error  =>   \& client_error ,
        server_error  =>   \& server_error ,
});

_start 事件建立了一个服务socket,再用ListenAccept轮子监听它。当发现有客户socket连接时,触发了server_accept事件。在 该事件中,我们使用ReadWrite轮子来监视客户输入,并将其返回给客户。如果发生意外,根据事故的制造者,分别交予client_error或者 server_error处理。其中server_error将销毁ListenAccept轮子,从而停止整个程序。
所以,不一样关键在于有了ListenAccept和ReadWrite两个轮子。前者用来监听客户socket连接,后者用来处理传入的数据并将它发送回去。这在很大程度上节约了代码量,使我们可以集中更多的注意力在业务逻辑上,而又不至于对于其中的具体实现一头雾水。

记忆点
对 于这两个轮子,需要牢记的有两点,首先是参数。ListenAccept需要至少三个,分别是Handle、AcceptEvent和 ErrorEvent。Handle是监视对象,AcceptEvent是接受连接时触发的事件,到处可见的ErrorEvent则用来处理错误。这三个 缺一不可,不然运行会发生错误,至少需要写一个空句柄。ReadWrite也差不多,只是InputEevnt代替了Listenaccept中的 AcceptEvent,用来处理客户输入。
第二点要注意的是ARG参数。可以这么认为,ARG0是与轮子具体功能相关的, ARG1则代表了轮子本身的ID。打个比方,因为ListenAccept是用来监听并接受客户连接的,那么ARG0就是被接受了的客户。由于对于轮子的 使用经常是跨事件的,所以必须保留对于轮子的引用。ID因其唯一性,所以可以在哈希表中作为键值以保存对轮子的引用。

测试

#!/usr/bin/perl
use warnings;
use strict;
use POSIX;
use IO :: Socket;
my $sock = IO::Socket::INET->new(
    PeerHost => '127.0.0.1',
    PeerPort => 12345,
    ) or die "can't connect to server: $@\n";
my $word = "hello.\nmy name is Jinhao.\nwlecome to POE. a \n";
$sock->send($word);
$buffer = '';
$sock->recv($buffer, POSIX::BUFSIZ);
print $buffer unless $buffer eq '';
$sock->shutdown(0);

客 户端还是在本地,应用了一个IO::Socket来发送一段带“\n”的数据,之后接受返回并打印结果。可以印证一点,ReadWrite轮子只有在发现 换行才起作用,也就是说如果发送的数据的最后不是以“\n”结尾的,那么打印结果会少一段。至于为什么没有发现数据结尾,我想可能跟操作系统有关系。


posted @ 2008-05-29 08:29 gyn_tadao 阅读(398) | 评论 (1)编辑 收藏

对于poe cookbook中Tk interface一例的解析

起因

Perl-Tk windows下的表现着实让我头疼了很长一段时间,最无法忍受的一点是缺乏稳定的线程支持。比如,无法在一个界面上一边录制新闻,一边完成下一个录制时间的设定,整个程序被阻塞在那儿无法动弹。某些情况下可以使用非阻塞的本地socket来实现进程间的通信,但这么做很费劲,而且使得程序像一个奇怪的网络服务器。POE作为在单进程中实现并行的框架,提供了对Tk的支持,很好地解决了组件间通信的问题。

大致的介绍

# !/usr/bin/perl

use  warnings;
use  strict;

use  Tk;
use  POE;

POE
:: Session -> create(
    inline_states 
=>  {
        _start 
=>   \& ui_start ,
        ev_count 
=>   \& ui_count ,
        ev_clear 
=>   \& ui_clear ,
    }
);

$poe_kernel -> run();
exit   0 ;

sub  ui_start {
    
my  (  $kernel ,   $session ,   $heap  )  =   @_ [ KERNEL ,  SESSION ,  HEAP ];
    
    
$poe_main_window -> Label(  - text  =>   " Counter "  ) -> pack ;
    
$poe_main_window -> Label(  - textvariable  =>   \ $heap -> {count} ) -> pack ;
    
    
$poe_main_window -> Button(
        
- text  =>   " Clear " ,
        
- command  =>   $session -> postback( " ev_clear " ) ,
    )
-> pack ;
    
    
$kernel -> yield( " ev_count " );
}

sub  ui_count {
    
$_ [HEAP] -> {count} ++ ;
        
$_ [KERNEL] -> delay(  " ev_count "   =>   . 5 );

    
#  $_[KERNEL]->yield("ev_count");
}

sub  ui_clear {
    
$_ [HEAP] -> {count}  =   0 ;
}

很容易可以看出来一个不一样的地方,比如说用$poe_kernel代替了一贯的POE::Kernel,但在实际运行中会发现$poe_kernel是对POE:;Kernel的引用,最终起作用的依然是POE::Kernel。好了,除了这一点外,其他还是与传统的POE程序差不多。

首先建立了一个包含了内置_start和两个自定义(ev_countev_clear)事件的session。之后在_start事件中,在已初始化了的$poe_main_window上建立一个内容为“Counter”的标签,内容为变量$_[HEAP]->{count}的标签和一个用来清零的按钮。至于ev_countev_clear事件不过是自增和清零$_[HEAP]->{count}而已,其中ev_count事件会递归回调自身以实现不断自增的过程。

如果一切顺利,运行之后的结果如下图所示:

poe_tk.JPG 

单击Clear按钮,中间的数字将从零开始继续飞速自增。如果想让它自增得慢一点,或者说可以看得清,可以在ev_count事件中delay自身回调一小段时间。需要注意的一点是不要指望用sleep,因为它会让整个POE崩溃。

一些细节

下面来讲一下隐藏在这一切背后的一些小细节。事实上,这些细节危险重重,任何细小的错误哪怕是位置的改变都可能导致程序的失败,而原文中的某些注释也比较晦涩或者是存在误导,需要详细说明一下。

$poe_main_window strict环境中出现,这让我很吃惊。一般情况下,这样的非my变量是无法通过检测的。唯一的解释是use POE在背后干了很多事情,多得连POE::Kernel都增加了叫一个$poe_kernel的引用,当然这是为了保证POE::Kernel不被自动垃圾回收。在这里不得不说的是,use Tk必须出现在use POE之前,否则use POE不会初始化生成$poe_main_window。所以可以这么认为,use POE会通过检测其之前的模块调用来决定之后的框架行为,这点在其他模块的应用中不是很多见,至少在我是第一次看见。

原文中有这么一句话“Widgets we need to work with later, such as the counter display,must be stored somewhere.The heap is a convenient place for them.”意思是说“要在之后用到的组件,比如说显示自增计数的标签,必须存储在某个地方,而堆栈就是一个方便的场所。”这也让我感到很困惑,至少在这个例子中,之后没有显式地用到这个计数显示标签。如果非要说用到,那么就是ev_count事件中的$_[HEAP]->{counter}textvariable的身份影响到其宿主,但事实上这非常隐晦,而且为此保存一个引用似乎也毫无意义。后来的实验证明了我的猜测,在注释掉这个引用之后程序依然可以正常运行,也就是说原文这句话并非针对于本例子或者说毫不相干,其中的“such as”极易引人误入歧途。它真正想说的是,为了可以在某个事件中改变组件的行为,需要一个全局可见的引用,而$_[HEAP]正是恰当的选择,其实用一个全局哈希表就可以得到相同的效果,只是这么多就显得多余了。

最后的一点是有关于POE本身的机制。在ev_count中为了实现不断自增,在其事件处理句柄中有一句自身回调,而在ev_clear中仅仅是将计数清零了而已。一开始我有些担心自增行为会中断,因此在ev_clear中增加了一句触发ev_count事件的命令,但这么做是多余的。首先POE中的每一个事件句柄都是并行的,因此ev_clear不会中断ev_count的行为;第二,作为两个事件之间关联的全局变量$_[HEAP]->{counter},POE为其提供了锁保护,不必担心其中一个事件的行为导致另外一个崩溃。

几句话

因为不是正儿八经地写论文,所以遣词造句就显得随性了一些。倒不是说我可以不认真了,相反地,对于一些细节倒是表现得更自然。

这个例子非常简单,简单得几乎没有实用价值,但是说明了问题。我认为一个好的tutorial就应该这样,简单而能说明问题。简单就是说容易让人看懂,说明问题则是把注意事项讲清楚了,之后全凭各人兴趣和悟性了。POE Cookbook是一本非官方的技术文集,没有出版物,甚至说如果你写了一个好的例子,也可以被收录其中。但是每个例子都很通俗易懂,从最简单的入手,由浅入深,最后形成一个系统。反观国内的一些技术出版物,往往枯燥理论先讲一大堆,吓跑一批人;再举一两个可以堪称作者代表作的复杂例子,又闷掉一批人,结果是弄得是曲高和寡。

posted @ 2008-05-26 20:01 gyn_tadao 阅读(402) | 评论 (0)编辑 收藏

POE状态机入门与进阶

一.前言
至于POE的应用,我不想多说什么,因为需要使用状态机的地方太多。举一个极端的例子,windows下的perl-tk对于多线程的支持极不稳定,如果在其中加入一个大数据量的处理应用,结果往往会是一个无法动弹的程序。这时除了使用POE,也许没有更好的解决办法了。
另外,python中有叫twisted的类似框架,被广泛地应用在网络服务中,具体的使用方法可以参考Oreilly出版的《Python Twisted Network Programming Essentials》。某种意义上,也说明了状态机的重要性。

二.基本原理与概念

在详细说明POE服务器的建立步骤之前,需要对POE的原理做一个大致的了解。在这个阶段中,我们将描述这方面的不同概念,以及它们在POE中的应用。

1. 事件与事件句柄
POE 是一个为网络工作和并行任务服务的事件驱动框架。首先作为前提,需要掌握事件和事件驱动编程的意义。
在抽象意义上,事件就是真实世界中发生的一件事情。比如说:早晨打铃、面包从烤机里弹出、茶煮好了等。而在用户界面上最常见的事件则是鼠标移动、按钮点击和键盘敲打等等。
具体到程序软件事件,则往往是一些抽象的事件。也就是说,它不仅包括了发送给程序的外部活动,而且也包括了一些在操作系统内部运行的事件。比如说,计时器到点了,socket建立了连接,下载完成等。
在事件驱动程序中,中心分配器的作用是将事件分配给不同的处理程序。这些处理程序就是事件句柄,顾名思义,它们的任务就是处理相应事件。
POE的事件句柄之间的关系是合作性质的。没有两个句柄会同时运行,每一个句柄在被激发运行期间将独占程序。通过尽可能快地返回来保证程序的其它部分得以顺畅运行,这就是事件句柄之间的合作方式。

2. POE程序的组成部分
最简单的POE程序包括两个模块和一些用户代码:它们分别是POE::Kernel,POE::Session以及一些事件句柄。

a. POE::Kernel:
POE::Kernel提供了基于事件的操作系统核心服务。包括I/O事件、警报和其它计时事件、信号事件和一些不被人意识到的事件。POE::Kernel提供不同的方法对这些事件进行设置,比如select_read(), delay()和sig()。
POE::Kernel还能够跟踪事件发生源和与之相关的任务之间的关系。之所以能够这么做,是因为当事件发生时,它将跟踪哪个任务被激活了。于是它便知道了哪个任务调用方法来使用了这些资源,而这些都是自动完成的。
POE::Kernel也知道何事需将任务销毁。它检测任务以确定是否还有事件需要处理,或者是哪个事需要释放占用的资源。当任务没有事件可以触发的时候,POE::Kernel就自动销毁该资源。
POE::Kernel会在最后一个session停止以后终止运行。

b. POE::Session:
POE::Session实例就是上面所讲的由POE::Kernel管理的“任务”。(以下的章节中为了便于识别将使用“session”)
每一个session都有一个自己私有的存储空间,叫“heap”。存储在当前session的heap中的数据很难被一个外部session得到。
每一个session还拥有自己的资源和事件句柄。这些资源为拥有它们的session生成事件,而事件只被指派到其所处的session中。举例说明,有多个session都可以设置相同的警报,并且任何一个都能接受其所请求的计时事件。但所有其他session不会在意发生在它们之外的事情。

c. 事件句柄:
事件句柄就是Perl程序。它们因为使用了POE::Kernel传递的参数而不同于一般的perl程序。
POE::Kernel是通过@_来传递参数。该数组的前七个成员定义了发生该事件的session的上下文。它包括了一个指向POE::Kernel运行实例的引用、事件自身的名字、指向私有heap的引用以及指向发出事件的session的引用。
@_中剩下的成员属于事件自身,其中的具体内容依照被指派的事件类型而定。举例说明:对于I/O事件,包括两个参数:一个是缓冲文件句柄,另一个是用来说明采取何种行为(input、output或者异常)的标记。
POE不强求程序员为每一个事件句柄分配所有的参数,要不然这将变成一件非常烦人的工作,因为它们中的一些参数是不常被用到的。而POE::Session会自动为@_输出剩余的常量,这样就能使我们相对比较轻松地将注意力放在重要的参数上,而让POE来处理不必需的参数。
POE还允许改变参数的顺序和数量,而不会对程序造成影响。比如说,KERNEL,HEAP和ARG0分别是POE::Kernel实例、当前session的堆栈和事件的第一个用户参数。它们可以一个个直接从@_被导出。
my $kernel = $_[KERNEL];
my $heap = $_[HEAP];
my $thingy = $_[ARG0];
或者一次性以队列片段的形式赋值给程序参数。
my ( $kernel, $heap, $thingy ) = @_[KERNEL, HEAP, ARG0];
当然在事件句柄中我们也可以直接使用$_[KERNEL],$_[HEAP]和$_[AG0]。但是因为诸如ARG0的参数很难从字面上知道它在事件中代表的真实意义, 所以我们不提倡直接使用这种做法。

三.简单的POE例子
现在大致知道了POE编程的概念,我们将举若干例子来了解它到底是怎么运行的。

1. 一个单session的例子
简单的POE程序包括三个部分:一个用来加载模块和配置条件的前端,初始化并且运行一个或者多个session的主体和用来描述事件句柄的具体程序。

a. 前端:
#!/usr/bin/perl

use warnings;
use strict;
use POE;
引入POE模块的过程的背后隐藏了一些细节,事实上这么做还加载了一些诸如POE::Kernel和POE:Sesson的模块并相应地做了一些初始化,而通常在每个POE程序中我们都会用到这些隐藏模块。
在POE::Kernel第一次被引入时,它生成了一个将贯穿整个程序POE::Kernel实例。POE::Session会根据不同的事件输出默认常量给事件句柄的某些参数,如:KERNEL,HEAP,ARG0等等。
所以一个简单的use POE为程序做了大量的初始化工作。

b. 主体session:
当所有的条件都准备好之后,为了保证POE::Kernel的有效运行,我们必须建立至少一个session。不然的话,运行程序意味着无事可做。
在这个例子里,我们将建立一个包含_start,_stop和count这三个事件的任务。POE::Session将每个事件与一个句柄联系在一起。
POE::Session->create{
    Inline_states => {
        _start => \&session_start,
        _stop => \&session_stop,
        count => \&session_count,
    }
};
前两个事件是由POE::Kernel自身所提供的。它们分别表示该session的启动和销毁。最后一个事件是用户自定义事件,它被用于程序的逻辑之中。
我们之所以没有为session保留一个引用的原因是因为该session会自动被注册到POE::Kernel中,并接收它的管理,而我们在程序是很少直接使用该session的。
事实上,保存一个session的应用是存在危险的。因为如果存在显式的引用,Perl将不会自动销毁session对象或者重新为其分配内存。
接着我们启动POE::Kernel,由此便建立了一个用来探测并分派事件的主循环。在此示例程序中,为了使运行结果更加明确,我们将注明POE::Kernel运行的开始处和结束点。
print “Starting POE::Kernel.\n”;
POE::Kernel->run();
print “POE::Kernle’s run method returned.\n”;
exit;
Kernel的run方法只有在所有session返回之后才会停止循环。之后,我们调用一个表示程序结束的提示符的exit系统方法来表示程序被终止,而在实际的应用中这么做是不必要的。

c. 事件句柄:
下面我们来了解一下事件句柄的应用,首先从_start开始。_start的句柄将在sesson初始化完成之后开始运行,session在其自身的上下文中使用它来实现输入引导。比如初始化heap中的值,或者分配一些必要的资源等等。
在该句柄中我们建立了一个累加器,并且发出了“count”事件以触发相应的事件句柄。
sub session_start {
    print "Session ", $_[SESSION]->ID, " has started.\n";
    $_[HEAP]->{count} = 0;
    $_[KERNEL]->yield("count");
}
一些熟悉线程编程的人可能会对yield方法在这里的使用产生困惑。事实上,它并不是用来中止session运行的,而是将一个事件放入fifo分派队列的末尾处,当队列中在其之前的事件被处理完毕之后,该事件将被触发以运行相应的事件句柄。这个概念在多任务环境下更容易被理解。我们可以通过在调用yield方法之后立即返回的办法,来清晰地体现yield方法的行为。
接下来的是_stop句柄。POE::Kernel将在所有session再无事件可触发之后,并且是在自身被销毁之前调用它。
sub session_stop {
    print "Session ", $_[SESSION]->ID, " has stopped.\n";
}
在_stop中设置一个事件是无用的。销毁session的过程本身包括清理与之相关的资源,而事件就是资源的组成部分。所以对于所有在_stop中的事件,在其能够被分派之前都是将被清理的。
最后讲一下count事件句柄。该函数用来增加heap中的累加器计数,并打印累加结果。我们可以使用一个while来完成这件工作,但是用yield方法一来可以使得程序更短小精悍,二来还能够加深对POE事件处理原理的理解。
sub session_count {
    my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
    my $session_id = $_[SESSION]->ID;

    my $count = ++$heap->{count};
    print "Session $session_id has counted to $count.\n";

    $kernel->yield("count") if $count < 10;
}
该函数的最后一句表示:只要累加器计数未超过10,session将再yield一个count事件。因为不断地触发了session_count句柄,使得当前session可以继续得以生存而不会被POE::Kernel清理。
当计数器到10时,便不再调用yield命令,session也将停止。一旦POE::Kernel检测到该session再没有事件句柄可被激发,便在调用_stop事件句柄之后将其清理销毁。
以下是运行的结果:
  Session 2 has started.
  Starting POE::Kernel.
  Session 2 has counted to 1.
  Session 2 has counted to 2.
  Session 2 has counted to 3.
  Session 2 has counted to 4.
  Session 2 has counted to 5.
  Session 2 has counted to 6.
  Session 2 has counted to 7.
  Session 2 has counted to 8.
  Session 2 has counted to 9.
  Session 2 has counted to 10.
  Session 2 has stopped.
  POE::Kernel's run() method returned.
对于该结果有几点需要解释一下。
?    为什么在运行结果中session的id是2。因为通常情况下,POE::Kernel是最先被创建的,它的id号会是1。接下来创建session的id号依次被累加。
?    因为当运行POE::Session->create时就会分派_start事件,所以_start事件句柄的激发是在POE::Kernel运行之前的。
?    第一个count事件句柄并没有被立即处理。这是因为该事件被Kernel放入了分派队列之中。
?    导致session停止的原因除了再没有事件可触发而之外,外部的终止信号也可以用来停止session。

2.多任务的POE例子

可以将以上的这个计数程序做成多任务的形式,使每一个session将在其自身的heap中保存累加器。各个session的事件被依次传送到POE::Kernel的事件队列中,并以先进先出的形式进行处理,以保证这些事件将轮流被执行。
为了演示这个结果,我们将复制以上程序中的session部分,其它部分保持原样不变。
for ( 1 .. 2 ) {
    POE::Session->create(
        inline_states => {
            _start => \&session_start,
            _stop  => \&session_stop,
            count  => \&session_count,
          }
    );
}
以下便是修改后的程序的运行结果:
  Session 2 has started.
  Session 3 has started.
  Starting POE::Kernel.
  Session 2 has counted to 1.
  Session 3 has counted to 1.
  Session 2 has counted to 2.
  Session 3 has counted to 2.
  Session 2 has counted to 3.
  Session 3 has counted to 3.
  Session 2 has counted to 4.
  Session 3 has counted to 4.
  Session 2 has counted to 5.
  Session 3 has counted to 5.
  Session 2 has counted to 6.
  Session 3 has counted to 6.
  Session 2 has counted to 7.
  Session 3 has counted to 7.
  Session 2 has counted to 8.
  Session 3 has counted to 8.
  Session 2 has counted to 9.
  Session 3 has counted to 9.
  Session 2 has counted to 10.
  Session 2 has stopped.
  Session 3 has counted to 10.
  Session 3 has stopped.
  POE::Kernel's run() method returned.
每一个session是在自身heap中保存计数数据的,这与我们建立的session实例数量无关。POE轮次处理每一个事件,每次只有一个事件句柄被运行。当事件句柄运行的时候,POE::Kernel自身也将被中断,在事件句柄返回之前,没有事件被分派。
当各个session的事件被传送到主程序事件队列后,位于队列头部的事件被首先处理,新来的事件将被放置在队列的尾部。以此保证队列的轮次处理。
POE::Kernek的run方法在最后一个session停止之后返回。

四.回声服务器
最后我们将用IO::Select建立一个非派生的回声服务器,然后再利用多个抽象层的概念将它移植到POE上。

1. 一个简单的select()服务器
这个非派生的服务器的原型来自于《Perl Cookbook》中的17.13章节。为了保持简洁并且也是为了更方便于移植到POE上,对其做了一些修改。同时为了增加可读性,还给该服务器设定一些小的目的和功能。
首先,需要引入所需的模块并初始化一些数据结构。
#!/usr/bin/perl

use warnings;
use strict;

use IO::Socket;
use IO::Select;
use Tie::RefHash;

my %inbuffer  = ();
my %outbuffer = ();
my %ready = ();

tie %ready, "Tie::RefHash";
接下来,我们要建立一个服务器socket。为了不阻塞单进程的服务器,这个socket被设置为非阻塞状态。
my $server = IO::Socket::INET->new
  ( LocalPort => 12345,
    Listen => 10,
  ) or die "can't make server socket: $@\n";

$server->blocking(0);
然后建立主循环。我们制造一个IO::Socket对象用以监视socket上的活动。无论何时,当有一个事件发生在socket上,都会有相应的程序来处理它。
my $select = IO::Select->new($server);

while (1) {
    foreach my $client ( $select->can_read(1) ) {
        handle_read($client);
    }

    foreach my $client ( keys %ready ) {
        foreach my $request ( @{ $ready{$client} } ) {
            print "Got request: $request";
            $outbuffer{$client} .= $request;
        }
        delete $ready{$client};
    }

    foreach my $client ( $select->can_write(1) ) {
        handle_write($client);
    }
}

exit;
以上的主循环对整个程序做了一个大致的总结。下面是用于处理socket不同行为的几个函数。
第一个函数用来处理可读状态的socket。如果这个准备就绪的socket是服务器的socket,我们再接收一个新的连接,并将它注册到IO::Socket对象中。如果这是一个存在输入数据的客户端socket,我们读取数据并对其进行处理,并将处理的结果添加到%ready数据结构中。主循环会捕获在%ready中的数据,并将它们回传给客户端。
sub handle_read {
    my $client = shift;

    if ( $client == $server ) {
        my $new_client = $server->accept();
        $new_client->blocking(0);
        $select->add($new_client);
        return;
    }

    my $data = "";
    my $rv   = $client->recv( $data, POSIX::BUFSIZ, 0 );

    unless ( defined($rv) and length($data) ) {
        handle_error($client);
        return;
    }

    $inbuffer{$client} .= $data;
    while ( $inbuffer{$client} =~ s/(.*\n)// ) {
        push @{ $ready{$client} }, $1;
    }
}
接下来是一个处理可写状态的socket的函数。等待被发送到客户端的数据将被写到这个socket中,之后被从输出缓冲中删除。
sub handle_write {
    my $client = shift;

    return unless exists $outbuffer{$client};

    my $rv = $client->send( $outbuffer{$client}, 0 );
    unless ( defined $rv ) {
        warn "I was told I could write, but I can't.\n";
        return;
    }

    if ( $rv == length( $outbuffer{$client} ) or
        $! == POSIX::EWOULDBLOCK
      ) {
        substr( $outbuffer{$client}, 0, $rv ) = "";
        delete $outbuffer{$client} unless length $outbuffer{$client};
        return;
    }

    handle_error($client);
}
最后我们需要一个程序来处理客户socket在读取和发送数据时产生的错误。它会为发生错误的socket做一些清理工作,并保证它们被正确关闭。
sub handle_error {
    my $client = shift;

    delete $inbuffer{$client};
    delete $outbuffer{$client};
    delete $ready{$client};

    $select->remove($client);
    close $client;
}
短短130行代码,我们就有了一个简单的回声服务器。不算太坏,但是我们可以做得更好。

2. 将服务器移植到POE上
为了把IO::Socket服务器移植到POE上,需要使用到某些POE的底层特征。为了详细说明的需要,我们竟可能地不省略细节,而最终的程序也将保留其中的大部分代码。
事实上,以上的IO::Socket服务器本身就是由事件驱动的,在其中包含了一个用于检测并分派之间的主循环,配以处理这些事件的相应事件句柄。从这一点上来说,与POE的原理和架构有异曲同工的意思。
新的服务器程序需要一个如下所示的POE空框架。用于具体功能实现的代码将被添加到这个框架之中。
#!/usr/bin/perl

use warnings;
use strict;

use POSIX;
use IO::Socket;
use POE;

POE::Session->create
  ( inline_states =>
      {
      }
  );

POE::Kernel->run();
exit;
在继续完成接下来的程序之前,为了勾勒出程序的大致框架结构,必须明确哪些事件的出现是必要的。
?    服务器启动,完成初始化。
?    服务器socket准备就绪,可以接收连接。
?    客户socket处于可读取状态,服务器读取数据并对其进行处理。
?    客户socket处于可写状态,服务器对其写入一些数据。
?    客户socket发生错误,需要将其关闭。
一旦知道了需要做些什么,就可以建立这些事件的名称,并为这些事件编写相应的事件处理句柄,从而快速地完成POE::Session的构造。
POE::Session->create
  ( inline_states =>
      { _start => \&server_start,
        event_accept => \&server_accept,
        event_read   => \&client_read,
        event_write  => \&client_write,
        event_error  => \&client_error,
      }
  );
现在是真正将IO::Select代码移植过来的时候了!和IO::Select服务器相同,需要为客户socket提供输入和输出缓冲,而且由于这两个缓冲对socket句柄的重要性并且不存在冲突,它们将被保持为程序的全局变量。另外,在这里将不再使用%ready哈希表。
my %inbuffer  = ();
my %outbuffer = ();
紧接着是引入IO::Select的程序片段,因为每一段都是上面指定的事件所触发的,因此这些片段将被移植入相应事件的处理句柄中。
在_start事件的处理句柄中,需要建立一个服务器的监听socket,并用select_read为其分配一个事件发生器。句柄中用到的POE::Kernel模块中的select_read方法接收两个参数:第一个是需要监视的socket,第二个是当该socket处于可读状态时所触发的处理句柄。
sub server_start {
    my $server = IO::Socket::INET->new
      ( LocalPort => 12345,
        Listen => 10,
        Reuse  => "yes",
      ) or die "can't make server socket: $@\n";

    $_[KERNEL]->select_read( $server, "event_accept" );
}
注意一点,我们并没有保存服务器socket。因为POE::Kernel会对其进行跟踪,并将其作为一个参数传递给event_accept事件句柄。只有在需要特殊用途的情况下,我们才会保存一个该socket的拷贝。
再回顾POE::Session构造器,事件event_accept会激发server_accept事件句柄。该句柄接收一个新的客户socket,并对其分配一个监视器。
sub server_accept {
    my ( $kernel, $server ) = @_[ KERNEL, ARG0 ];

    my $new_client = $server->accept();
    $kernel->select_read( $new_client, "event_read" );
}
之后我们在client_read句柄中处理处理来自客户的数据。当新连接的客户socket处于可读状态时,句柄被触发。该句柄中的第一个客户参数即为所连接的客户socket,因此我们无需再为其保留一个拷贝。
在内容上,句柄client_read与IO::Select服务器中的handle_read几乎一样。而由于handle_read的accept部分的内容被移植到了server_accept句柄中,相应地就不再需要%ready哈希表了。
如果接收过程发生错误,客户socket通过POE::Kernel的yield方法将被传送到event_error句柄中。因为该yield方法是根据程序员的具体要求发送事件的,所以需要在yield中将发生错误的客户socket作为某个参数,而此socket在处理该event_error的事件句柄client_error中被赋值给$_[ARG0]。
接着,如果在client_read中发现输出缓存存在数据,我们将检测以保证当该客户socket处于可写状态时,及时触发事件处理句柄,将数据发送出去。
sub client_read {
    my ( $kernel, $client ) = @_[ KERNEL, ARG0 ];

    my $data = "";
    my $rv   = $client->recv( $data, POSIX::BUFSIZ, 0 );

    unless ( defined($rv) and length($data) ) {
        $kernel->yield( event_error => $client );
        return;
    }

    $inbuffer{$client} .= $data;
    while ( $inbuffer{$client} =~ s/(.*\n)// ) {
        $outbuffer{$client} .= $1;
    }

    if ( exists $outbuffer{$client} ) {
        $kernel->select_write( $client, "event_write" );
    }
}
在用于发送数据的事件句柄中,第一个客户参数依然是一个可用的socket。在该句柄中,如果输出缓存为空,则停止检测并迅速返回。否则,我们将试图将缓冲内的数据全部发出。如果所有数据均发送成功,该缓冲将被销毁。与client_read类似,client_write中也有相应的错误处理句柄。
sub client_write {
    my ( $kernel, $client ) = @_[ KERNEL, ARG0 ];

    unless ( exists $outbuffer{$client} ) {
        $kernel->select_write($client);
        return;
    }

    my $rv = $client->send( $outbuffer{$client}, 0 );
    unless ( defined $rv ) {
        warn "I was told I could write, but I can't.\n";
        return;
    }

    if ( $rv == length( $outbuffer{$client} ) or
        $! == POSIX::EWOULDBLOCK
      ) {
        substr( $outbuffer{$client}, 0, $rv ) = "";
        delete $outbuffer{$client} unless length $outbuffer{$client};
        return;
    }

    $kernel->yield( event_error => $client );
}
最后说明一下在以上两个句柄中被用到的错误处理句柄。我们首先删除了客户socket的输入输出缓存,再关闭建立在该socket上的所有监视,最后保证该socket被成功关闭。如此这般便有效地关闭了来自于客户的连接。
sub client_error {
    my ( $kernel, $client ) = @_[ KERNEL, ARG0 ];

    delete $inbuffer{$client};
    delete $outbuffer{$client};

    $kernel->select($client);
    close $client;
}
移植成功!

posted @ 2008-05-09 11:20 gyn_tadao 阅读(1496) | 评论 (0)编辑 收藏

多看代码是提高能力的捷径

最近一直有在学习POE,这是一个类似于状态机的网络框架,在python上有叫twisted的类似框架。为此也看了一些文章,并学习了一些例子。
对于SOCKET编程,也许是很久没有用了的缘故,看到相关的一段程序时,感觉相当地眼生。还因为之前一直是用perl原生的socket命令来写的,所以对IO::Socket没有做过多的了解。再去看书,或者将perldoc里的内容从头到尾地通读一遍,一来没有什么兴趣,二来就以往经验来看很多内容都是很少用到的,看了也容易忘记。所以就将这段代码抄了一遍,对于其中不了解或者有疑问的再去查perldoc,或者写一些测试代码来验证。事实证明收获也是不小,很快就了解了其中的主要内容,顺带地还将POSIX中的一些常量搞搞清楚。更重要的是在抄写这样规范的代码,对于自身编写程序的格式也会有潜移默化的影响。
这么做的关键在于,首先要有一段好的代码,第二不要抱着得过且过的心态放过一些细节。
#!/usr/bin/perl

use warnings;
use strict;

use POSIX;
use IO::Socket;
use IO::Select;
use Tie::RefHash;

# Create the server socket.

my $server = IO::Socket::INET->new(
        LocalPort 
=> 12345,
        
Listen => 10,
    ) or 
die "can't make server socket: $@\n";
    
$server->blocking(0);

# Set structures to track input and output data.

my %inbuffer = ();
my %outbuffer = ();
my %ready = ();

tie %ready, "Tie::RefHash";

# The select loop itself.

$my select = IO::Select->new($server);

while(1) {
    
    
# Process sockets that are ready for reading.
    foreach my $client$select->can_read(1) ) {
        handle_read(
$client);
    }
    
    
# Process any complete requests. Echo the data back to the client,
    # by putting the ready lines into the client's output buffer.

    foreach my $client ( keys %ready ) {
        
foreach my $request ( @{ $ready{$client} } ) {
            
print "Got request: $request\n";
            
$outbuffer{$client.= $request;
        }
        
delete $ready{$client};
    }
    
    
# Process sockets that are ready for writing.
    foreach my $client ( $server->can_write(1) ) {
        handle_write(
$client);
    }
}

exit;

# Handle a socket that's ready to be read from.

sub handle_read {
    
my $client = shift;
    
    
# If it's the server socket, accept a new client connection.
    if ( $client == $server ) {
        
my $new_client = $server->accept();
        
$new_client->blocking(0);
        
$select->add($new_client);
        
return;
    }
    
    
# Read from an established client socket.
    my $data = "";
    
my $rv = $client->recv$data, POSIX::BUFSIZ, 0 );
    
    
# Handle socket errors.
    unless ( defined($rv) and length($data) ) {
        handle_error(
$client);
        
return;
    }
    
    
# Successful read. Buffer the data we got, and parse it into lines.
    # Place the lines into %ready, where they will be processed later.

    $inbuffer($client.= $data;
    
while ( $inbuffer{$client=~ s/(.*\n)// ) {
        
push @{ $ready{$client} }, $1;
    }
}

# Handle a socket that's ready to be write to.

sub handle_write {
    
my $client = shift;
    
    
# Skip this client if there's nothing write.
    return unless exists $outbuffer{$client};
    
    
# Attempt to write pending data to the client.
    my $rv = $client->send$outbuffer{$client}, 0 );
    
unless ( defined $rv ) {
        
warn "I was told I could write, but I can't.\n";
        
return;
    }
    
    
# Successful write. Remove what was sent from the output buffer.
    if ( $rv == length$outbuffer{$client} ) or $! ==POSIX::EWORLDBLOCK ) {
        
substr$outbuffer($client), 0, $rv ) = "";
        
delete $outbuffer{$clientunless length $outbuffer{$client};
        
return;
    }
    
    
# Otherwise there was an error.
    handle_error($client);
}

# Handle client error. Clean up after dead socket.
sub handle_error {
    
my $client = shift;
    
    
delete $inbuffer{$client};
    
delete $outbuffer{$client};
    
delete $ready{$client};
    
    
$select->remove($client);
    
close $client;
}

posted @ 2008-05-04 16:36 gyn_tadao 阅读(706) | 评论 (2)编辑 收藏

应用JMF编写mp3播放器

     摘要: 通过网上搜索,可以找到很多利用JMF播放单个文件的代码,写一个简单的播放器书是轻而易举的事情。而事实上JMF中的start似乎就可以让我们搞定一切。但是这样的东西,或者说仅仅一段代码,实在不能说是播放器。我希望让这个播放器尽量地可以使用,我的意思是对于一个提供的播放列表,它至少可以连续播放以及暂停、前进和后退。如果能做到这些,对于我自己,也许还有使用并且修改它的热情。首先要解决的一个问题是如何实现...  阅读全文

posted @ 2008-04-11 17:16 gyn_tadao 阅读(2348) | 评论 (4)编辑 收藏

仅列出标题
共11页: 1 2 3 4 5 6 7 8 9 Last 
<2024年5月>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

导航

统计

常用链接

留言簿(15)

随笔分类(126)

随笔档案(108)

相册

搜索

最新评论

阅读排行榜

评论排行榜