September 27, 2005

perl client for Authlib - 一个courier认证库的客户端

有时候,人懒的话,就啥事也觉得不想干不想做,觉得做起来都麻烦,一直都想做一个通过courier套件中的authlib认证用户信息的插件模块,却因懒惰而拖延。

今天,不知道哪里来的动力,翻开cyrus-sasl 2.1.19的source,看了一把,用perl简单的实现了一个courier-authlib的客户端,支持查询和修改密码。

这样这个模块应用到extmail中,就成为了目前为止第四个认证模块了。

Authdamond 认证基本原理

注意:这个所谓原理只是我看checkpw.c 粗浅的认识,详细的原理请直接在courier官方网站查找,或直接联系Mr Sam(courier作者)

认证(查询信息):

1. 客户端搜集用户名/密码,并计算查询信息总长度
2. 创建Unix socket,连接到authdaemond的socket绑定文件
3. 将查询信息总长度,服务名,login,用户名,密码等信息以新行间隔开,发送到
    socket,最后以两个新行newline (回车)结束
4. 读authdaemond的返回信息,如果返回信息结尾包括"."则表示成功,否则
    将返回FAIL等出错信息
5. 关闭该socket

修改密码

1. 客户端搜集用户名/密码和新密码
2. 创建Unix socket,连接到authdaemond的socket绑定文件
3. 将服务名,用户名,旧密码,新密码发送到该socket,注意与查询不同,信息之间
   以\t间隔,结束以一个新行结束。
4. 检查返回结果,如果是成功则返回OK,失败则返回FAIL
5. 关闭socket

authdameond_cli.pl源码

具体代码见下文,请阅读本文的读者注意:
程序代码以GPL版权释出,作者不对代码作出任何担保,不回答有关问题,使用与否与作者无关


#!/usr/bin/perl -w
# vim: set cindent expandtab ts=4 sw=4:
#
# authdaemond_cli.pl
# ==================
# A small progarme to query/change user information via courier 
# authlib, only test on courier-authlib 0.5x, use it as your risk!
#
# License: GPL v2
# Author: He zhiqiang <hzqbbc@hzqbbc.com>
# Copyright (c) 1998-2005

use IO::Socket::UNIX;
use vars qw($socket_path);
$socket_path = '/var/spool/authdaemon/socket';

usage() unless(scalar @ARGV>1);

my $sock = IO::Socket::UNIX->new($socket_path) or die  "Error: $!\n";
my ($user, $pass) = ($ARGV[0], $ARGV[1]);
my ($serv, $type) = ('authdaemond', 'login');

if($ARGV[2]) {
     print STDOUT "Changing password for $user ...\n";
     printf($sock "PASSWD %s\t%s\t%s\t%s\n",
         $serv,
         $user,
         $pass,
         $ARGV[2]
     );
     print while(<$sock>);
}else {
     my $len = length($user.$pass.$serv.$type)+4;
     print STDOUT "Querying information for $user ...\n";
     printf($sock "AUTH %s\n%s\n%s\n%s\n%s\n\n",
         $len,
         $serv,
         $type,
         $user,
         $pass
     );
     print while(<$sock>);
}

# close the socket
$sock->shutdown(1);

# ===================== #
# small function to use #
# ======================#

sub usage {
   print STDERR "$0 username passwd [newpasswd]\n";
   exit(255);
}

使用方法

修改程序中$socket_path,指向系统中authdaemond绑定的socket路径(必须是全路径),假设数据库中有一个用户叫demo@extmail.dns0755.net,密码是demo,则按如下格式执行:

perl authdaemond_cli.pl demo@demo.dns0755.net demo

结果:

Querying information for demo@extmail.dns0755.net ...
UID=1000
GID=1000
HOME=/home/domains/extmail.dns0755.net/demo/
ADDRESS=demo@extmail.dns0755.net
NAME=demo me
QUOTA=104857600S
PASSWD=$1$BdJPD$oBV5a/25BAO2B8B2zOexx0
PASSWD2=demo
.

修改密码则按如下格式执行:

perl authdaemond_cli.pl demo@extmail.dns0755.net demo newpwd

正常的话结果如下:

Changing password for demo@extmail.dns0755.net ...
OK

该小程序还可以作为courier-authlib的authtest的一个补充,有其他需要的朋友可以在此基础上进行修改。

