Fastcgi 协议解析及 get&post 使用实例

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/liushengxi_root/article/details/84976353

前言:

基于:

csdn1

娄神的描述

其实看上面两位大佬的博客就已经ojbk了.写的目地主要是自己总结学习一下.

基础:

1.基础的 WebServer应该支持客户端请求静态文件和动态文件.
2. 浏览器是不能够解析动态的php文件的!那么我们编写服务器程序时候如果遇到请求.php动态文件时就应该将php文件翻译为html文件.
3. php-fpm就能够将php文件翻译为html文件.所以我们的webserver将通过进程间通信把php文件交给php-fpm,然后把php-fpm翻译过后的html文件发给客户端即可,(php-fpm)就等价于一个CGI 服务器
4.那么我们如何才能让php-fpm帮我们解析我们想要翻译成.html文件的.php文件呢?通过**fastcgi协议,其实就是WebServerphp-fpm之间通信的规则(或者说是'语言')**

1. fastcgi 协议

(1) 请求头

   和’任何协议一样,fastcgi协议也有一个消息头或者叫做请求头.其格式是固定的.用以表示消息体的类型和信息.任意一个FastCGI数据包必须以一个8字节的消息头开始

typedef struct
{
    unsigned char version;     //FCGI版本信息,目前一般定义为1
    unsigned char type;        //每次发送的消息的类型.相当于flag,具体表示见下面:
    
    unsigned char requestIdB1; //合起来表示本次请求的编号 ID
    unsigned char requestIdB0;
    
    unsigned char contentLengthB1; //合起来表示 body 长度
    unsigned char contentLengthB0;
    
    unsigned char paddingLength; //填充字节长度,填充长度不可超过255字节
    unsigned char reserved;      //保留字节
} FCGI_Header;                   //消息头

type 字段分别是如下含义:

// FCGI_Header 中 type 的具体值
#define FCGI_BEGIN_REQUEST 1  //一次请求的开始(web->fastcgi)
#define FCGI_ABORT_REQUEST 2  //异常终止一次请求(web->fastcgi)
#define FCGI_END_REQUEST   3  //请求处理完毕,正常结束(fastcgi->web)

#define FCGI_PARAMS        4  /*传递参数,表明消息中包含的数据为某个name-value对
(web->fastcgi)*/

#define FCGI_STDIN         5  
/*POST 内容传递,从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm时,
这种消息的type就得设为5(web->fastcgi)*/

#define FCGI_STDOUT        6 
//正常响应内容,php-fpm给web服务器回的正常响应消息的type就设为6(fastcgi->web)

#define FCGI_STDERR        7   
//php-fpm给web服务器回的错误响应设为7(fastcgi->web)

#define FCGI_DATA          8   //向CGI程序传递的额外数据(WEB->FastCGI) 
#define FCGI_GET_VALUES    9   // 向FastCGI程序询问一些环境变量(WEB->FastCGI)
#define FCGI_GET_VALUES_RESULT 10 // 询问环境变量的结果(FastCGI->WEB)
#define FCGI_UNKNOWN_TYPE      11 //通知 webserver 所请求 type 非正常类型
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)  // 未知类型,可能用作拓展

   requestIdB1 ,requestIdB0 合起来表示本次请求的编号,其中requestIdB1是请求编号的高八位,requestIdB0是请求编号的低八位。这个字段的存在允许Web服务器在一次连接中向FastCGI服务器发送多个不同的请求,只要使用不同的请求编号即可

   contentLengthB1`` contentLengthB0)合起来表示消息头后仍有多少字节的数据(数据长度),contentLengthB1表示其高八位,contentLengthB0表示其低八位。数据长度的表示范围在0~65535(即2^16-1)之间,因而若数据超过65535字节,则必须将之分为多个数据包来传输(编程中要注意这一点)

实例1:makeHeader函数的构造

//FCGI的版本
#define FCGI_VERSION_1 1

