用C一步步開發(fā)web服務器(5)

大家可以教程1教程2教程3,教程4中查看之前內容。

本來這個系列告一段落了,但是看到@指尖流年的評論中提到的關于PHP中$_GET以及$_POST取值的一些疑問,我也想搞清楚這塊的內容,故我重新寫了一些代碼做了些測試以及一部分關于php源碼的探究與學習

  • $_POST $_GET 概述
  • $_POST $_GET php源碼中的封裝
  • 繼續(xù)完善web服務器,使其支持get,post請求,并嘗試模擬$_GET,$_POST方式獲取數據

$_POST $_GET 由來

1.$_GET:

在之前的教程中,我們可以很容易的知道$_GET的內容是取自瀏覽器訪問地址?號后面的參數串,例如:

http://localhost:8000/index.php?id=2

這個地址$_GET 應該是id=2所轉化的數組

2.$_POST

相比較$_GET不同,我們可以用firefox來看到$_POST所接收的值,例如是一個靜態(tài)界面,是一個提交表單

image

我們在2個輸入框輸入一些字段,然后通過firebug-網絡查看post內容

image

中看到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覽器請求一下


iamge

大致的數據可以得到,也可以模擬到$_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);
}

具體的所有代碼我會放在github上,希望大家能夠得到些許幫助,也希望大家能夠幫我解決上面的問題原文章鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容