
上次写了个 Perl CGI 的本地部署测试,这次把实现原理补上。主要看 Apache httpd 怎么把一个 HTTP 请求交给 Perl 脚本,再把脚本输出原样返回给浏览器。
这东西现在不常见了,但原理特别干净。CGI 全称 Common Gateway Interface,说白了是一套约定:Web 服务器收到请求后,把请求信息塞给外部程序,外部程序自己输出响应内容,服务器再把这个输出原样发回浏览器。
Apache 这边就是 fork 一个子进程去跑这个程序,环境变量、请求头、请求体都传进去,子进程的标准输出就是 HTTP 响应。
Perl 只是写 CGI 脚本的一种选择。你用 Python、Shell、C 都一样,只要这个程序能读标准输入、写标准输出,就能当 CGI 程序。
画了个简单的流程图,一看就明白:
Apache 通过 ScriptAlias 或 Alias + ExecCGI 认出 CGI 目录。子进程启动后,请求方法、路径、查询参数、请求头这些都被 Apache 塞到环境变量里。CGI 脚本必须自己输出 Content-type 响应头,然后空一行,再输出 body。少了空行就是 500。
CGI 规范定义了一大堆环境变量,脚本里通过 %ENV 就能读到。插几个常见的:
REQUEST_METHOD:GET 还是 POSTQUERY_STRING:URL 问号后面的参数CONTENT_TYPE:POST 请求的 body 类型CONTENT_LENGTH:POST 请求的 body 长度SCRIPT_NAME:CGI 脚本的路径PATH_INFO:脚本名后面的附加路径REMOTE_ADDR:客户端 IPHTTP_USER_AGENT:客户端浏览器标识#!/usr/bin/perl
use strict;
use warnings;
print "Content-type: text/html\n\n";
print "<html><body>";
print "<h1>Hello from Perl CGI</h1>";
print "<p>REQUEST_METHOD: $ENV{REQUEST_METHOD}</p>";
print "<p>QUERY_STRING: $ENV{QUERY_STRING}</p>";
print "</body></html>";
最关键的是这个头:
Content-type: text/html
最后这个空行不能少,它是响应头和 body 的分隔符。少了就是 500。
假设 CGI 脚本放在 /var/www/html/cgi-bin/ 目录下,Apache 配置可以这么写:
<VirtualHost *:80>
DocumentRoot /var/www/html
ScriptAlias /cgi-bin/ /var/www/html/cgi-bin/
<Directory "/var/www/html/cgi-bin/">
Options +ExecCGI
AddHandler cgi-script .cgi .pl
Require all granted
</Directory>
</VirtualHost>
ScriptAlias 同时完成了两个事:把 URL 路径映射到本地目录,并且默认这个目录下的文件都是 CGI 脚本。如果你用 Alias 而不是 ScriptAlias,就需要手动加 ExecCGI 和 AddHandler。
以 GET /cgi-bin/index.cgi?a=1&b=2 为例,走一遍完整流程:
GET /cgi-bin/index.cgi?a=1&b=2 HTTP/1.1/var/www/html/cgi-bin/index.cgiindex.cgiindex.cgi 通过 STDIN 读 body(POST 时)、通过 ENV 读请求信息、通过 STDOUT 写响应HTTP/1.1 200 OK,返回给浏览器每个 CGI 请求 Apache 都要 fork 一个进程,进程启动和销毁都有开销。请求量小无所谓,大了以后进程频繁创建销毁,CPU 和内存都扛不住。
后来就有了 FastCGI、mod_perl 这些方案。FastCGI 让 CGI 进程常驻内存,Apache 通过 socket 跟它通信,不用每次都 fork。mod_perl 则是把 Perl 解释器嵌到 Apache 进程里,直接调用 Perl 代码,省掉进程间通信。
但是学习的话,还是从最原始的 Apache + CGI 开始最清楚。原理弄懂了,再去看 FastCGI、WSGI、mod_perl 都不难。
用 Docker 跑起来最方便,不用折腾本地 Apache 环境。
目录结构:
D:.
├─ cgi-bin
│ └─ index.cgi
├─ Dockerfile
└─ httpd.conf
index.cgi:
#!/usr/bin/perl
use strict;
use warnings;
print "Content-type: text/html\n\n";
print "<h1>Hello from Perl CGI</h1>";
print "<p>Method: $ENV{REQUEST_METHOD}</p>";
print "<p>Query: $ENV{QUERY_STRING}</p>";
Dockerfile:
FROM perl:latest
RUN apt-get update && apt-get install -y apache2 libapache2-mod-perl2
COPY . /var/www/html/
RUN chmod +x /var/www/html/cgi-bin/*.cgi
COPY httpd.conf /etc/apache2/sites-available/000-default.conf
RUN a2enmod cgi
EXPOSE 80
CMD ["apachectl", "-D", "FOREGROUND"]
httpd.conf:
<VirtualHost *:80>
DocumentRoot /var/www/html
ScriptAlias /cgi-bin/ /var/www/html/cgi-bin/
<Directory "/var/www/html/cgi-bin/">
Options +ExecCGI
AddHandler cgi-script .cgi .pl
Require all granted
</Directory>
</VirtualHost>
启动:
docker build -t my-perl-cgi-app .
docker run -p 8080:80 my-perl-cgi-app
访问 http://localhost:8080/cgi-bin/index.cgi?a=1,就能看到 Perl 脚本返回的响应。
Apache + CGI + Perl 这套组合现在看着老,但原理很干净。核心就是 Apache 把请求信息交给外部进程,外部进程自己决定输出什么。先把这套弄明白,后面再看 FastCGI、WSGI、mod_perl 都会顺很多。