Posted by hzqbbc at 03:32 PM | Comments (1)

June 27, 2005

awstats auto update的方法及perl脚本

awstats是一个功能强大的日志分析工具,对于一个简单的网站而言,只需要根据awstats官方网站的帮助来安装和配置即可使用。但是如果需要服务多个网站,例如提供虚拟主机的ISP,需要统计大量的网站的话,常规的方法有点麻烦。

实现自动统计更新是一个必然的需求。实现起来也比较简单,一些shell脚本+perl脚本,配置一下crontab就差不多了。

1. 配置Apache调整日志存储方式
为每一个虚拟主机单独保存日志,这样便于统计。注意,配置的命令必须在每一个虚拟主机的配置里面,即里,简单的配置如下:(以www.hzqbbc.com为例)

CustomLog /var/log/httpd/www.hzqbbc.com_log combined

2. 配置awstats 的per Host config
为每个要进行统计的虚拟主机单独配置一个配置文件,在我的awstat安装中配置文件的目录放在/etc/awstats下,每个虚拟主机的配置文件名都有如下格式(以hzqbbc.com为例):

awstats.www.hzqbbc.com.conf

每个配置文件里只需要指定几个主要参数即可。其中如下几个参数必须指定:

LogFile="/var/log/httpd/www.hzqbbc.com_log"
SiteDomain="hzqbbc.com"
HostAliases="www.hzqbbc.com 127.0.0.1 localhost"

3. 自动更新脚本
以下是自动更新脚本,命名为cron.pl


#!/usr/bin/perl -w
use strict;
my @list = glob("/etc/awstats/*");

for(0...scalar @list-1) {
         $list[$_]=~s#.*/awstats\.([^\/]+)\.conf$#$1#;
}

foreach(@list) {
         `perl awstats.pl -config=$_ -upate`;
}
exit(0);

将这个perl脚本防止在awstats的cgi-bin目录里,即/var/www/cgi-bin/awstats/wwwroot/cgi-bin里

4. 配置crontab
以root身份登陆系统,执行crontab -e,增加一条记录:

0 */3 * * * (cd /var/www/cgi-bin/awstats/wwwroot/cgi-bin/; perl cron.pl)

这里定义了每隔3小时呼叫一次cron.pl并更新日志。如果机器的负载很高,并且虚拟主机的数量巨大,那么建议每天只更新1次,并且在深夜负载低时进行。

经过上述配置后,以后新增的虚拟主机只需要简单增加一个awstats的配置即可实现自动的日志分析。访问时,只需要将URL中的config=xxx替换成相应网站名即可。

Posted by hzqbbc at 09:19 AM | Comments (1)

June 05, 2005

Mailing List (邮件列表)原理简述及我的perl实现

注: 本文本来是一早要写的,可是程序写了有段时间了,最近一段时间又很忙,居然给忘了,现在补上。

正文

大部分IT人员都使用过邮件列表,或者类似的服务,但邮件列表的内部工作原理则不是简单的订阅,退订阅那么简单。最近根据自己的一些认识,用perl实现了一个非常简单的MLM程序,也顺便谈谈邮件列表的最基本工作原理。

邮件列表,简单的来说,就是任一列表成员向该列表发的邮件,其他所有人(可以包括他自己)都能收到,并且每个人能自由订阅、退订。更丰富的邮件列表还包括了摘要,精确权限管理,web archive功能等等。

著名的开源邮件列表软件如mailman, majodomo, ezmlm, sympa, ecartis等都是功能完备的邮件列表软件,但归根结底,最简单的邮件列表至少应该包含如下功能:

要实现上述的功能,如果使用perl的话并不复杂,配合Postfix MTA可以非常方便的开发出简易的邮件列表软件。以下是自己开发的MMList(Mini Mailing List) 的基本结构:

MMList atomy(流程图)

配置

基于Postfix,使用alias的方法,将邮件通过管道送到MMList:

main.cf里需要配置的内容:

alias_maps = hash:/etc/postfix/aliases hash:/etc/postfix/mml.aliases
virtual_alias_maps = hash:/etc/postfix/mml.virtual_alias_maps

mml.aliases的内容:

