本來這個系列告一段落了,但是看到@指尖流年的評論中提到的關于PHP中$_GET以及$_POST取值的一些疑問,我也想搞清楚這塊的內容,故我重新寫了一些代碼做了些測試以及一部分關于php源碼的探究與學習
- $_POST $_GET 概述
- $_POST $_GET php源碼中的封裝
- 繼續(xù)完善web服務器,使其支持get,post請求,并嘗試模擬$_GET,$_POST方式獲取數據
$_POST $_GET 由來
1.$_GET:
在之前的教程中,我們可以很容易的知道$_GET的內容是取自瀏覽器訪問地址?號后面的參數串,例如:
這個地址$_GET 應該是id=2所轉化的數組
2.$_POST
相比較$_GET不同,我們可以用firefox來看到$_POST所接收的值,例如是一個靜態(tài)界面,是一個提交表單

我們在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,嗯 一個是獲取環(huán)境變量,另一個是獲取post傳遞的數據的,借助一些之前掌握的知識,get接收的數據他是存儲在環(huán)境變量中,web服務器中通常是這樣存儲的
setenv("QUERY_STRING", cgi_params, 1);//cgi_params為url后面跟著的字符串
我們繼續(xù)往下看,看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就是獲取輸入緩沖區(qū)的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中去尋找
- 繼續(xù)看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;
}
嗯,我們繼續(xù)去找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方法
繼續(xù)完善web服務器,使其支持get,post請求,并嘗試模擬$_GET,$_POST方式獲取數據
1.完善get請求
回到之前做的web服務器,在wrap_socket.c中php_cgi方法中,設置query_string環(huán)境變量
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程序中獲取環(huán)境變量數據,就可以捕獲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>
我們?yōu)g覽器請求一下

大致的數據可以得到,也可以模擬到$_GET方式的接收
2.糾結的post請求
實話說,post請求我一直沒有調試好,所以我就簡單的寫了這塊的代碼,只實現了從瀏覽器獲取到post數據,但是讓cgi程序從緩沖區(qū)獲取post數據一直沒有實現,這也是隔了好久沒有更新的原因,希望有大神可以吧這塊調試出來。。
獲取post數據的過程
/*
* 處理客戶端的http請求.
* cfd : 客戶端文件描述符
* path : 請求的文件路徑
* query : 請求發(fā)送的過來的數據, 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請求的內容,截圖為
image
后面的調試不通過,我就不寫了,最后展示下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);
//獲取緩沖區(qū)內容,也就是獲取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);
}
