1. 程式人生 > >Fastcgi 協議解析及 get&post 使用例項

Fastcgi 協議解析及 get&post 使用例項

前言:

基於:

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,哈哈哈哈...