用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所接收的值,例如是一个静态界面,是一个提交表单

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,嗯 一个是获取环境变量,另一个是获取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>

我们浏览器请求一下


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请求的内容,截图为


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上,希望大家能够得到些许帮助,也希望大家能够帮我解决上面的问题原文章链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容