七阶子博客: 杂文 | 游戏 | 戏剧 | 白蛇 | 文艺 | 编程 | 近期
请输入标题关键字或 yyyymmdd 格式的日期

    Perl 信仰之用 FastCGI 建设网站

    前言

    Perl 曾经风光一时,但现在似乎不太流行了。但后面涌现的许多脚本语言都或深受其启发与影响,如 PHP Python 之流。从某意义上说,Perl 就像脚本语言族内的 C 语言,虽然直接用它的人不多了,但仍然是个高效、有趣的语言,且影响深远。在大多 Linux 发行版中都是默认安装的,另外,有许多著名软件或服务都依赖一个所谓的 Perl 兼容正则表达式库(PCRE)。Perl 为文本处理而生,一直到现在也是处理文本的最佳工具语句,而 Perl 正则表达式俨然成为事实上的标准。

    我也接触与学习过许多脚本语言,但最喜爱的仍然是 Perl 语言。所以当我想做个人博客网站时,最终决定还是使用 Perl 吧,在网上找了个 FastCGI 基础模块就开始手动搭建起来了。

    安装

    虽然在 Perl 网络编程中,也有模块支持直接用纯 Perl 实现 Web 服务器。但是出于稳定性与扩展性考虑,还是安装 Nginx 服务器。而 Perl 通过 CGI 生成动态网页,就像现在更流行的 PHP 的做法那样。

    所以需要先在 nginx.conf 中加入类似下面的配置:

    location ~ \.pl|cgi$ {
      fastcgi_pass  127.0.0.1:8999;
      fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
      include fastcgi_params;
    } 
    

    与 PHP 的 CGI 配置非常类似,只不过匹配通讯端口的事。

    关键是用 Perl 实现的 CGI。这也不难,Perl 有标准模块 Socket,再用 CPan 安装个 FCGI 模块,就可以实现的。不过我直接从网上找了个fastcgi-wrapper.pl包装脚本,不过百行。还有一个非关键的perl-cgi启动脚本。扔在 /etc/init.d 目录下,就可以用 service 命令启动服务了。

    请求-响应模型

    这个 Perl CGI 模型很简洁。就是 Nginx 服务器先接收来自网络的 http 请求,如果匹配访问的路径(后缀名),就转而请求那个 fastcgi-wrapper.pl 脚本启动的常驻 daemon 服务;后者通过 fork 子进程,调用目标 perl 脚本;该脚本负责生成网页文本,最终通过 Nginx 服务器返还给最初的访问者。

    Nginx 会传给 fastcgi 许多参数,cgi 服务将这些参数保存在环境变量中,再传给真正工作的目标 perl 脚本。最重要的环境变量如访问路径与 GET 参数。而由于 cgi 服务重定向了标准输入输出,网络请求的 POST 内容就通过标准输入到目标 perl 脚本,而在脚本中简单的 print 向标准输出,就是相当于响应最初的 http 请求。

    需要注意的是,fastcgi-wrapper.pl 只做了尽可能少的工作,需要由目标脚本用户自己负责所有响应,按 http 协议标准在输出主体内容之前输出响应头,中间隔一空行。当然在 Nginx 出入口端,也会处理少量简单的响应头,如是否成功的状态码。

    示例

    还是用代码说事更直观。比如可用下面这个脚本反映用户由 cgi 接收到的环境变量:

    #!/usr/local/bin/perl
    print "Content-type:text/html\n\n";
    print <<EndOfHTML;
    <html><head><title>Perl Environment Variables</title></head>
    <body>
    <h1>Perl Environment Variables</h1>
    EndOfHTML
    foreach $key (sort(keys %ENV)) {
      print "$key = $ENV{$key}<br>\n";
    }
    print "</body></html>";
    

    第一个 print 语句输出的 Content-type:text/html 就是 http 响应头的一部分,再由一个空行分隔现在的 <html> 主体网页内容。由于还要经过 Nginx 的过滤,这个 perl 脚本不必输出所有响应头,像错误状态码之类 Nginx 自己会处理。同时也不必太纠结换行符格式(按 http 协议标准,换行符要用 \r\n

    如果服务配置正确,通过浏览器访问该脚本,就可获知 cgi 传递的环境变量,简单列几个重要的:

    可以用以下代码片断将 GET 参数字符串处理为 Perl 的散列表(hash):

    my $query = $ENV{QUERY_STRING};
    my %query = map {$1 => $2 if /(\w+)=(\S+)/} split(/&/, $query);
    

    不过这也没处理 URI 转义,所以 GET 可能存在的特殊字符如空格还会保持为 %20 的转义序列。如果有这需要,可用 URI::Escape 提供的 uri_unescape 函数解转义。

    至于通过 POST 提交的内容,可以用以下示例脚本观察一下,将上面那个脚本中间的foreach 部分换成这样的读取标准输入的代码片断:

    while (<>) {
    	chomp;
    	print "$. : $_<br>\n";
    }
    

    其含义是输出 POST 内容的每一行,前面附加行号 $. ,有点类似 cat -n 的作用。

    可以用 curl http://URI -d@postfile 来测试该脚本。然而注意的是 curl 的 -d参数会将 post 文件的换行符吞噬掉,将所有文件内容当成一个长行来发送给 Web 服务器(我们这里是 Nginx),于是上面这个脚本通过标准输入读到的内容也只有一行。改用--date-binary 可保持文件内容的换行符。

    更常见的应该是通过表单 post 内容,因此可以写个简单的 html 网页,包含一个测试表单,将 method 设为 post ,观察该脚本的响应。大约会是如下形式:

    1 : name1=value1&name2=value2&name3=value3...<br>;
    

    因为表单通过 POST 方法提交的内容与通过 GET 的内容格式是一样的,也是由 & 分隔的一键值对,只不过 POST 支持更长的行,且在地址栏中隐藏。但以服务器脚本看来,也只是一个长行而已。

    在 Perl 中,可通过重设行分隔符来获取 POST 内容的每个键值对:

    $/ = '&';
    while (<>) {
    	chomp;
    	print "$. : $_<br>\n";
    }
    

    当然最好是将修改 $/ 的相关行为放在单独的作用域中,并用 local 修饰。就这个需求而言,也可以将所有内容一次读入后,按上面分隔 GET 参数的方法处理。在 Perl 中,一次读入文件所有内容的标准做法如下:

    $/ = undef;
    my $all = <>;
    

    不过在处理表单 POST 内容时,我们已经知道只有一长行,就可以不重设 $/ ,直接用$post = <>; 在标量语境中读入一行。如果写成 @post = <>; 则是在列表语境中读入所有行,存入一个数组中。

    实践

    当弄清楚了输入输出的基础问题后,就可以着手处理业务逻辑了。如果是做复杂的网站,可能还需要一个业务框架与(或)网页模板。同时 Perl 也可以采用面向对象的方式来组织逻辑代码。

    但是,我在做个人博客网站时,想了想,没必要那么复杂的功能,就用基础的 perl 语言功能开撸了。我这基本是“伪”静态网页呈现文章,应该以内容为主,代码量与文章相比应该是很小的比例才对。简洁的同时,也更高效、安全。不过另外用了一个 Markdown 模块,因为我用 markdown 语法写文章,主要用到这么个模块将本地文章转为 html 格式输出而已。

    说起 markdown 标记语言来,最初提出这个规则概念时,好像也正是 perl 实现的。后来广泛采用与扩展后,才有 C 之类的编译语言重新更高效地实现。这就是脚本语言快速原型的重大意义(现在流行的 python 也是推这个概念)。还有个 ack ,最初也是个 perl 程序而已,就是个更好用的 grep 搜索工具,后来就有了用 C 重写的 ag ,用法与 ack 基本一样。

    结语

    作为职业工作的语言选择,固然取决于公司团队,以及那个(些)能够给工资的老板。但是作为自己的玩具,不妨遵从本心的喜好,爱啥用啥吧,何必随波逐流呢。