第 B-1 章 CGI 动态请求
B1、CGI 是什么
CGI 是 common gateway interface 的缩写,大家都译作通用网关接口。
我们知道,web 服务器所处理的内容都是静态的,要想处理动态内容,需要依赖于 web 应用程序,如 php、jsp、python、perl 等。
但是 web server 如何将动态的请求传递给这些应用程序?它所依赖的就是 cgi 协议。没错,是协议,也就是 web server 和 web 应用程序交流时的规范。
换句话说,通过 cgi 协议,再结合已搭建好的 web 应用程序,就可以让 web server 也能 "处理" 动态请求(或者说,当用户访问某个特定资源时,可以触发执行某个 web 应用程序来实现特定功能),你肯定知道处理两字为什么要加上双引号。
简单版的 cgi 工作方式如下:
例如,在谷歌搜索栏中搜索一个关键词 "http",对应的 URL 为:
https://www.google.com/search?q=http&oq=http&aqs=chrome..69i57j69i60l4j0.1136j0j8&sourceid=chrome&ie=UTF-8
- 当谷歌的 web server 收到该请求后,先分析该 url,从中知道了要执行 search 程序,并且还知道了一系列要传递给 search 的参数及其对应的 value。
- web server 会将这些程序参数和其它一些环境变量根据 cgi 协议通过 TCP 或套接字等方式传递给已启动的 cgi 程序(可能是 cgi 进程,或者是已加载的模块 cgi 模块)。
- 当 cgi 进程接收到 web server 的请求后,调用 search 程序并执行,同时还会传递参数给 search 程序。
- search 执行结束后,cgi 进程/线程将处理结果返回给 web server,web server 再返回给浏览器。
有多种方式可以执行 cgi 程序,但对 http 的请求方法来说,只有 get 和 post 两种方法允许执行 cgi 脚本(即上面的 search 程序)。实际上 post 方法的内部本质还是 get 方法,只不过在发送 http 请求时,get 和 post 方法对 url 中的参数处理方式不一样而已。
任何一种语言都能编写 CGI,只不过有些语言比较擅长,有些语言则非常繁琐,例如用 bash shell 开发,那么需要用 echo 等打印语句将执行结果放在巨多无比的 html 的标签中输出给客户端。常用于编写 CGI 的语言有 perl、php、python 等,java 也一样能写,但 java 的 servlet 完全能实现 CGI 的功能,且更优化、更利于开发。
B2、专业术语
以 php 为例,我将一次动态请求相关的概念大致都简单解释一遍。
cgi
:它是一种协议。通过 cgi 协议,web server 可以将动态请求和相关参数发送给专门处理动态内容的应用程序。fastcgi
:也是一种协议,只不过是 cgi 的优化版。php-cgi
:fastcgi 是一种协议,而 php-cgi 实现了这种协议。不过这种实现是单进程的,一个进程处理一个请求,处理结束后进程就销毁。php-fmp
:是对 php-cgi 的改进版,它直接管理多个 php-cgi 进程/线程。也就是说,php-fpm 是 php-cgi 的进程管理器,因此它也算是 fastcgi 协议的实现。在一定程度上讲,php-fpm 与 php 的关系,和 tomcat 对 java 的关系是类似的。cgi 进程/线程
:在 php 上,就是 php-cgi 进程/线程。专门用于接收 web server 的动态请求,调用并初始化 zend 虚拟机。cgi 脚本
:被执行的 php 源代码文件。zend 虚拟机
:对 php 文件做词法分析、语法分析、编译成 opcode,并执行。最后关闭 zend 虚拟机。cgi 进程/线程和 zend 虚拟机的关系
:cgi 进程调用并初始化 zend 虚拟机的各种环境。
以 php-fpm 为例,web server 从转发动态请求到结束的过程大致如下:
- Web Browser 用户把请求交给 Apache 服务器。
- Apache 通过一个叫作 FastCGI 的高速二进制接口把 code 送给 PHP-FPM 主进程。
- PHP-FPM 主进程从线程池中选择一个 worker 线程,然后把 code 交给该线程。
- worker 线程执行 code,然后把执行结果返回给 Apache,由 Apache 转交给 Web Browser 用户。
- worker 线程一旦完成工作,就返回到线程池中等待下一个请求。
B3、web server 和 CGI 的交互模式
web server 对 cgi 进程/线程来说,它的作用就是发起动态处理请求,传递一些参数和环境变量,最后接收 cgi 的返回结果。通过下面 httpd.conf 中的转发配置应该很容易理解(httpd 和 php-fpm 的交互):
ProxyRequests off
ProxyPassMatch ^/(.*\.php)$ fcgi://127.0.0.1:9000/usr/local/apache/htdocs/$1
以最典型的 apache httpd 和 php 为例,对于 httpd 来说,web server 和 php-cgi 有 3 种交互模式:
cgi 模式
:httpd 接收到一个动态请求就 fork 一个 cgi 进程,cgi 进程返回结果给 httpd 进程后自我销毁。动态模块模式
:将 php-cgi 的模块(例如 php5_module)编译进 httpd。在 httpd 启动时会加载模块,加载时也将对应的模块激活,php-cgi 也就启动了。- 注意:很多人以为动态编译的模块是可以在需要的时候随时加载调用,不需要的时候它们就停止了,实际上不是这样的。和静态编译的模块一样,动态加载的模块在被加载时就被加入到激活链表中,无论是否使用它,它都已经运行在 apache httpd 的内部。
php-fpm 模式
:使用 php-fpm 管理 php-cgi,此时 httpd 不再控制 php-cgi 进程的启动。可以将 php-fpm 独立运行在非 web 服务器上,实现所谓的动静分离。
实际上,借助模块 mod_fastcgi 还可以实现 fastcgi 模式。同 cgi 一样,管理模式的先天缺陷决定了这并不是一种好方法。
B3.1、CGI 模式
使用 CGI 模式时,当动态请求到达,httpd 临时启动一个 cgi 解释器,并通过 cgi 协议转发要运行的内容。当 cgi 脚本运行结束后,将结果返回给 httpd,然后 cgi 解释器进程自我销毁。当多个动态请求到达时,将先后启动多个 cgi 解释器。因此,这种方法效率极低。
在注释掉 php5_module 的 LoadModule 相关行后,使用 action 指令指定要使用 cgi 运行的类型。但注意,action 指令是 mod_action 提供的,所以必须已经加载该模块。
例如:指定 MIME 类型为 image/gif 的请求使用 images.cgi 运行。显然,images.cgi 脚本你必须先写好。
Action image/gif /cgi-bin/images.cgi
还可以通过添加 handler 来复合文件类型,再使用某个 cgi 脚本去运行这个 handler 中的任意类型。
AddHandler my-file-type .xyz
Action my-file-type "/cgi-bin/program.cgi"
对于 php 来说,则可以使用安装 php 时 bin 目录下提供的 php-cgi 程序作为 cgi 程序。
[root@arm64v8 ~]# ls /usr/local/php/bin/
pear peardev pecl phar phar.phar php php-cgi php-config phpize
[root@arm64v8 ~]#
# 复制到apache默认的cgi-bin目录下,方便管理
[root@arm64v8 ~]# cp /usr/local/php/bin/php-cgi /usr/local/apache/cgi-bin/
# 在httpd.conf中添加以下行
Action application/x-httpd-php /usr/local/php/bin/cgi-bin/php-cgi
B3.2、模块方式
在编译 php 时,将 php5_module 模块编译到 apache 中,例如在编译 php 时在 ./configure 配置中加上 "--with-apxs2=/usr/local/apache/bin/apxs"。
这种交互模式下,httpd 在启动时加载并激活 php_module。也就是说,php-cgi 常驻在 httpd 进程内部。当动态请求到达时,httpd 不用再生成 cgi 解释器,而是直接将动态请求转发给它内部 php-cgi。
配置实用这种交互模式非常简单,只需使用 LoadModule 加载 php_module,再添加对应的 MIME 处理器即可。
LoadModule php5_module modules/libphp5.so
# 在mime模块中添加对应的类型
<IfModule mime_module>
AddType application/x-httpd-php .php
AddType applicaiton/x-httpd-php-source .phps
</IfModule>
B3.3、php-fpm 方式
前面说了,php-fpm 是 php-cgi 的进程管理器。这种交互方式实际上是让 php-cgi 以独立于 httpd 的方式存在,目前基本使用 php-fpm 的方式管理 php-cgi 进程。
也就是说,这种模式下,php-cgi 和 httpd 已经分离了,它们的分离意味着请求的动静分离变为可能:httpd 和 php-fpm 分别运行在不同服务器上。动静分离后,压力也分散到各自的服务器上。
要让 php-fpm 以这种方式运行,需要在编译的 ./configure 配置选项中添加 "--enable-fpm" 选项。当然,还得启动 php-fpm 服务。例如:
service php-fpm start
这样 php-cgi 进程就开放着端口(默认9000)等待 httpd 转发动态请求。要让 httpd 能够转发请求到 php-cgi 上,需要在 httpd.conf 中关闭正向代理,并设置 fastcgi 协议代理参数。例如,转发到 192.168.0.88 主机上的 php-fpm。
# 加载代理模块
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
# 添加MIME类型
AddType application/x-httpd-php .php
AddType application/x-httpd-php-source .phps
# 在需要转发的虚拟主机中配置转发代理
ProxyRequests off
ProxyPassMatch ^/(.*\.php)$ fcgi://192.168.0.88:9000/usr/local/apache/htdocs/$1