FCGI_Header makeHeader(int type, int requestId,
                       int contentLength, int paddingLength)
{
    FCGI_Header header;

    header.version = FCGI_VERSION_1;

    header.type = (unsigned char)type;

    /* 两个字段保存请求ID */
    header.requestIdB1 = (unsigned char)((requestId >> 8) & 0xff);
    header.requestIdB0 = (unsigned char)(requestId & 0xff);

    /* 两个字段保存内容长度 */
    header.contentLengthB1 = (unsigned char)((contentLength >> 8) & 0xff);
    header.contentLengthB0 = (unsigned char)(contentLength & 0xff);

    /* 填充字节的长度 */
    header.paddingLength = (unsigned char)paddingLength;

    /* 保存字节赋为 0 */
    header.reserved = 0;

    return header;
}

(2) 消息体:

类似于http协议,在我们发送完消息头之后,我们就需要发送消息体了.那这里肯定还是会因为type的不同而有所不同.

type == FCGI_BEGIN_REQUEST 1 一次请求的开始(web->fastcgi)

这种消息是一中固定的8字节结构,因此我们会推出消息头中的contentLengthBx在这种情况下肯定也是固定的.

typedef struct
{
    unsigned char roleB1; 
    unsigned char roleB0;
    //合起来表示 webserver 所期望php-fpm 扮演的角色,具体取值下面有


    unsigned char flags; //确定 php-fpm 处理完一次请求之后是否关闭,flag=1,不关闭
    unsigned char reserved[5]; //保留字段
} FCGI_BeginRequestBody;       //开始请求体

//webserver 期望 php-fpm 扮演的角色(想让php-fpm做什么)
#define FCGI_RESPONDER 1  
//接受http关联的所有信息,并产生http响应,接受来自webserver的PARAMS环境变量

#define FCGI_AUTHORIZER 2 
//对于认证的会关联其http请求,未认证的则关闭请求

#define FCGI_FILTER 3     
//过滤web server 中的额外数据流,并产生过滤后的http响应

总的来讲,fastcgi协议中规定了三种角色,有:

enum FCGI_Role {
  FCGI_RESPONDER  = 1,  // 响应器,php-fpm接受我们的http所关联的信息,并产生响应
  
  FCGI_AUTHORIZER = 2,  
 //认证器,php-fpm会对我们的请求进行认证,认证通过的其会返回响应,认证不通过则关闭请求

  FCGI_FILTER     = 3   // 过滤器,过滤请求中的额外数据流,并产生过滤后的http响应
};

