引言
本篇文章学习了CGI的原理和一个简单的http服务器的实现,该服务器支持CGI。
给出CGI和http服务器参考地址,读者可以移步这里:
http://www.cnblogs.com/liuzhang/p/3929198.html (CGI)
https://github.com/EZLippi/Tinyhttpd (httpserver)
什么是CGI
引自wikipedia:
Common Gateway Interface (CGI) is a standard way for web servers to interface with executable programs installed on a server that generate web pages dynamically. Such programs are known as CGI scripts or simply CGIs; they are usually written in a scripting language, but can be written in any programming language.
为什么要引入CGI
引文: http://www.cnblogs.com/liuzhang/p/3929198.html (很好的文章)
最早的Web服务器简单地响应浏览器发来的HTTP请求,并将存储在服务器上的HTML文件返回给浏览器,也就是静态html。事物总是不断发展,网站也越来越复杂,所以出现动态技术。但是服务器并不能直接运行 php,asp这样的文件,自己不能做,外包给别人吧,但是要与第三做个约定,我给你什么,然后你给我什么,就是握把请求参数发送给你,然后我接收你的处理结果给客户端。那这个约定就是 common gateway interface,简称cgi。这个协议可以用vb,c,php,python 来实现。cgi只是接口协议,根本不是什么语言。下面图可以看到流程
一个简单的http服务器
网友EZLippi用C语言写了一个简单的http服务器,逻辑比较清晰,展示了对HTTP协议的处理过程,以及对CGI的支持。HTTP协议是建立在TCP协议之上的,对HTTP协议的分析主要还是分析读取的每一行内容,比较简单。下面主要分析对CGI的实现:
大体过程是这样的:
服务器从URL中获取cgi的名字,然后fork一个子进程,设置好环境变量和管道(使用dup2函数将STDIN、STDOUT重定向到管道)之后,使用exec系列函数运行cgi程序。cgi程序可以从STDIN或环境变量表中读取数据,然后把结果写到STDOUT就完事了。
而服务器会从管道读取cgi程序的处理结果,返回给浏览器。
这里使用的进程间通信方法是管道。
// 创建管道
if (pipe(cgi_output) < 0) {
cannot_execute(client);
return;
}
if (pipe(cgi_input) < 0) {
cannot_execute(client);
return;
}
// 创建子进程
if ( (pid = fork()) < 0 ) {
cannot_execute(client);
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
if (pid == 0) /* child: CGI script */
{
char meth_env[255];
char query_env[255];
char length_env[255];
dup2(cgi_output[1], STDOUT); // 做重定向,STDOUT指向cgi_output的写端
dup2(cgi_input[0], STDIN); // 做重定向,STDIN指向cgi_input的读端
close(cgi_output[0]); //子进程不需要从cgi_output读,所以close它
close(cgi_input[1]); //子进程不需要向cgi_input写,所以close它
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env); // 设置环境变量
if (strcasecmp(method, "GET") == 0) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
execl(path, NULL); // 执行cgi程序
exit(0);
} else { /* parent */
close(cgi_output[1]); // 父进程不需要向cgi_output写,所以close它
close(cgi_input[0]); // 父进程不需要从cgi_input读,所以close它
if (strcasecmp(method, "POST") == 0)
for (i = 0; i < content_length; i++) {
recv(client, &c, 1, 0);
write(cgi_input[1], &c, 1); // 把POST的数据通过管道发给CGI程序
}
while (read(cgi_output[0], &c, 1) > 0) // 读取CGI程序的返回结果
send(client, &c, 1, 0); // 然后把CGI的返回结果发给客户端(浏览器)
close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid, &status, 0);
}
fork()之后,之前打开的文件描述符在子进程中仍是打开的,(使用了CLOSE_ON_EXEC的除外),所以父子进程关闭了无用的fd。
server—————————————-cgi
cgi_output[0] <—————- cgi_output[1] (STDOUT)
cgi_input[1] —————-> cgi_input[0] (STDIN)
因为执行CGI程序之前对管道做了重定向,所以cgi程序操作标准输入和标准输出就可以了。
附上作者给出的cgi脚本程序,使用perl语言编写:
#!/usr/bin/perl -Tw
use strict;
use CGI;
my($cgi) = new CGI;
print $cgi->header;
my($color) = "blue";
$color = $cgi->param('color') if defined $cgi->param('color');
print $cgi->start_html(-title => uc($color),
-BGCOLOR => $color);
print $cgi->h1("This is $color");
print $cgi->end_html;
在附上我用C语言写的CGI,与上面的perl脚本功能相同
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
size_t content_len = 0;
char response[1024];
printf("HTTP/1.0 200 OK\r\n");
printf("Content-Type: text/html\r\n");
printf("\r\n");
content_len = atoi(getenv("CONTENT_LENGTH")); // 读取环境变量
int nr = read(0, response, content_len); // 从stdin读取父进程通过管道发送过来的数据
response[nr] = '\0';
char *color = strchr(response, '=') + 1; // 分析出颜色
// 结果写到stdout,服务器会从管道读取到这些数据,返回给浏览器显示
puts("<html>\r\n");
puts("<head>\r\n");
printf("<title>%s</title>\r\n", color);
puts("</head>\r\n");
printf("<body bgcolor=\"%s\">\r\n", color);
puts("<h1>This is green, Generate by CGI written in C</h1>\r\n");
puts("</body>\r\n");
puts("</html>\r\n");
return 0;
}