Apache httpd + CGI + Perl 是怎么跑起来的

Updated on with 0 views and 0 comments

Apache httpd + CGI + Perl 是怎么跑起来的

上次写了个 Perl CGI 的本地部署测试,这次把实现原理补上。主要看 Apache httpd 怎么把一个 HTTP 请求交给 Perl 脚本,再把脚本输出原样返回给浏览器。

这东西现在不常见了,但原理特别干净。CGI 全称 Common Gateway Interface,说白了是一套约定:Web 服务器收到请求后,把请求信息塞给外部程序,外部程序自己输出响应内容,服务器再把这个输出原样发回浏览器。

Apache 这边就是 fork 一个子进程去跑这个程序,环境变量、请求头、请求体都传进去,子进程的标准输出就是 HTTP 响应。

Perl 只是写 CGI 脚本的一种选择。你用 Python、Shell、C 都一样,只要这个程序能读标准输入、写标准输出,就能当 CGI 程序。

Apache 处理 CGI 请求的流程

画了个简单的流程图,一看就明白:

浏览器 GET /cgi-bin/index.cgi Apache httpd 匹配 /cgi-bin/, fork 子进程 Perl CGI 脚本 读 ENV / STDIN 标准输出 Apache 读取 stdout Apache httpd 包装 HTTP 响应 浏览器 200 OK + HTML

Apache 通过 ScriptAliasAlias + ExecCGI 认出 CGI 目录。子进程启动后,请求方法、路径、查询参数、请求头这些都被 Apache 塞到环境变量里。CGI 脚本必须自己输出 Content-type 响应头,然后空一行,再输出 body。少了空行就是 500。

环境变量

CGI 规范定义了一大堆环境变量,脚本里通过 %ENV 就能读到。插几个常见的:

  • REQUEST_METHOD:GET 还是 POST
  • QUERY_STRING:URL 问号后面的参数
  • CONTENT_TYPE:POST 请求的 body 类型
  • CONTENT_LENGTH:POST 请求的 body 长度
  • SCRIPT_NAME:CGI 脚本的路径
  • PATH_INFO:脚本名后面的附加路径
  • REMOTE_ADDR:客户端 IP
  • HTTP_USER_AGENT:客户端浏览器标识

最小可运行的 Perl CGI 脚本

#!/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。

Apache 配置

假设 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,就需要手动加 ExecCGIAddHandler

请求生命周期再梳理

GET /cgi-bin/index.cgi?a=1&b=2 为例,走一遍完整流程:

  1. 浏览器组装请求行 GET /cgi-bin/index.cgi?a=1&b=2 HTTP/1.1
  2. Apache 收到请求,解析出方法、路径、查询字符串
  3. Apache 根据配置找到 /var/www/html/cgi-bin/index.cgi
  4. Apache 检查文件是否存在、是否有执行权限
  5. 权限不够就返回 403
  6. Apache fork 子进程,设置环境变量,执行 index.cgi
  7. index.cgi 通过 STDIN 读 body(POST 时)、通过 ENV 读请求信息、通过 STDOUT 写响应
  8. Apache 等子进程结束,把 stdout 作为响应 body
  9. Apache 在响应前面加上 HTTP/1.1 200 OK,返回给浏览器

CGI 的瓶颈

每个 CGI 请求 Apache 都要 fork 一个进程,进程启动和销毁都有开销。请求量小无所谓,大了以后进程频繁创建销毁,CPU 和内存都扛不住。

后来就有了 FastCGI、mod_perl 这些方案。FastCGI 让 CGI 进程常驻内存,Apache 通过 socket 跟它通信,不用每次都 fork。mod_perl 则是把 Perl 解释器嵌到 Apache 进程里,直接调用 Perl 代码,省掉进程间通信。

但是学习的话,还是从最原始的 Apache + CGI 开始最清楚。原理弄懂了,再去看 FastCGI、WSGI、mod_perl 都不难。

一个完整的 Docker 示例

用 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 都会顺很多。

------------------------- 走在路上的symoon