一般,我们的webserver 就把它当作响应器就行了(也就是说把该字段设为FCGI_RESPONDER

实例2:与php-fpm的连接与第一次请求

typedef struct
{
    int sockfd_;    //与php-fpm 建立的 sockfd
    int requestId_; //record 里的请求ID
    int flag_;      //用来标志当前读取内容是否为html内容

} FastCgi_t;

void FastCgi_init(FastCgi_t *c)
{
    c->sockfd_ = 0;    //与php-fpm 建立的 sockfd
    c->flag_ = 0;      //record 里的请求ID
    c->requestId_ = 0; //用来标志当前读取内容是否为html内容
}

void setRequestId(FastCgi_t *c, int requestId)
{
    c->requestId_ = requestId;
}

int main()
{
	FastCgi_t *c;
    c = (FastCgi_t *)malloc(sizeof(FastCgi_t));
    FastCgi_init(c);
    setRequestId(c, 1); /*1 用来表明此消息为请求开始的第一个消息*/
    startConnect(c); //略,就是与127.0.0.1 9000 建立了一个连接

    sendStartRequestRecord(c); //主要是这个函数!!!!
    
    sendParams(c, "SCRIPT_FILENAME", "/home/Shengxi-Liu/WebServer/wwwroot/php/Operation.php");
    sendParams(c, "REQUEST_METHOD", "POST");
    ...
}

sendStartRequestRecord(c) 第一次请求函数:

typedef struct
{
    unsigned char roleB1; 
    unsigned char roleB0;
    unsigned char flags;       //确定 php-fpm 处理完一次请求之后是否关闭
    unsigned char reserved[5]; //保留字段
} FCGI_BeginRequestBody;       //开始请求体

typedef struct
{
    FCGI_Header header;         //消息头
    FCGI_BeginRequestBody body; //开始请求体
} FCGI_BeginRequestRecord;      //完整消息--开始

FCGI_BeginRequestBody makeBeginRequestBody(int role, int keepConnection)
{
    FCGI_BeginRequestBody body;

    /* 两个字节保存期望 php-fpm 扮演的角色 */
    body.roleB1 = (unsigned char)((role >> 8) & 0xff);
    body.roleB0 = (unsigned char)(role & 0xff);

    /* 大于0长连接,否则短连接 */
    body.flags = (unsigned char)((keepConnection) ? FCGI_KEEP_CONN : 0);

    bzero(&body.reserved, sizeof(body.reserved));

    return body;
}

int sendStartRequestRecord(FastCgi_t *c)
{
    int rc;
    FCGI_BeginRequestRecord beginRecord;

    beginRecord.header = 
    makeHeader(FCGI_BEGIN_REQUEST, c->requestId_, sizeof(beginRecord.body), 0);
    beginRecord.body = makeBeginRequestBody(FCGI_RESPONDER, 0);

    rc = write(c->sockfd_, (char *)&beginRecord, sizeof(beginRecord));
    assert(rc == sizeof(beginRecord));

    return 1;
}

type == FCGI_END_REQUEST 3  //请求处理完毕,正常结束(fastcgi->web)

8字节固定格式:

typedef struct
{
    unsigned char appStatusB3; 
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
 //合起来表示CGI程序的结束状态,0为正常,此处是一个网络字节序,需要手动转换

    unsigned char protocolStatus; //fastcgi协议状态,如下:
    unsigned char reserved[3];
} FCGI_EndRequestBody; //结束消息体
//几种结束状态
#define FCGI_REQUEST_COMPLETE 0 //正常结束

#define FCGI_CANT_MPX_XONN 1    //拒绝新请求,单线程
#define FCGI_OVERLOADED 2       //拒绝新请求,应用负载了
#define FCGI_UNKNOWN_ROLE 3     //webserver 指定了一个应用不能识别的角色

type == FCGI_PARAMS 4 传递参数,表明消息中包含的数据为某个name-value对(web->fastcgi)

php-fpm传递name-value对,可传递自己的,也可以传递fastcgi提供的.fasttcgi提供的name主要有如下这些:


name名 含义
*SCRIPT_NAME 要执行的CGI程序的名字
*REQUEST_METHOD 信息传输方式(GET/POST/PUT等)
*QUERY_STRING 查询字符串
CONTENT_LENGTH 向CGI标准输入传递的信息长度(应当等于FCGI_STDIN消息contentLength字段之和)
CONTENT_TYPE 向CGI标准输入传递的信息类型

其余更多的可参考娄神的boke

type == FCGI_STDIN 5  POST 内容传递,从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm时,这种消息的type就得设为5(web->fastcgi)

***实例3 : 完成 post 请求

#include <stdio.h>
#include <stdlib.h>
#include "fcgi.h"
#include <sys/types.h>
#include <sys/socket.h>

int main()
{
    FastCgi_t *c;
    c = (FastCgi_t *)malloc(sizeof(FastCgi_t));
    FastCgi_init(c);
    setRequestId(c, 1); /*1 用来表明此消息为请求开始的第一个消息*/
    startConnect(c);
    sendStartRequestRecord(c);

    sendParams(c, "SCRIPT_FILENAME", "/home/Shengxi-Liu/WebServer/wwwroot/php/Operation.php");
    sendParams(c, "REQUEST_METHOD", "POST");
    sendParams(c, "CONTENT_LENGTH", "17"); // 17 为body的长度 !!!!
    sendParams(c, "CONTENT_TYPE", "application/x-www-form-urlencoded");

    sendEndRequestRecord(c);

    /*FCGI_Header makeHeader(int type, int requestId,
                       int contentLength, int paddingLength)*/
    //设置type==5,为了发 body
    FCGI_Header t = makeHeader(FCGI_STDIN, c->requestId_, 17, 0); // 17 为body的长度 !!!!
    send(c->sockfd_, &t, sizeof(t), 0);

    /*发送正式的 body */
    send(c->sockfd_, "a=20&b=10&c=5&d=6", 17, 0); // 17 为body的长度 !!!!

    //制造头告诉 body 结束 
    FCGI_Header endHeader;
    endHeader = makeHeader(FCGI_STDIN, c->requestId_, 0, 0);
    send(c->sockfd_, &endHeader, sizeof(endHeader), 0);

    printf("end-----\n");

    readFromPhp(c);

    FastCgi_finit(c);
    return 0;
}

Operation.php文件

<html>
<body>
<?php
        #预定义的 $_REQUEST 变量包含了 $_GET、$_POST 和 $_COOKIE 的内容。
        #$_REQUEST 变量可用来收集通过 GET 和 POST 方法发送的表单数据。
    $a=$_REQUEST["a"];
    $b=$_REQUEST["b"];
    $c=$_REQUEST["c"];
    $d=$_REQUEST["d"];
    $result =($a-$b)+($c*$d);

    echo  $a.' - '.$b. ' + ' .$c. ' * ' .$d. " = $result"
    // echo '1';
    // var_dump($_REQUEST);
    // echo $a;
?>
</body>
</html>

运行截图:
在这里插入图片描述

***实例4 : 完成简单 get 请求

见:csdn1

***实例5: 完成带参数 get 请求

#include <stdio.h>
#include <stdlib.h>
#include "fcgi.h"
#include <sys/types.h>
#include <sys/socket.h>

int main()
{
    FastCgi_t *c;
    c = (FastCgi_t *)malloc(sizeof(FastCgi_t));
    FastCgi_init(c);
    setRequestId(c, 1); /*1 用来表明此消息为请求开始的第一个消息*/
    startConnect(c);
    sendStartRequestRecord(c);

    sendParams(c, "SCRIPT_FILENAME", "/home/Shengxi-Liu/WebServer/wwwroot/php/Operation.php");
    sendParams(c, "REQUEST_METHOD", "GET");
    sendParams(c, "CONTENT_LENGTH", "0"); // 0 表示没有 body
    sendParams(c, "CONTENT_TYPE", "text/html");
    sendParams(c, "QUERY_STRING", "a=20&b=10&c=5&d=6");

    sendEndRequestRecord(c); //告诉cgi程序 head 有多长
	/*
	int sendEndRequestRecord(FastCgi_t *c)
	{
	    int rc;
	
	    FCGI_Header endHeader;
	    endHeader = makeHeader(FCGI_PARAMS, c->requestId_, 0, 0);
	
	    rc = write(c->sockfd_, (char *)&endHeader, FCGI_HEADER_LEN);
	    assert(rc == FCGI_HEADER_LEN);
	
	    return 1;
	}

*/
    printf("end-----\n");

    readFromPhp(c);

    FastCgi_finit(c);
    return 0;
}

运行截图:
在这里插入图片描述

需要注意的是查询字符串(QUERY_STRING)必须放在sendEndRequestRecord(c);函数之前,想一想http协议是怎样处理带参数的get就要知道了.....

(3) 一个完整消息称为一个 record ,我们每次发送的单位就是record。通过上面的介绍,我们可以总结出常见的记录格式


type record
1 header(消息头) + 开始请求体(8字节)
3 header + 结束请求体(8字节)
4 header + name-value长度(8字节) + 具体的name-value
5,6,7 header + 具体内容

最后,附上我的webserver项目地址,里边含有使用到的fastcgi库.求star,求fork,哈哈哈哈...

猜你喜欢

转载自blog.csdn.net/liushengxi_root/article/details/84976353