本来这个系列告一段落了,但是看到@指尖流年的评论中提到的关于PHP中$_GET以及$_POST取值的一些疑问,我也想搞清楚这块的内容,故我重新写了一些代码做了些测试以及一部分关于php源码的探究与学习
- $_POST $_GET 概述
- $_POST $_GET php源码中的封装
- 继续完善web服务器,使其支持get,post请求,并尝试模拟$_GET,$_POST方式获取数据
$_POST $_GET 由来
1.$_GET:
在之前的教程中,我们可以很容易的知道$_GET的内容是取自浏览器访问地址?号后面的参数串,例如:
这个地址$_GET 应该是id=2所转化的数组
2.$_POST
相比较$_GET不同,我们可以用firefox来看到$_POST所接收的值,例如是一个静态界面,是一个提交表单
我们在2个输入框输入一些字段,然后通过firebug-网络查看post内容
中看到form data 就是post内容
name=111&password=222&submit=submit
$_POST $_GET php源码中的封装
既然我们需要做出一个服务器来支持post跟get传递,我们需要分析$_GET跟$_POST的由来,而这一块,我们就需要去查看PHP源码中涉及到的知识了。套用Linus Torvalds的一句话
talk is cheap,show me the code
1.php源码中对于get跟post的分析
一切从sapi开始,我们可以开始阅读相关的php源码,具体可以参考这个链接开始阅读
http://www.php-internals.com/book/?p=chapt02/02-02-00-overview
我们跳过其他的代码,直线从cgi部分入手,我们在php.-5.6-src/sapi/cgi 这个目录下的cgi_main.c中有这样的代码
static sapi_module_struct cgi_sapi_module = {
"cgi-fcgi", /* name */
"CGI/FastCGI", /* pretty name */
php_cgi_startup, /* startup */
php_module_shutdown_wrapper, /* shutdown */
sapi_cgi_activate, /* activate */
sapi_cgi_deactivate, /* deactivate */
sapi_cgi_ub_write, /* unbuffered write */
sapi_cgi_flush, /* flush */
NULL, /* get uid */
sapi_cgi_getenv, /* getenv */
php_error, /* error handler */
NULL, /* header handler */
sapi_cgi_send_headers, /* send headers handler */
NULL, /* send header handler */
sapi_cgi_read_post, /* read POST data */
sapi_cgi_read_cookies, /* read Cookies */
sapi_cgi_register_variables, /* register server variables */
sapi_cgi_log_message, /* Log message */
NULL, /* Get request time */
NULL, /* Child terminate */
STANDARD_SAPI_MODULE_PROPERTIES
};
好的 这块很显然我们需要2个字段,sapi_cgi_getenv以及sapi_cgi_read_post,嗯 一个是获取环境变量,另一个是获取post传递的数据的,借助一些之前掌握的知识,get接收的数据他是存储在环境变量中,web服务器中通常是这样存储的
setenv("QUERY_STRING", cgi_params, 1);//cgi_params为url后面跟着的字符串
我们继续往下看,看get跟post是怎么获取的
- sapi_cgi_getenv
static char *sapi_cgi_getenv(char *name, size_t name_len TSRMLS_DC)
{
return getenv(name);
}
- sapi_cgi_read_post
static int sapi_cgi_read_post(char *buffer, uint count_bytes TSRMLS_DC)
{
uint read_bytes = 0;
int tmp_read_bytes;
count_bytes = MIN(count_bytes, SG(request_info).content_length - SG(read_post_bytes));
while (read_bytes < count_bytes) {
tmp_read_bytes = read(STDIN_FILENO, buffer + read_bytes, count_bytes - read_bytes);
if (tmp_read_bytes <= 0) {
break;
}
read_bytes += tmp_read_bytes;
}
return read_bytes;
}
嗯,跟我们之前猜想的一样,get就是用
getenv("query_string")
获取的,而post就是获取输入缓冲区的data,我们可以用
read(STDIN_FILENO, buffer + read_bytes, count_bytes - read_bytes);
而我看到很多例子是用
fgets(data,post_content_length+1,stdin);
与cgi_main.c类似的,我们可以在fpm中看到相同的结构
/php.-5.6-src/sapi/fpm/fpm/fpm_main.c中看到一样的。
借着说一句,我们知道php-cli模式也是可以进行PHP代码的请求与编译的,单身php-cli模式并不支持get,post请求,大家知道原因吗?我们通过源码就能很容易看出来
- php-cli模式也称为embed模式,中文名叫做嵌入式模式,我们查看这块源码/php.-5.6-src/sapi/embed/php_embed.c中有这么一段
extern EMBED_SAPI_API sapi_module_struct php_embed_module = {
"embed", /* name */
"PHP Embedded Library", /* pretty name */
php_embed_startup, /* startup */
php_module_shutdown_wrapper, /* shutdown */
NULL, /* activate */
php_embed_deactivate, /* deactivate */
php_embed_ub_write, /* unbuffered write */
php_embed_flush, /* flush */
NULL, /* get uid */
NULL, /* getenv */
php_error, /* error handler */
NULL, /* header handler */
NULL, /* send headers handler */
php_embed_send_header, /* send header handler */
NULL, /* read POST data */
php_embed_read_cookies, /* read Cookies */
php_embed_register_variables, /* register server variables */
php_embed_log_message, /* Log message */
NULL, /* Get request time */
NULL, /* Child terminate */
STANDARD_SAPI_MODULE_PROPERTIES
};
我们可以看到getenv以及read POST data这2个部分都是NULL,所以可以得出php-cli模式是无法获取get,post请求的
好了,回到主线,我们换个思路看,我们从一个php请求开始到结束中去寻找这2个变量的定义,还是在cgi_main.c中去寻找
- 继续看cgi_sapi_module
在这个结构体中,中间要实现的方法php_cgi_startup,这个方法是当一个应用要调用PHP的时候,这个函数会被调用,我们就会估计GET跟POST是在这个方法中声明的,我们顺着这个往下找,在这个文件中是这样定义的
static int php_cgi_startup(sapi_module_struct *sapi_module)
{
if (php_module_startup(sapi_module, &cgi_module_entry, 1) == FAILURE) {
return FAILURE;
}
return SUCCESS;
}
嗯,我们继续去找php_module_startup这个方法
于是在/main/main.c中的找到这个php_module_startup方法,我们在这个方法中找到下面这个方法
php_startup_auto_globals(TSRMLS_C);//2302行左右
我们去寻找这个方法
- /main/php_variables.c中有这么一段
void php_startup_auto_globals(TSRMLS_D)
{
zend_register_auto_global(ZEND_STRL("_GET"), 0, php_auto_globals_create_get TSRMLS_CC);
zend_register_auto_global(ZEND_STRL("_POST"), 0, php_auto_globals_create_post TSRMLS_CC);
zend_register_auto_global(ZEND_STRL("_COOKIE"), 0, php_auto_globals_create_cookie TSRMLS_CC);
zend_register_auto_global(ZEND_STRL("_SERVER"), PG(auto_globals_jit), php_auto_globals_create_server TSRMLS_CC);
zend_register_auto_global(ZEND_STRL("_ENV"), PG(auto_globals_jit), php_auto_globals_create_env TSRMLS_CC);
zend_register_auto_global(ZEND_STRL("_REQUEST"), PG(auto_globals_jit), php_auto_globals_create_request TSRMLS_CC);
zend_register_auto_global(ZEND_STRL("_FILES"), 0, php_auto_globals_create_files TSRMLS_CC);
}
哈哈,我们终于找到了,那也就是说这个是php-cgi以及php-fpm等PHP内核才封装的这个$_GET以及$_POST 方法,所以我们实现的cgi程序无法调用起$_GET,$_POST方法
继续完善web服务器,使其支持get,post请求,并尝试模拟$_GET,$_POST方式获取数据
1.完善get请求
回到之前做的web服务器,在wrap_socket.c中php_cgi方法中,设置query_string环境变量
void php_cgi(char* script_path, int fd,char *cgi_params) {
char *emptylist[] = {script_path };
setenv("QUERY_STRING", cgi_params, 1);
dup2(fd, STDOUT_FILENO);
//execl("/usr/bin/php","php",script_path,(void *)NULL);
//execve("./slow-cgi", emptylist, envp);
execlp("./slow-cgi.cgi",script_path,(char *) NULL);
//execve(script_path, emptylist, environ);
}
在cgi程序中获取环境变量数据,就可以捕获get请求啦
int main(int argc, char * argv[]) {
char *cgi_params,*script_path;
int post_content_length;
char content[MAXLINE],data[MAXLINE];
printf("Content-type: text/html\r\n\r\n");
cgi_params = getenv("QUERY_STRING");
script_path = argv[0];
execl("/usr/local/php56/bin/php-cgi","php-cgi",script_path,cgi_params,(void *)NULL);
exit(1);
}
这里,我们不用php程序去请求我们的程序,因为我们要打印$_GET 方法,所以根据上文分析的,我们只能用php-cgi 或者php-fpm去请求我们的文件
script_path 为请求的php文件
cgi_params 获取到的get参数
这里为什么是这样的请求方式在php源码中也是有迹可循的,可以看这里/sapi/cgi/cgi_main.c 2280行左右
/* all remaining arguments are part of the query string
* this section of code concatenates all remaining arguments
* into a single string, separating args with a &
* this allows command lines like:
*
* test.php v1=test v2=hello+world!
* test.php "v1=test&v2=hello world!"
* test.php v1=test "v2=hello world!"
*/
if (!SG(request_info).query_string && argc > php_optind) {
int slen = strlen(PG(arg_separator).input);
len = 0;
for (i = php_optind; i < argc; i++) {
if (i < (argc - 1)) {
len += strlen(argv[i]) + slen;
} else {
len += strlen(argv[i]);
}
}
len += 2;
s = malloc(len);
*s = '\0'; /* we are pretending it came from the environment */
for (i = php_optind; i < argc; i++) {
strlcat(s, argv[i], len);
if (i < (argc - 1)) {
strlcat(s, PG(arg_separator).input, len);
}
}
SG(request_info).query_string = s;
free_query_string = 1;
}
好的,我们看看展示请求的PHP页面以及浏览器展示的结果
index.php
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>鹏哥的第一个web服务器</title>
</head>
<body>
<?php
$array = array(
"id" => "1",
"name"=> "pengge",
"aaa" => "sdsdd",
"yes" => "sdsdfsfsff"
);
echo "<pre>";
var_dump($_GET);
var_dump($_SERVER);
var_dump($array);
?>
</body>
</html>
我们浏览器请求一下
大致的数据可以得到,也可以模拟到$_GET方式的接收
2.纠结的post请求
实话说,post请求我一直没有调试好,所以我就简单的写了这块的代码,只实现了从浏览器获取到post数据,但是让cgi程序从缓冲区获取post数据一直没有实现,这也是隔了好久没有更新的原因,希望有大神可以吧这块调试出来。。
获取post数据的过程
/*
* 处理客户端的http请求.
* cfd : 客户端文件描述符
* path : 请求的文件路径
* query : 请求发送的过来的数据, url ? 后面那些数据
*/
void request_cgi(int fd, const char* path, const char* query)
{
char buf[MAXLINE],data[MAXLINE];
char contlen_string[MAXLINE];
int p[2];
pid_t pid;
int contlen = -1; //报文长度
char c;
while(getfdline(fd, buf, sizeof(buf))>0){
buf[15] = '\0';
if(!strcasecmp(buf, "Content-Length:"))
contlen = atoi(buf + 16);
}
if(contlen == -1){ //错误的报文,直接返回错误结果
p_error("contlen error");
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
sprintf(buf, "%sServer: Tiny Web Server\r\n",buf);
sprintf(buf, "%sContent-Type:text/html\r\n",buf);
sprintf(contlen_string, "%d", contlen);
setenv("CONTENT-LENGTH",contlen_string , 1);
read(fd, data, contlen);
printf("post data= %s\n",data);
write(fd, buf, strlen(buf));
dup2(fd,STDOUT_FILENO);
execlp("./slow-cgi.cgi", path, (char *) NULL);
exit(1);
}
通过read(fd, data, contlen);就可以获取到post请求的内容,截图为
后面的调试不通过,我就不写了,最后展示下cgi程序获取get以及post请求的全代码
#define MAXLINE 1024
int main(int argc, char * argv[]) {
char *cgi_params,*script_path;
int post_content_length;
char content[MAXLINE],data[MAXLINE];
printf("Content-type: text/html\r\n\r\n");
//获取get数据
cgi_params = getenv("QUERY_STRING");
if(getenv("CONTENT-LENGTH") != NULL) {
//获取post长度
post_content_length = atol(getenv("CONTENT-LENGTH"));
printf("post_content_length=%d\n",post_content_length);
//获取缓冲区内容,也就是获取post内容
fflush(stdin);
while((fgets(data,post_content_length+1,stdin)) != NULL) {
sprintf(content, "Info:%s\r\n",data);
printf("Content-length: %lu\r\n", strlen(content));
printf("Content-type: text/html\r\n\r\n");
printf("%s", content);
exit(1);
}
fflush(stdout);
exit(0);
exit(1);
script_path = argv[0];
}
script_path = argv[0];
execl("/usr/local/php56/bin/php-cgi","php-cgi",script_path,cgi_params,(void *)NULL);
exit(1);
}