# alias file
test-subscribe-hzqbbc.com:   "|/usr/bin/mml -cmd=subscribe -list=test@hzqbbc.com"
test-confirm-hzqbbc.com:     "|/usr/bin/mml -cmd=confirm -list=test@hzqbbc.com"
test-unsubscribe-hzqbbc.com: "|/usr/bin/mml -cmd=unsubscribe -list=test@hzqbbc.com"

mml.virtual_alias_maps的内容:

test-subscribe@hzqbbc.com        test-subscribe-hzqbbc.com
test-confirm@hzqbbc.com          test-confirm-hzqbbc.com
test-unsubscribe@hzqbbc.com      test-unsubscribe-hzqbbc.com

MMList 的perl实现


#!/usr/bin/perl -w
# vim: set cindent expandtab ts=4 sw=4:
# MMList - a very lightweight MLM software
#
# Author: He zhiqiang <hzqbbc@hzqbbc.com>
# CopyRight (c) 1998-2005 hzqbbc.com
#
# License: GPL v2
use strict;
use Getopt::Long;
use vars qw(%cfg $cmd $list @KEY_MAP);
use vars qw($user $subj $SLOG);
$user = $subj = "";

@KEY_MAP = (
    0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E',
    'F','G','H','I','J','K','L','M','N','O',
    'P','Q','R','S','T','U','V','W','X','Y',
    'Z','a','b','c','d','e','f','g','h','i',
    'j','k','l','m','n','o','p','q','r','s',
    't','u','v','w','x','y','z'
);

# proto-type:
# cmd ==> indicate the 'subscribe' or 'unsubscribe'
# list ==> indicate the list name

my $res = GetOptions(
	"cmd=s" => \$cmd,
        "list=s" => \$list
);

$cfg{'basedir'} = "/var/lib/mmlist";
$cfg{'listdir'} = $cfg{'basedir'}."/lists";
$cfg{'hostname'} = "list.hzqbbc.com";

open (MLOG, ">> $cfg{'basedir'}/mail.log");
open ($SLOG, ">> $cfg{'basedir'}/base.log");
# read from STDIN
while(<STDIN>) {
     print MLOG $_;
 
     if(/^From: (.*)$/) {
          chomp;
          m/([a-zA-Z0-9-_=\.]+\@[a-zA-Z0-9-_=\.]+)/;
          if($1) {
               $user = lc $1;
           }
      }elsif(/^Subject: (.*)$/) {
          chomp;
          $subj = $1;
          $subj =~ s/\s//g;
      }
}

syslog("cmd = $cmd");

if($cmd eq "subscribe") {
     if(user_exist($user)) {
          syslog("$user subscribed");
          my $body = q(Hey guy, you have already subscribed!);
          sendmail($user, "Subscribe failure", $body);
      }else {
          my $sid = gen_sid();
          open(FD, "> $cfg{'listdir'}/$list/queue/$user") or
              syslog("$!") and die "Can't write to $user, $!\n";
          printf FD "%s\:%s\n", time, $sid;
          close FD;
  
          syslog("confirm $user");
          my $body = "Hey guy, reply to me with the code $sid \n"
                    ."in the subject section\n";
          $list =~ m/([^:]+)\@(.*)/;
          my $from = "$1-confirm\@$2";
          sendmail($user, "Confirm subscribe", $body, $from);
      }
}elsif($cmd eq "confirm") {
     if(not user_exist($user)) {
          syslog("$user not exist");
          if(valid_sid($user, $subj)) {
               syslog("added $user");
               add_user($user);
               my $body = "Welcome to $list :-)\n";
               sendmail($user, "Added to the list", $body);
           }else {
               syslog("fail to confirm $user");
               my $body = "Hey guy, your confirm fail, please try again\n";
               sendmail($user, "Confirm failure", $body);
           }
      }else {
          my $body = "Hey guy, you step into a wrong situation!\n";
          sendmail($user, "Wrong action", $body);
      }
}elsif($cmd eq "unsubscribe") {
     if(user_exist($user)) {
          syslog("$user removed");
          del_user($user);
          my $body = "Hey guy, you have been removed from the $list\n";
          sendmail($user, "Goodbye - from $list", $body);
      }else {
          my $body = "Hey guy, you step into a wrong situation!\n";
          sendmail($user, "Wrong action", $body);
      }
}else {
     print STDERR "m3 error cmd!\n";
     exit(13);
}

exit(0);

## funcs to handle mail list
sub sendmail {
     my($to, $subj, $body, $from) = @_;
     if(not defined $from) {
          $from = "m3\@$cfg{'hostname'}";
      }
 
     open(CMD, "| /usr/sbin/sendmail -oi -t -f \"$from\" $to") or 
         die "Can't exec /usr/sbin/sendmail, $!\n";
     print CMD <<EOF
 Return-Path: $from
 From: $from
 To: $to
 Subject: $subj
 
 $body
 EOF
 ;
     close CMD;
}

sub user_exist {
     my $user = shift;
     if (! -r "$cfg{'listdir'}/$list/users.txt") {
          return 0;
      }
 
     open(FD, "< $cfg{'listdir'}/$list/users.txt") or die "Can't open $list, $!\n";
     while(<FD>) {
          chomp;
          if(/^$user$/i) {
               return 1;
           }
      }
     close FD;
     0;
}

# gen_sid - to generate unique Session id
sub gen_sid {
     my ($sid, $len) = ("", $_[0] ? $_[0]-1 : 23);
     srand(time());
     foreach(0...$len) {
          $sid .= $KEY_MAP[int rand(61)]; # total of $#KEY_MAP -1
      }
     $sid;
}

sub valid_sid {
     my ($user, $sid) = @_;
     open(FD, "< $cfg{'listdir'}/$list/queue/$user") or
         syslog("can't open $user, $!") and die "Can't open $user, $!\n";
     $_ = <FD>;
     chomp;
     ($_) = m/[^:]+:(.*)/;
     if($sid eq $_) {
          syslog("auth ok for $user");
          return 1;
      }
     close FD;
     return 0;
}

sub add_user {
     my ($user) = @_;
     unlink "$cfg{'listdir'}/$list/queue/$user"; # clean up user cookie/queue
     open(FD, ">> $cfg{'listdir'}/$list/users.txt") or
         die "Can't append to users.txt for $list, $!\n";
     print FD $user, "\n";
     close FD;
}

sub del_user {
     my ($user) = @_;
     my $buf = undef;
 
     open(FD, "< $cfg{'listdir'}/$list/users.txt") or
         die "Can't open users.txt for $list, $!\n";
     while(<FD>) {
          chomp;
          if(!/^$user$/) {
               $buf.="$_\n";
           }
      }
     close FD;
 
     open(FD, "> $cfg{'listdir'}/$list/users.txt") or
         die "Can't write to users.txt for $list, $!\n";
     print FD $buf;
     close FD;
}

sub syslog {
     my ($msg) = @_;
     chomp $msg;
     printf $SLOG "%s $msg\n", time;
}

Posted by hzqbbc at 03:13 PM | Comments (0)

June 03, 2005

Perl Socket 编程样例(2)

前两天介绍了使用Socket及IO::Socket 来进行TCP client/server的编程基本套路和代码,现在再介绍使用Socket及IO::Socket模块来进行Unix domain Socket的client/server开发。

Unix Domain Socket(简称unix socket)和TCP/UDP等INET类型socket相比起来有几个优点:

因此使用Unix socket来设计单机的IPC应用是首选。非常实用。大量的Unix应用软件都使用unix socket来进行程序间通信。

Unix Domain Socket客户端, Socket模块

简介:使用Unix domain socket的客户端。

#!/usr/bin/perl -w
use strict;
use Socket;
use IO::Handle;

my $path = $ARGV[0] || '/tmp/daytime.sock';

socket(my $sock, PF_UNIX, SOCK_STREAM, 0);
my $sun = sockaddr_un($path);
connect($sock, $sun) or die "Connect: $!\n";
$sock->autoflush(1);
my $buf = <$sock>;
my $bs = length($buf);
print "Received $bs bytes, content $buf\n";
close $sock;

Unix Domain Socket 服务端, Socket模块

简介:使用Unix domain socket实现的daytime服务器。

#!/usr/bin/perl -w
# tcp_socket_dt_srv.pl
use strict;
use Socket;
use IO::Handle;
use POSIX qw(WNOHANG);

my $path     = $ARGV[0] || '/tmp/daytime.sock';

$SIG{'CHLD'} = sub {
      while((my $pid = waitpid(-1, WNOHANG)) >0) {
            print "Reaped child $pid\n";
        }
};

