首先得说明下CGI和高性能没有半毛钱关系,甚至是低性能的代名词。
CGI是什么
CGI是一种协议,并不是一种具体的代码程序。上古时代的PHP程序就是靠CGI协议与HTTP服务器比如Apache协作完成。最开始那会儿Web站点的出现一般都是纯静态货色,只要你精通HTML和PS然后你就能配合Apache什么的就能搞出一个炫酷狂拽屌炸天的网站。
然而总有刁民想整幺蛾子:他们想整动态数据
但是这件事情让Apache来做总归是不合理的。从工程角度来讲,叫做耦合太严重;从UNIX哲学角度来讲,软件功能要精悍专一。Apache服务器就应该老老实实做好http,动态数据的读取应该交给其他程序来做。所以CGI就应运而生,全称叫做Common Gateway Interface。除了HTML和CSS以及jQuery外的任何一门语言都可以用来编写CGI程序,PHP、Python、Perl都可以的。
CGI粗暴流程
http服务器和cgi程序相互进行友好数据磋商一共就三个套路:
其中http服务器向cgi程序传输数据,是通过环境变量和标准输入。比如php里我们常见的$_SERVER['REQUEST_METHOD']等就是通过环境变量传递的,又或者说POST方法的PO过去的数据一般说来是通过标准输入向cgi写入。当cgi程序完成了CURD工作后处理好的数据需要返回给http服务器,此时则是通过cgi向标准输出中写数据完成。考虑到一般情况下http服务器的标准输入已经重定向到了cgi程序,所以cgi程序里直接echo、print_r等等就相当于直接将数据写入到了标准输出。
每当有HTTP请求打到http服务器上时候,服务器程序要做的标准流程就是fork出一个子进程,然后该子进程去exec写好的cgi程序。我听行业大佬们叫这个流程为fork-and-execute。毫无疑问,这就是传说中“低性能”代表操作。fork为宝贵系统资源,一次fork操作都是需要一些吃奶力气的,更可怕的时候如果有10000个http请求,就需要fork 10000次,你们感受下。
为了让我们广大泥腿子们内心找到灵魂归宿和熟悉的味道配方,为了更好的以熟悉的面孔向大家展示CGI协议的具体内容,我决定从ietf的CGI协议标准里给大家找一些老面孔混个脸熟,你们感受一下:
原来PHP里$_SERVER全局变量的数据都是来自于这里... ...
Ctrl + C && Ctrl + V
有道是老话说的好
下面我们【粗暴地模拟】一下上古时代的基于CGI协议的Web开发是什么感受。首先我用上古语言C语言手写了一个【能用】的服务器,然后我们在服务器收到请求的时候fork一个子进程,在子进程中调用php-cgi程序(此处注意!php-cgi是fastcgi协议的实现)。我先把基于C语言的服务器代码贴一下,里面包含大量注释,一般人都能看懂,尽管该段程序可能充斥着内存泄漏、野指针到处飞、随时出现core dump,但是大概率情况下还是能用的。在后面的日子里,这坨烂代码将会伴随着我们逐渐演化为可能是【高性能】的服务器软件。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define BUFFER_SIZE 4096
extern char ** environ;
int main( int argc, char * argv[] ) {
//for ( int index = 0; environ[ index ] != NULL; index++ ) {
//printf( "%s\n", environ[ index ] );
//}
if ( argc < 3 ) {
printf( "usage : ./server 0.0.0.0 6666\n" );
exit( -1 );
}
const char * ip_string_p = argv[ 1 ];
int port_int = atoi( argv[ 2 ] );
int backlog_int = 10;
int common_ret_int;
int listen_socket_fd;
int client_socket_fd;
struct sockaddr_in socket_base_struct;
struct sockaddr_in client_base_struct;
int client_struct_length_int;
int socket_opt_address_reuse_int = 1;
listen_socket_fd = socket( PF_INET, SOCK_STREAM, 0 );
if ( listen_socket_fd < 0 ) {
exit( -1 );
}
setsockopt( listen_socket_fd, SOL_SOCKET, SO_REUSEADDR, &socket_opt_address_reuse_int, sizeof( socket_opt_address_reuse_int ) );
// 创建socket struct结构体并清空其中内存的数据
bzero( &socket_base_struct, sizeof( socket_base_struct ) );
socket_base_struct.sin_family = PF_INET;
// 将PORT转换成big-endian的PORT
socket_base_struct.sin_port = htons( port_int );
// 将IP地址转换为big-endian的IP地址
inet_pton( PF_INET, ip_string_p, &socket_base_struct.sin_addr );
// 将分配好的address struct绑定好创建的listen socket上去
common_ret_int = bind( listen_socket_fd, ( struct sockaddr * )&socket_base_struct, sizeof( socket_base_struct ) );
if ( common_ret_int < 0 ) {
exit( -1 );
}
// 开始监听listen socket
common_ret_int = listen( listen_socket_fd, backlog_int );
if ( common_ret_int < 0 ) {
exit( -1 );
}
client_struct_length_int = sizeof( client_base_struct );
// 让服务器陷入无限循环中
while ( 1 ) {
client_socket_fd = accept( listen_socket_fd, ( struct sockaddr * )&client_base_struct, &client_struct_length_int );
if ( client_socket_fd < 0 ) {
exit( -1 );
}
// fork一下,子进程去调用处理 php-cgi 程序
pid_t pid;
pid = fork();
if ( 0 == pid ) {
// 别废话那么多,先能用再说
char buf[ BUFFER_SIZE ];
char content[ BUFFER_SIZE ];
char * http_state_line_string_p;
char * http_method_string_p;
char * http_query_string_p;
char * http_version_string_p;
FILE * file_fd;
recv( client_socket_fd, content, BUFFER_SIZE - 1, 0 );
/*
此处顺带为了让泥腿子们了解HTTP协议,我直接把http协议传输过来的数据
全部打印出来,你们感受一下传说中HTTP协议load的数据是长什么样子的.
一般说来,http服务器要做的就是解析这段http数据,解析成标准格式供我们
使用。
*/
printf( "这就是传说中的HTTP协议的具体数据内容:\n" );
printf( "%s\n", content );
printf( "传说中HTTP协议数据内容已经OVER\n" );
/*
下面四行代码,是将HTTP数据中第一行:状态请求行 截取出来后开始解析
- GET则是PHP中常见的$_SERVER['http_method']
- /?username=xiaodushe则为QUERY_STRING
- HTTP/1.1则为http协议版本
这三项内容在php中都保存在了$_SERVER中..如果我没记错的话
strtok()是C语言函数中的一个奇葩......
*/
http_state_line_string_p = strtok( content, "\r\n" );
http_method_string_p = strtok( http_state_line_string_p, " " );
http_query_string_p = strtok( NULL, " " );
http_version_string_p = strtok( NULL, " " );
// 就先码死这个php-cgi程序吧,理论上cgi程序应该根据请求路径不同加载不同的cgi程序...
// 这个。。。先将就一下,码死成一个固定的cgi,能用就行..
// 我们将get参数通过设置环境变量传递给php-cgi程序
setenv( "QUERY_STRING", http_query_string_p, 1 );
setenv( "HTTP_METHOD", http_method_string_p, 1 );
setenv( "HTTP_VERSION", http_version_string_p, 1 );
// 从php-cgi拿回来数据...
FILE * fp = popen( "./test.php", "r" );
// 下面是按照http协议标准手工构造http数据返回给客户端
// 如果你不按照下面标准进行构造,客户端一般会返回一些提示,比如
// curl会返回:curl: (52) Empty reply from server
char html_entity[ BUFFER_SIZE ];
char html_body_content[ BUFFER_SIZE ];
fread( html_body_content, sizeof( char ), sizeof( html_body_content ), fp );
char html_response_template[] = "HTTP/1.1 200 OK\r\nContent-Type:text/plain\r\nContent-Length: %d\r\nHttp-Server:ti-server\r\n\r\n%s";
sprintf( html_entity, html_response_template, strlen( html_body_content ), html_body_content );
send( client_socket_fd, html_entity, sizeof( html_entity ), 0 );
// 关闭与客户端的连接.
close( client_socket_fd );
exit( -1 );
}
}
// 关闭socket
close( listen_socket_fd );
return 0;
}
上面的程序保存为server.c,请在Linux下输入如下命令编译一下,反正能用:
gcc server.c -o server // 编译
./server 0.0.0.0 6666 // 表示在6666端口上启动该服务器
与server.c同级目录下新建一个test.php文件,内容如下:
#! /usr/bin/php-cgi
<?php
echo 'http版本:'.$_SERVER['HTTP_VERSION'].PHP_EOL;
echo 'http方法:'.$_SERVER['HTTP_METHOD'].PHP_EOL;
echo 'query-string:'.$_SERVER['QUERY_STRING'].PHP_EOL;
echo "hello,xiaodushe~".PHP_EOL;
上述demo代码已经上传到github,地址为:
https://github.com/elarity/wechat-official-accounts-demo-code
好了,一切就绪,我们使用curl充当浏览器访问一下服务器。我这里服务器打印日志和curl客户端打印的日志分别如下图所示,你们感受一下:
服务器端的日志数据
curl客户端的日志数据
好了,这就是一个典型的极其粗暴的CGI程序流程。其中一些细节并不完全遵守CGI标准流程,重在参与重在参与。。。
遥想泥腿子之王Lerdorf当年,拖鞋裤衩,抠脚趾头间PHP CGI码网站...
说来有点儿意思,我当初刚接触PHP那会儿还是用的APACHE服务器,这APACHE最初和PHP就有两种友好的数据洽谈方式:
由于PHP_MOD方式性能明显是要比CGI方式好不少的,所以默认情况下APACHE是用PHP_MOD方式。有兴趣的同学可以扒一个APACHE然后配置一波儿试试看,还是那句话:反正能用。
文章附录以及关键字: