CGI 简介
CGI全称是“通用网关接口”(Common Gateway Interface),它可以让一个客户端,从网页浏览器向实行在Web做事器上的程序要求数据。 CGI描述了客户端和这个程序之间传输数据的一种标准。 CGI的一个目的是要独立于任何措辞的,以是CGI可以用任何一种措辞编写,只要这种措辞具有标准输入、输出和环境变量。 如php,perl,tcl等。
传统 CGI 事情事理剖析
客户端访问某个 URL 地址之后,通过 GET/POST/PUT 等办法提交数据,并通过 HTTP 协议向 Web 做事器发出要求,做事器真个 HTTP Daemon(守护进程)将 HTTP 要求里描述的信息通过标准输入 stdin 和环境变量(environment variable)通报给主页指定的 CGI 程序,并启动此运用程序进行处理(包括对数据库的处理),处理结果通过标准输出 stdout 返回给 HTTP Daemon 守护进程,再由 HTTP Daemon 进程通过 HTTP 协议返回给客户端。
上面的这段话理解可能还是比较抽象,下面我们就通过一次GET要求为例进行详细解释。
下面用代码来实现图中表述的功能。Web 做事器启动一个 socket 监听做事,然后在本地实行 CGI 程序。后面有比较详细的代码解读。
Web 做事器代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define SERV_PORT 9003
char
str_join(
char
str1,
char
str2);
char
html_response(
char
res,
char
buf);
int
main(
void
)
{
int
lfd, cfd;
struct
sockaddr_in serv_addr,clin_addr;
socklen_t clin_len;
char
buf[1024],web_result[1024];
int
len;
FILE
cin;
if
((lfd = socket(AF_INET,SOCK_STREAM,0)) == -1){
perror
(
\公众create socket failed\"大众
);
exit
(1);
}
memset
(&serv_addr, 0,
sizeof
(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);
if
(bind(lfd, (
struct
sockaddr )&serv_addr,
sizeof
(serv_addr)) == -1)
{
perror
(
\"大众bind error\"大众
);
exit
(1);
}
if
(listen(lfd, 128) == -1)
{
perror
(
\"大众listen error\"大众
);
exit
(1);
}
signal
(SIGCLD,SIG_IGN);
while
(1)
{
clin_len =
sizeof
(clin_addr);
if
((cfd = accept(lfd, (
struct
sockaddr )&clin_addr, &clin_len)) == -1)
{
perror
(
\"大众吸收缺点\n\"大众
);
continue
;
}
cin = fdopen(cfd,
\"大众r\"大众
);
setbuf
(cin, (
char
)0);
fgets
(buf,1024,cin);
//读取第一行
printf
(
\"大众\n%s\"大众
, buf);
//============================ cgi 环境变量设置演示 ============================
// 例如 \"大众GET /user.cgi?id=1 HTTP/1.1\"大众;
char
delim =
\"大众 \"大众
;
char
p;
char
method, filename, query_string;
char
query_string_pre =
\"大众QUERY_STRING=\"大众
;
method =
strtok
(buf,delim);
// GET
p =
strtok
(NULL,delim);
// /user.cgi?id=1
filename =
strtok
(p,
\"大众?\"大众
);
// /user.cgi
if
(
strcmp
(filename,
\公众/favicon.ico\"大众
) == 0)
{
continue
;
}
query_string =
strtok
(NULL,
\公众?\公众
);
// id=1
putenv(str_join(query_string_pre,query_string));
//============================ cgi 环境变量设置演示 ============================
int
pid = fork();
if
(pid > 0)
{
close(cfd);
}
else
if
(pid == 0)
{
close(lfd);
FILE
stream = popen(str_join(
\公众.\公众
,filename),
\"大众r\"大众
);
fread
(buf,
sizeof
(
char
),
sizeof
(buf),stream);
html_response(web_result,buf);
write(cfd,web_result,
sizeof
(web_result));
pclose(stream);
close(cfd);
exit
(0);
}
else
{
perror
(
\"大众fork error\"大众
);
exit
(1);
}
}
close(lfd);
return
0;
}
char
str_join(
char
str1,
char
str2)
{
char
result =
malloc
(
strlen
(str1)+
strlen
(str2)+1);
if
(result == NULL)
exit
(1);
strcpy
(result, str1);
strcat
(result, str2);
return
result;
}
char
html_response(
char
res,
char
buf)
{
char
html_response_template =
\"大众HTTP/1.1 200 OK\r\nContent-Type:text/html\r\nContent-Length: %d\r\nServer: mengkang\r\n\r\n%s\"大众
;
sprintf
(res,html_response_template,
strlen
(buf),buf);
return
res;
}
如上代码中的重点:
66~81行找到CGI程序的相对路径(我们为了大略,直接将其根目录定义为Web程序确当前目录),这样就可以在子进程中实行 CGI 程序了;同时设置环境变量,方便CGI程序运行时读取;
94~95行将 CGI 程序的标准输出结果写入 Web 做事器守护进程的缓存中;
97行则将包装后的 html 结果写入客户端 socket 描述符,返回给连接Web做事器的客户端。
CGI 程序(user.c)
#include <stdio.h>
#include <stdlib.h>
// 通过获取的 id 查询用户的信息
int
main(
void
){
//============================ 仿照数据库 ============================
typedef
struct
{
int
id;
char
username;
int
age;
} user;
user users[] = {
{},
{
1,
\公众mengkang.zhou\"大众
,
18
}
};
//============================ 仿照数据库 ============================
char
query_string;
int
id;
query_string =
getenv
(
\"大众QUERY_STRING\公众
);
if
(query_string == NULL)
{
printf
(
\"大众没有输入数据\"大众
);
}
else
if
(
sscanf
(query_string,
\"大众id=%d\"大众
,&id) != 1)
{
printf
(
\"大众没有输入id\"大众
);
}
else
{
printf
(
\"大众用户信息查询<br>学号: %d<br>姓名: %s<br>年事: %d\"大众
,id,users[id].username,users[id].age);
}
return
0;
}
将上面的 CGI 程序编译成gcc user.c -o user.cgi
,放在上面web程序的同级目录。
代码中的第28行,从环境变量中读取前面在Web做事器守护进程中设置的环境变量,是我们演示的重点。
FastCGI 事情事理剖析
相对付 CGI/1.1 规范在 Web 做事器在本地 fork 一个子进程实行 CGI 程序,添补 CGI 预定义的环境变量,放入系统环境变量,把 HTTP body 体的 content 通过标准输入传入子进程,处理完毕之后通过标准输出返回给 Web 做事器。FastCGI 的核心则是撤消传统的 fork-and-execute 办法,减少每次启动的巨大开销(后面以 PHP 为例解释),以常驻的办法来处理要求。
FastCGI 事情流程如下:
1. FastCGI 进程管理器自身初始化,启动多个 CGI 阐明器进程,并等待来自 Web Server 的连接。
2. Web 做事器与 FastCGI 进程管理器进行 Socket 通信,通过 FastCGI 协议发送 CGI 环境变量和标准输入数据给 CGI 阐明器进程。
3. CGI 阐明器进程完成处理后将标准输出和缺点信息从同一连接返回 Web Server。
4. CGI 阐明器进程接着等待并处理来自 Web Server 的下一个连接。
FastCGI 与传统 CGI 模式的差异之一则是 Web 做事器不是直接实行 CGI 程序了,而是通过 socket 与 FastCGI 相应器(FastCGI 进程管理器)进行交互,Web 做事器须要将 CGI 接口数据封装在遵照 FastCGI 协议包中发送给 FastCGI 相应器程序。正是由于 FastCGI 进程管理器是基于 socket 通信的,以是也是分布式的,Web做事器和CGI相应器做事器分开支配。
再啰嗦一句,FastCGI 是一种协议,它是建立在CGI/1.1根本之上的,把CGI/1.1里面的要通报的数据通过FastCGI协议定义的顺序、格式进行通报。
准备事情
可能上面的内容理解起来还是很抽象,这是由于第一对FastCGI协议还没有一个大概的认识,第二没有实际代码的学习。以是须要预先学习下 FastCGI 协议的内容,不一定须要完备看懂,可大致理解之后,看完本篇再结合着学习理解消化。
http://www.fastcgi.com/devkit/doc/fcgi-spec.html (英文原版)
http://andylin02.iteye.com/blog/648412 (中文版)
FastCGI 协议剖析
下面结合 PHP 的 FastCGI 的代码进行剖析,不作分外解释以下代码均来自于 PHP 源码。
FastCGI 类型
FastCGI 将传输的做了很多类型的划分,其构造体定义如下:
typedef
enum
_fcgi_request_type {
FCGI_BEGIN_REQUEST = 1,
/ [in] /
FCGI_ABORT_REQUEST = 2,
/ [in] (not supported) /
FCGI_END_REQUEST = 3,
/ [out] /
FCGI_PARAMS = 4,
/ [in] environment variables /
FCGI_STDIN = 5,
/ [in] post data /
FCGI_STDOUT = 6,
/ [out] response /
FCGI_STDERR = 7,
/ [out] errors /
FCGI_DATA = 8,
/ [in] filter data (not supported) /
FCGI_GET_VALUES = 9,
/ [in] /
FCGI_GET_VALUES_RESULT = 10
/ [out] /
} fcgi_request_type;
的发送顺序
下图是一个大略的通报流程
最先发送的是FCGI_BEGIN_REQUEST
,然后是FCGI_PARAMS
和FCGI_STDIN
,由于每个头(下面将详细解释)里面能够承载的最大长度是65535,以是这两种类型的不一定只发送一次,有可能连续发送多次。
FastCGI 相应体处理完毕之后,将发送FCGI_STDOUT
、FCGI_STDERR
,同理也可能多次连续发送。末了以FCGI_END_REQUEST
表示要求的结束。
须要把稳的一点,FCGI_BEGIN_REQUEST
和FCGI_END_REQUEST
分别标识着要求的开始和结束,与全体协议息息相关,以是他们的体的内容也是协议的一部分,因此也会有相应的构造体与之对应(后面会详细解释)。而环境变量、标准输入、标准输出、缺点输出,这些都是业务干系,与协议无关,以是他们的体的内容则无构造体对应。
由于全体是二进制连续通报的,以是必须定义一个统一的构造的头,这样以便读取每个的体,方便的切割。这在网络通讯中是非常常见的一种手段。
FastCGI 头
如上,FastCGI 分10种类型,有的是输入有的是输出。而所有的都以一个头开始。其构造体定义如下:
typedef
struct
_fcgi_header {
unsigned
char
version;
unsigned
char
type;
unsigned
char
requestIdB1;
unsigned
char
requestIdB0;
unsigned
char
contentLengthB1;
unsigned
char
contentLengthB0;
unsigned
char
paddingLength;
unsigned
char
reserved;
} fcgi_header;
字段阐明下:
version
标识FastCGI协议版本。
type
标识FastCGI记录类型,也便是记录实行的一样平常职能。
requestId
标识记录所属的FastCGI要求。
contentLength
记录的contentData组件的字节数。
关于上面的xxB1
和xxB0
的协议解释:当两个相邻的构造组件除了后缀“B1”和“B0”之外命名相同时,它表示这两个组件可视为估值为B1<<8 + B0的单个数字。该单个数字的名字是这些组件减去后缀的名字。这个约定归纳了一个由超过两个字节表示的数字的处理办法。
比如协议头中requestId
和contentLength
表示的最大值便是65535
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int
main()
{
unsigned
char
requestIdB1 = UCHAR_MAX;
unsigned
char
requestIdB0 = UCHAR_MAX;
printf
(
\公众%d\n\"大众
, (requestIdB1 << 8) + requestIdB0);
// 65535
}
你可能会想到如果一个别长度超过65535怎么办,则分割为多个相同类型的发送即可。
FCGI_BEGIN_REQUEST 的定义
typedef
struct
_fcgi_begin_request {
unsigned
char
roleB1;
unsigned
char
roleB0;
unsigned
char
flags;
unsigned
char
reserved[5];
} fcgi_begin_request;
字段阐明
role
表示Web做事器期望运用扮演的角色。分为三个角色(而我们这里谈论的情形一样平常都是相应器角色)
typedef
enum
_fcgi_role {
FCGI_RESPONDER = 1,
FCGI_AUTHORIZER = 2,
FCGI_FILTER = 3
} fcgi_role;
而FCGI_BEGIN_REQUEST
中的flags
组件包含一个掌握线路关闭的位:flags & FCGI_KEEP_CONN
:如果为0,则运用在对本次要求相应后关闭线路。如果非0,运用在对本次要求相应后不会关闭线路;Web做事器为线路保持相应性。
FCGI_END_REQUEST 的定义
typedef
struct
_fcgi_end_request {
unsigned
char
appStatusB3;
unsigned
char
appStatusB2;
unsigned
char
appStatusB1;
unsigned
char
appStatusB0;
unsigned
char
protocolStatus;
unsigned
char
reserved[3];
} fcgi_end_request;
字段阐明
appStatus
组件是运用级别的状态码。
protocolStatus
组件是协议级别的状态码;protocolStatus
的值可能是:
FCGI_REQUEST_COMPLETE:要求的正常结束。
FCGI_CANT_MPX_CONN:谢绝新要求。这发生在Web做事器通过一条线路向运用发送并发的要求时,后者被设计为每条线路每次处理一个要求。
FCGI_OVERLOADED:谢绝新要求。这发生在运用用完某些资源时,例如数据库连接。
FCGI_UNKNOWN_ROLE:谢绝新要求。这发生在Web做事器指定了一个运用不能识别的角色时。
protocolStatus
在 PHP 中的定义如下
typedef
enum
_fcgi_protocol_status {
FCGI_REQUEST_COMPLETE = 0,
FCGI_CANT_MPX_CONN = 1,
FCGI_OVERLOADED = 2,
FCGI_UNKNOWN_ROLE = 3
} dcgi_protocol_status;
须要把稳dcgi_protocol_status
和fcgi_role
各个元素的值都是 FastCGI 协议里定义好的,而非 PHP 自定义的。
通讯样例
为了大略的表示,头只显示的类型和的 id,其他字段都不予以显示。下面的例子来自于官网
{FCGI_BEGIN_REQUEST, 1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS, 1,
\公众\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... \公众
}
{FCGI_STDIN, 1,
\"大众quantity=100&item=3047936\公众
}
{FCGI_STDOUT, 1,
\"大众Content-type: text/html\r\n\r\n<html>\n<head> ... \"大众
}
{FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
合营上面各个构造体,则可以大致想到 FastCGI 相应器的解析和相应流程:
首先读取消息头,得到其类型为FCGI_BEGIN_REQUEST
,然后解析其体,得知其须要的角色便是FCGI_RESPONDER
,flag
为0,表示要求结束后关闭线路。然后解析第二段,得知其类型为FCGI_PARAMS
,然后直接将体里的内容以回车符切割后存入环境变量。与之类似,处理完毕之后,则返回了FCGI_STDOUT
体和FCGI_END_REQUEST
体供 Web 做事器解析。
PHP 中的 FastCGI 的实现
下面对代码的解读条记只是我个人知识的一个梳理提炼,如有勘误,请大家指出。对不熟习该代码的同学来说可能是一个勾引,初步认识,如果以为很模糊不清晰,那么还是须要自己逐行去阅读。
以php-src/sapi/cgi/cgi_main.c
为例进行剖析解释,假设开拓环境为 unix 环境。main 函数中一些变量的定义,以及 sapi 的初始化,我们就不谈论在这里谈论了,只解释关于 FastCGI 干系的内容。
1.开启一个 socket 监听做事
fcgi_fd = fcgi_listen(bindpath, 128);
从这里开始监听,而fcgi_listen
函数里面则完成 socket 做事前三步socket
,bind
,listen
。
2.初始化要求工具
为fcgi_request
工具分配内存,绑定监听的 socket 套接字。
fcgi_init_request(&request, fcgi_fd);
全体要求从输入到返回,都环绕着fcgi_request
构造体工具在进行。
typedef
struct
_fcgi_request {
int
listen_socket;
int
fd;
int
id;
int
keep;
int
closed;
int
in_len;
int
in_pad;
fcgi_header out_hdr;
unsigned
char
out_pos;
unsigned
char
out_buf[10248];
unsigned
char
reserved[
sizeof
(fcgi_end_request_rec)];
HashTable env;
} fcgi_request;
3.创建多个 CGI 解析器子进程
这里子进程的个数默认是0,从配置文件中读取设置到环境变量,然后在程序中读取,然后创建指天命目的子进程来等待处理 Web 做事器的要求。
if
(
getenv
(
\"大众PHP_FCGI_CHILDREN\"大众
)) {
char
children_str =
getenv
(
\"大众PHP_FCGI_CHILDREN\"大众
);
children =
atoi
(children_str);
...
}
do
{
pid = fork();
switch
(pid) {
case
0:
parent = 0;
// 将子进程中的父进程标识改为0,防止循环 fork
/ don't catch our signals /
sigaction(SIGTERM, &old_term, 0);
sigaction(SIGQUIT, &old_quit, 0);
sigaction(SIGINT, &old_int, 0);
break
;
case
-1:
perror
(
\公众php (pre-forking)\公众
);
exit
(1);
break
;
default
:
/ Fine /
running++;
break
;
}
}
while
(parent && (running < children));
4.在子进程中吸收要求
到这里统统都还是 socket 的做事的套路。接管要求,然后调用了fcgi_read_request
。
fcgi_accept_request(&request)
int
fcgi_accept_request(fcgi_request req)
{
int
listen_socket = req->listen_socket;
sa_t sa;
socklen_t len =
sizeof
(sa);
req->fd = accept(listen_socket, (
struct
sockaddr )&sa, &len);
...
if
(req->fd >= 0) {
// 采取多路复用的机制
struct
pollfd fds;
int
ret;
fds.fd = req->fd;
fds.events = POLLIN;
fds.revents = 0;
do
{
errno
= 0;
ret = poll(&fds, 1, 5000);
}
while
(ret < 0 &&
errno
== EINTR);
if
(ret > 0 && (fds.revents & POLLIN)) {
break
;
}
// 仅仅是关闭 socket 连接,不清空 req->env
fcgi_close(req, 1, 0);
}
...
if
(fcgi_read_request(req)) {
return
req->fd;
}
}
并且把request
放入全局变量sapi_globals.server_context
,这点很主要,方便了在其他地方对要求的调用。
SG(server_context) = (
void
) &request;
5.读取数据
下面的代码删除一些非常情形的处理,只显示了正常情形下实行顺序。
在fcgi_read_request
中则完成我们在通讯样例中的读取,而个中很多的len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
操作,已经在前面的FastCGI 头中阐明过了。
这里是解析 FastCGI 协议的关键。
static
inline
ssize_t safe_read(fcgi_request req,
const
void
buf,
size_t
count)
{
int
ret;
size_t
n = 0;
do
{
errno
= 0;
ret = read(req->fd, ((
char
)buf)+n, count-n);
n += ret;
}
while
(n != count);
return
n;
}
static
int
fcgi_read_request(fcgi_request req)
{
...
if
(safe_read(req, &hdr,
sizeof
(fcgi_header)) !=
sizeof
(fcgi_header) || hdr.version < FCGI_VERSION_1) {
return
0;
}
len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
padding = hdr.paddingLength;
req->id = (hdr.requestIdB1 << 8) + hdr.requestIdB0;
if
(hdr.type == FCGI_BEGIN_REQUEST && len ==
sizeof
(fcgi_begin_request)) {
char
val;
if
(safe_read(req, buf, len+padding) != len+padding) {
return
0;
}
req->keep = (((fcgi_begin_request)buf)->flags & FCGI_KEEP_CONN);
switch
((((fcgi_begin_request)buf)->roleB1 << 8) + ((fcgi_begin_request)buf)->roleB0) {
case
FCGI_RESPONDER:
val = estrdup(
\公众RESPONDER\"大众
);
zend_hash_update(req->env,
\"大众FCGI_ROLE\"大众
,
sizeof
(
\公众FCGI_ROLE\"大众
), &val,
sizeof
(
char
), NULL);
break
;
...
default
:
return
0;
}
if
(safe_read(req, &hdr,
sizeof
(fcgi_header)) !=
sizeof
(fcgi_header) || hdr.version < FCGI_VERSION_1) {
return
0;
}
len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
padding = hdr.paddingLength;
while
(hdr.type == FCGI_PARAMS && len > 0) {
if
(safe_read(req, &hdr,
sizeof
(fcgi_header)) !=
sizeof
(fcgi_header) || hdr.version < FCGI_VERSION_1) {
req->keep = 0;
return
0;
}
len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
padding = hdr.paddingLength;
}
...
}
}
6.实行脚本
假设这次要求为PHP_MODE_STANDARD
则会调用php_execute_script
实行PHP文件。这里就不展开了。
7.结束要求
fcgi_finish_request(&request, 1);
int
fcgi_finish_request(fcgi_request req,
int
force_close)
{
int
ret = 1;
if
(req->fd >= 0) {
if
(!req->closed) {
ret = fcgi_flush(req, 1);
req->closed = 1;
}
fcgi_close(req, force_close, 1);
}
return
ret;
}
在fcgi_finish_request
中调用fcgi_flush
,fcgi_flush
中封装一个FCGI_END_REQUEST
体,再通过safe_write
写入 socket 连接的客户端描述符。
8.标准输入标准输出的处理
标准输入和标准输出在上面没有一起谈论,实际在cgi_sapi_module
构造体中有定义,但是cgi_sapi_module
这个sapi_module_struct
构造体与其他代码耦合太多,我自己也没深入的理解,这里大略做下比较,希望其他网友予以指示、补充。
cgi_sapi_module
中定义了sapi_cgi_read_post
来处理POST数据的读取
while
(read_bytes < count_bytes) {
fcgi_request request = (fcgi_request) SG(server_context);
tmp_read_bytes = fcgi_read(request, buffer + read_bytes, count_bytes - read_bytes);
read_bytes += tmp_read_bytes;
}
在fcgi_read
中则对FCGI_STDIN
的数据进行读取。
同时cgi_sapi_module
中定义了sapi_cgibin_ub_write
来接管输出处理,而个中又调用了sapi_cgibin_single_write
,末了实现了FCGI_STDOUT
FastCGI 数据包的封装
fcgi_write(request, FCGI_STDOUT, str, str_length);