socket(SOCK, PF_UNIX, SOCK_STREAM, 0)
    or die "socket() failed: $!";
setsockopt(SOCK,SOL_SOCKET,SO_REUSEADDR,1)
    or die "Can't set SO_REUSADDR: $!" ;

unlink $path if -r $path;

bind(SOCK,sockaddr_un($path))    or die "bind() failed: $!";
listen(SOCK,SOMAXCONN)           or die "listen() failed: $!";

warn "Starting server on path $path...\n";

while (1) {
      next unless my $sockname = accept(SESSION,SOCK);
      defined (my $pid=fork) or die "Can't fork: $!\n";
 
      if($pid==0) {
          SESSION->autoflush(1);
          print SESSION (my $s = localtime);
          close SESSION;
          exit 0;
       }else {
          print "Forking child $pid\n";
       }
}

close SOCK;

...to be continued

Posted by hzqbbc at 09:00 AM | Comments (1)

June 01, 2005

Perl Socket 编程样例(1)

Perl的networking 功能非常强大,基本上用c/c++能做的事perl都能做,而且做得更轻松方便,甚至可以只用10来行代码就完成了c/c++要几十上百甚至几百行才能完成得好的工作。

在networking方面,最基础的是BSD socket编程,但往往perl入门时在这个方面,最头疼的无疑是如何开始,如何Step by step。最好的药方就是Example,一段完整的可以运行(working)的代码,通过实践来感受远比看枯燥的manual来得深刻。

以下给出几段使用Socket及IO::Socket编写的Server/client,他们能实现最简单但是却最基本的任务,包括一个forking/accept的模型。可以直接复制这些代码,然后小加修改即可开发一些小型的tcp/udp应用了。

TCP 客户端, Socket 模块

简介:实现从服务器端读取一行信息然后返回


#!/usr/bin/perl -w
# tcp_socket_cli.pl
use strict;
use Socket;

my $addr = $ARGV[0] || '127.0.0.1';
my $port = $ARGV[1] || '3000';
my $dest = sockaddr_in($port, inet_aton($addr));
my $buf = undef;

socket(SOCK,PF_INET,SOCK_STREAM,6) or die "Can't create socket: $!";
connect(SOCK,$dest)                or die "Can't connect: $!";

my $bs = sysread(SOCK, $buf, 2048); # try to read 2048
print "Received $bs bytes, content $buf\n"; # actually get $bs bytes
close SOCK;

执行结果:

perl tcp_socket_cli.pl localhost 25
Received 41 bytes, content 220 ESMTP Postfix - ExtMail 0.12-hzqbbc

TCP 服务端 Socket模块, forking/accept模型

简介:一个多进程的TCP 服务器,sample中实现了daytime的功能

#!/usr/bin/perl -w
# tcp_socket_dt_srv.pl
use strict;
use Socket;
use IO::Handle;
use POSIX qw(WNOHANG);

my $port     = $ARGV[0] || '3000';
my $proto    = getprotobyname('tcp');

$SIG{'CHLD'} = sub {
     while((my $pid = waitpid(-1, WNOHANG)) >0) {
          print "Reaped child $pid\n";
      }
};

socket(SOCK, AF_INET, SOCK_STREAM, getprotobyname('tcp'))
    or die "socket() failed: $!";
setsockopt(SOCK,SOL_SOCKET,SO_REUSEADDR,1)
    or die "Can't set SO_REUSADDR: $!" ;

my $my_addr = sockaddr_in($port,INADDR_ANY);
bind(SOCK,$my_addr)    or die "bind() failed: $!";
listen(SOCK,SOMAXCONN) or die "listen() failed: $!";

warn "Starting server on port $port...\n";

while (1) {
     next unless my $remote_addr = accept(SESSION,SOCK);
     defined(my $pid=fork) or die "Can't fork: $!\n";
   
     if($pid==0) {
          my ($port,$hisaddr) = sockaddr_in($remote_addr);
          warn "Connection from [",inet_ntoa($hisaddr),",$port]\n";
          SESSION->autoflush(1);
          print SESSION (my $s = localtime);
          warn "Connection from [",inet_ntoa($hisaddr),",$port] finished\n";
          close SESSION;
          exit 0;
      }else {
          print "Forking child $pid\n";
      }
}

close SOCK;

