用C一步步开发web服务器(5)

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

本来这个系列告一段落了,但是看到@指尖流年的评论中提到的关于PHP中$_GET以及$_POST取值的一些疑问,我也想搞清楚这块的内容,故我重新写了一些代码做了些测试以及一部分关于php源码的探究与学习

  • $_POST $_GET 概述
  • $_POST $_GET php源码中的封装
  • 继续完善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所接收的值,例如是一个静态界面,是一个提交表单

用C一步步开发web服务器(5)_第1张图片
image

我们在2个输入框输入一些字段,然后通过firebug-网络查看post内容

用C一步步开发web服务器(5)_第2张图片
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,嗯 一个是获取环境变量,另一个是获取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




鹏哥的第一个web服务器


 "1",
    "name"=> "pengge",
    "aaa" => "sdsdd",
    "yes" => "sdsdfsfsff"
);
echo "
";
var_dump($_GET);
var_dump($_SERVER);
var_dump($array);
?>


我们浏览器请求一下


用C一步步开发web服务器(5)_第3张图片
iamge

大致的数据可以得到,也可以模拟到$_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请求的内容,截图为


用C一步步开发web服务器(5)_第4张图片
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);
        
        //获取缓冲区内容,也就是获取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上,希望大家能够得到些许帮助,也希望大家能够帮我解决上面的问题原文章链接

你可能感兴趣的:(用C一步步开发web服务器(5))