利用上述tcp_socket_cli.pl访问该server的执行结果:

[hzqbbc@local misc]$ perl tcp_socket_dt_srv.pl 
Starting server on port 3000...
Connection from [127.0.0.1,32888]
Connection from [127.0.0.1,32888] finished
Reaped child 13927
Forking child 13927

TCP 客户端 ,IO::Sockiet模块

简介:同样为客户端,不过使用的是IO::Socket 面向对象模块

#!/usr/bin/perl -w
# tcp_iosocket_cli.pl
use strict;
use IO::Socket;

my $addr = $ARGV[0] || '127.0.0.1';
my $port = $ARGV[1] || '3000';
my $buf = undef;

my $sock = IO::Socket::INET->new(
        PeerAddr => $addr,
        PeerPort => $port,
        Proto    => 'tcp')
    or die "Can't connect: $!\n";
$buf = <$sock>;
my $bs = length($buf);
print "Received $bs bytes, content $buf\n"; # actually get $bs bytes
close $sock;

TCP 服务端, IO::Socket模块, forking/accept模型

简介:同样的一个daytime 服务器,使用IO::Socket重写。

#!/usr/bin/perl
# tcp_iosocket_dt_srv.pl
use strict;
use IO::Socket;
use POSIX qw(WNOHANG);

$SIG = sub {
     while((my $pid = waitpid(-1, WNOHANG)) >0) {
          print "Reaped child $pid\n";
      }
};

my $port     = $ARGV[0] || '3000';
my $sock = IO::Socket::INET->new( Listen    => 20,
                                  LocalPort => $port,
                                  Timeout   => 60*1,
                                  Reuse     => 1)
  or die "Can't create listening socket: $!\n";

warn "Starting server on port $port...\n";
while (1) {
     next unless my $session = $sock->accept;
     defined (my $pid = fork) or die "Can't fork: $!\n";
 
     if($pid == 0) {
          my $peer = gethostbyaddr($session->peeraddr,AF_INET) || $session->peerhost;
          my $port = $session->peerport;
          warn "Connection from [$peer,$port]\n";
          $session->autoflush(1);
          print $session (my $s = localtime), "\n";
          warn "Connection from [$peer,$port] finished\n";
          close $session;
          exit 0;
      }else {
          print "Forking child $pid\n";
      }
}
close $sock;

...to be continue...

Posted by hzqbbc at 09:03 AM | Comments (0)

May 27, 2005

perl 性能优化

Perl是强大的语言,是强大的工具,也是一道非常有味道的菜:-) 利用很多perl 的特性,可以实现一些非常有趣而实用的功能。

利用Perl开发一些服务应用时,有时会遇到性能或资源占用的问题,如何解决呢?以下是自己过去开发实践的一些经验,几个主要的技巧分别是:

巧用require装载模块

为避免程序一启动就加载大量模块,降低启动速度,可以在必要的时候再装载模块,这时候就是require大派用场的时候了。

如:


#!/usr/bin/perl -w
use pre_load_module;

# Initialize some thing
init_args();

# if $use_this_module is true, load the Module
if($use_this_module) {
     require Module;
}

上述代码中,如果变量$use_this_module设置了,那么才加载Module,如果没设置则不需要加载,实现了:use on demand的功能。在CGI应用程序中,这相当有用,如果每次请求(fork)都加载大量无用模块的话,响应速度会有所降低,而在特定场合才加载一些模块将加块启动、解析的速度。

再看一个例子:


#!/usr/bin/perl
my $pid = fork or die "can't fork:$!\n";
if($pid) {
     print "i'm father\n";
     sleep;
}else {
     print "i'm child\n":
     require IO::Socket;
     sleep;
}

上述代码中,如果在程序一开始就用use 来载入IO::Socket模块,那么子/父进程都加载了该模块,通过top命令发现子父进程大小都是3.07MB;如果只在子进程里加载,则只在子进程里有效,内存的消耗将降低,top命令发现子进程3.04MB,父进程变为1.4MB。

使用系统函数及XS化模块

Perl内建的系统函数及用c编写的perl XS扩展模块的速度和效率都比纯perl的实现要好得多。在性能要求较高的场合(如开发Application Server,Network Server等),可以考虑使用这些内建函数或XS化模块。

如Socket就比IO::Socket的内存消耗要低,XS编写的Data::Dumper就比纯Perl的Data::Dumper要快4-5倍。

此外,一些简单的任务并没必要使用Perl 模块,如获得主机IP地址就大可不必载入庞大的Net::DNS而只是使用gethostbyname()系统函数即可。

以下是一些常用的替代方案以获得更快的速度,更好的效率:
  • 用sys*系列函数等替代open/seek/tell/<>等标准IO操作
  • 用Socket代替IO::Socket以获得更低开销和内存占用
  • 用get*by*系列函数代替Net::DNS
  • 用index/substr等代替部分低效正则表达式
  • 用select(3参数版本)代替IO::Handle部分功能
  • .......

    自写低开销模块

    通常我们使用一些Perl模块时,只使用了其中很小一部分的功能,可是却不得不载入整个模块,甚至要载入其他不相关的模块。因此往往使整个程序非常臃肿庞大。

    著名的web管理软件webmin的miniserv(一个简化的http服务端)功能强大,还支持SSL,但资源占用却出奇的少,只有大约5.6MB的大小!这是为什么呢?因为miniserver只使用了2个Perl 系统模块(Socket及POSIX),没有载入其他的模块。一些本需要其他perl 模块的功能,均由web-lib.pl等用系统函数编写代替。

    例如以下是一个获得A记录的高速函数get_mx(),它不依赖任何模块,速度非常快。

    
    sub get_mx {
         my @info = gethostbyname shift;
         my @addr = splice(@info, 4);
         my @rt;
         foreach(@addr) {
              push @rt, join('.', unpack('C4', $_));
          }
         \@rt;
    }
    

    另一个例子,对于标准的IO::Handle对象,可以使用$obj->autoflush(1);来设置缓冲的特性,我们通过使用系统函数select()来获得同样的能力,而无需要载入IO::Handle,代码如下:

    
    sub autoflush {
         my $io = $_[0];
         select((select($io), $|=1)[0]);
    }
    

    使用方法很简单,例如要对IO::Socket::INET类型的$sock设置为立即冲刷,则autoflush($sock)即可。

    ...to be continue...

    Posted by hzqbbc at 05:30 PM | Comments (0)

    May 06, 2005

    如何捕捉CGI程序exception

    用perl写cgi程序的时候,如果出现了问题,大多数都必须查看Web server的日志才能知道程序哪里出了错误,页面一般只返回500服务器错误,不能立刻获得错误的原因。

    使用php的程序员就没有这个烦恼,因为php默认会将错误都打印到页面上。CGI程序里如何才能做到这一点呢?

    CGI::Carp这个模块支持这个功能,以下是例子:

    
    use CGI::Carp qw(fatalsToBrowser);
    die "Bad error here";
    

    详细请参阅CGI::Crap的在线手册

    那如果是自己写的简单CGI程序,不使用CGI.pm怎么办?以下是简单的方法......

    原理简述

    Perl 提供了%SIG 这个特殊的HASH,通过定义信号响应函数,可以捕捉die及一些warning的信息,并将这些信息打印到web页上。但为了尽可能早的加载这些代码,最好将信号捕捉代码放到BEGIN块中,这样就能保证程序一执行就先执行异常捕捉这段代码了。
    
    BEGIN {
         # fatal handler setting.
         $SIG{__DIE__} = $SIG{__WARN__} = \&some_func;
    }
    

    代码例子

    以下是一个简单的例程,定义了一个叫handler_fatal处理函数来处理意外错误信息。
    
    #!/usr/bin/perl -w
    use strict
    
    BEGIN {
         # fatal handler setting.
         $SIG{__DIE__} = $SIG{__WARN__} = \&handler_fatal;
    }
    # some perl code goes here
    ......
    
    sub handler_fatal {
         print "Content-type: text/html\n\n";
         print "@_";
    }
    

    上面这段perl程序中,如果调用一个名称为abc();的子例程,浏览器将看到如下的错误信息:

    Undefined subroutine &main::abc called at /home/hzqbbc/cgi-bin/fatal.cgi line 8.
    

    通过这个方法就可以很简便的进行程序调试了。Extmail中的CGI.pm就是使用类似的方法,可以捕捉die(), warn() 等函数产生的错误及系统的错误提示。

    Posted by hzqbbc at 09:00 AM | Comments (2)