PHP 進階之路 - 深刻理解 FastCGI 協議以及在 PHP 中的實現

廣告

不少工程師在工做1~3年的時候最容易遇到瓶頸,不知道本身應該學習什麼,面試老是吃閉門羹。那麼 PHP 後面應該怎麼學呢?安利一波個人系列直播 《PHP 進階之路》php

在討論 FastCGI 以前,不得不說傳統的 CGI 的工做原理,同時應該大概瞭解 CGI 1.1 協議html

傳統 CGI 工做原理分析

客戶端訪問某個 URL 地址以後,經過 GET/POST/PUT 等方式提交數據,並經過 HTTP 協議向 Web 服務器發出請求,服務器端的 HTTP Daemon(守護進程)將 HTTP 請求裏描述的信息經過標準輸入 stdin 和環境變量(environment variable)傳遞給主頁指定的 CGI 程序,並啓動此應用程序進行處理(包括對數據庫的處理),處理結果經過標準輸出 stdout 返回給 HTTP Daemon 守護進程,再由 HTTP Daemon 進程經過 HTTP 協議返回給客戶端。程序員

上面的這段話理解可能仍是比較抽象,下面咱們就經過一次GET請求爲例進行詳細說明。1478757541407927.png
下面用代碼來實現圖中表述的功能。Web 服務器啓動一個 socket 監聽服務,而後在本地執行 CGI 程序。後面有比較詳細的代碼解讀。web

Web 服務器代碼

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
    
#define SERV_PORT 9003
 
char* str_join(char *str1, char *str2);
char* html_response(char *res, char *buf);
   
int main(void)
{
    int lfd, cfd;
    struct sockaddr_in serv_addr,clin_addr;
    socklen_t clin_len;
    char buf[1024],web_result[1024];
    int len;
    FILE *cin;
  
    if((lfd = socket(AF_INET,SOCK_STREAM,0)) == -1){
        perror("create socket failed");
        exit(1);
    }
      
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(SERV_PORT);
  
    if(bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        perror("bind error");
        exit(1);
    }
  
    if(listen(lfd, 128) == -1)
    {
        perror("listen error");
        exit(1);
    }
     
    signal(SIGCLD,SIG_IGN);
   
    while(1)
    {
        clin_len = sizeof(clin_addr);
        if ((cfd = accept(lfd, (struct sockaddr *)&clin_addr, &clin_len)) == -1)
        {
            perror("接收錯誤\n");
            continue;
        }
 
        cin = fdopen(cfd, "r");
        setbuf(cin, (char *)0);
        fgets(buf,1024,cin); //讀取第一行
        printf("\n%s", buf);
 
        //============================ cgi 環境變量設置演示 ============================
         
        // 例如 "GET /user.cgi?id=1 HTTP/1.1";
 
        char *delim = " ";
        char *p;
        char *method, *filename, *query_string;
        char *query_string_pre = "QUERY_STRING=";
 
        method = strtok(buf,delim);         // GET
        p = strtok(NULL,delim);             // /user.cgi?id=1 
        filename = strtok(p,"?");           // /user.cgi
         
        if (strcmp(filename,"/favicon.ico") == 0)
        {
            continue;
        }
 
        query_string = strtok(NULL,"?");    // id=1
        putenv(str_join(query_string_pre,query_string));
 
        //============================ cgi 環境變量設置演示 ============================
 
        int pid = fork();
  
        if (pid > 0)
        {
            close(cfd);
        }
        else if (pid == 0)
        {
            close(lfd);
            FILE *stream = popen(str_join(".",filename),"r");
            fread(buf,sizeof(char),sizeof(buf),stream);
            html_response(web_result,buf);
            write(cfd,web_result,sizeof(web_result));
            pclose(stream);
            close(cfd);
            exit(0);
        }
        else
        {
            perror("fork error");
            exit(1);
        }
    }
   
    close(lfd);
       
    return 0;
}
 
char* str_join(char *str1, char *str2)
{
    char *result = malloc(strlen(str1)+strlen(str2)+1);
    if (result == NULL) exit (1);
    strcpy(result, str1);
    strcat(result, str2);
   
    return result;
}
 
char* html_response(char *res, char *buf)
{
    char *html_response_template = "HTTP/1.1 200 OK\r\nContent-Type:text/html\r\nContent-Length: %d\r\nServer: mengkang\r\n\r\n%s";
 
    sprintf(res,html_response_template,strlen(buf),buf);
     
    return res;
}

如上代碼中的重點:

  • 66~81行找到CGI程序的相對路徑(咱們爲了簡單,直接將其根目錄定義爲Web程序的當前目錄),這樣就能夠在子進程中執行 CGI 程序了;同時設置環境變量,方便CGI程序運行時讀取;面試

  • 94~95行將 CGI 程序的標準輸出結果寫入 Web 服務器守護進程的緩存中;數據庫

  • 97行則將包裝後的 html 結果寫入客戶端 socket 描述符,返回給鏈接Web服務器的客戶端。segmentfault

CGI 程序(user.c)

#include <stdio.h>
#include <stdlib.h>
// 經過獲取的 id 查詢用戶的信息
int main(void){
 
    //============================ 模擬數據庫 ============================
    typedef struct 
    {
        int  id;
        char *username;
        int  age;
    } user;
 
    user users[] = {
        {},
        {
            1,
            "mengkang.zhou",
            18
        }
    };
    //============================ 模擬數據庫 ============================
 
 
    char *query_string;
    int id;
 
    query_string = getenv("QUERY_STRING");
     
    if (query_string == NULL)
    {
        printf("沒有輸入數據");
    } else if (sscanf(query_string,"id=%d",&id) != 1)
    {
        printf("沒有輸入id");
    } else
    {
        printf("用戶信息查詢<br>學號: %d<br>姓名: %s<br>年齡: %d",id,users[id].username,users[id].age);
    }
     
    return 0;
}

將上面的 CGI 程序編譯成gcc user.c -o user.cgi,放在上面web程序的同級目錄。
代碼中的第28行,從環境變量中讀取前面在Web服務器守護進程中設置的環境變量,是咱們演示的重點。後端

FastCGI 工做原理分析

相對於 CGI/1.1 規範在 Web 服務器在本地 fork 一個子進程執行 CGI 程序,填充 CGI 預約義的環境變量,放入系統環境變量,把 HTTP body 體的 content 經過標準輸入傳入子進程,處理完畢以後經過標準輸出返回給 Web 服務器。FastCGI 的核心則是取締傳統的 fork-and-execute 方式,減小每次啓動的巨大開銷(後面以 PHP 爲例說明),以常駐的方式來處理請求。api

FastCGI 工做流程以下:緩存

  1. FastCGI 進程管理器自身初始化,啓動多個 CGI 解釋器進程,並等待來自 Web Server 的鏈接。

  2. Web 服務器與 FastCGI 進程管理器進行 Socket 通訊,經過 FastCGI 協議發送 CGI 環境變量和標準輸入數據給 CGI 解釋器進程。

  3. CGI 解釋器進程完成處理後將標準輸出和錯誤信息從同一鏈接返回 Web Server。

  4. CGI 解釋器進程接着等待並處理來自 Web Server 的下一個鏈接。

1478757572486219.png

FastCGI 與傳統 CGI 模式的區別之一則是 Web 服務器不是直接執行 CGI 程序了,而是經過 socket 與 FastCGI 響應器(FastCGI 進程管理器)進行交互,Web 服務器須要將 CGI 接口數據封裝在遵循 FastCGI 協議包中發送給 FastCGI 響應器程序。正是因爲 FastCGI 進程管理器是基於 socket 通訊的,因此也是分佈式的,Web服務器和CGI響應器服務器分開部署。

再囉嗦一句,FastCGI 是一種協議,它是創建在CGI/1.1基礎之上的,把CGI/1.1裏面的要傳遞的數據經過FastCGI協議定義的順序、格式進行傳遞。

準備工做

可能上面的內容理解起來仍是很抽象,這是因爲第一對FastCGI協議尚未一個大概的認識,第二沒有實際代碼的學習。因此須要預先學習下 FastCGI 協議的內容,不必定須要徹底看懂,可大體瞭解以後,看完本篇再結合着學習理解消化。

http://www.fastcgi.com/devkit... (英文原版)
http://andylin02.iteye.com/bl... (中文版)

FastCGI 協議分析

下面結合 PHP 的 FastCGI 的代碼進行分析,不做特殊說明如下代碼均來自於 PHP 源碼。

FastCGI 消息類型

FastCGI 將傳輸的消息作了不少類型的劃分,其結構體定義以下:

typedef enum _fcgi_request_type {
    FCGI_BEGIN_REQUEST      =  1, /* [in]                              */
    FCGI_ABORT_REQUEST      =  2, /* [in]  (not supported)             */
    FCGI_END_REQUEST        =  3, /* [out]                             */
    FCGI_PARAMS             =  4, /* [in]  environment variables       */
    FCGI_STDIN              =  5, /* [in]  post data                   */
    FCGI_STDOUT             =  6, /* [out] response                    */
    FCGI_STDERR             =  7, /* [out] errors                      */
    FCGI_DATA               =  8, /* [in]  filter data (not supported) */
    FCGI_GET_VALUES         =  9, /* [in]                              */
    FCGI_GET_VALUES_RESULT  = 10  /* [out]                             */
} fcgi_request_type;

消息的發送順序

下圖是一個簡單的消息傳遞流程

1478757581275137.png

最早發送的是FCGI_BEGIN_REQUEST,而後是FCGI_PARAMSFCGI_STDIN,因爲每一個消息頭(下面將詳細說明)裏面可以承載的最大長度是65535,因此這兩種類型的消息不必定只發送一次,有可能連續發送屢次。

FastCGI 響應體處理完畢以後,將發送FCGI_STDOUTFCGI_STDERR,同理也可能屢次連續發送。最後以FCGI_END_REQUEST表示請求的結束。

須要注意的一點,FCGI_BEGIN_REQUESTFCGI_END_REQUEST分別標識着請求的開始和結束,與整個協議息息相關,因此他們的消息體的內容也是協議的一部分,所以也會有相應的結構體與之對應(後面會詳細說明)。而環境變量、標準輸入、標準輸出、錯誤輸出,這些都是業務相關,與協議無關,因此他們的消息體的內容則無結構體對應。

因爲整個消息是二進制連續傳遞的,因此必須定義一個統一的結構的消息頭,這樣以便讀取每一個消息的消息體,方便消息的切割。這在網絡通信中是很是常見的一種手段。

FastCGI 消息頭

如上,FastCGI 消息分10種消息類型,有的是輸入有的是輸出。而全部的消息都以一個消息頭開始。其結構體定義以下:

typedef struct _fcgi_header {
    unsigned char version;
    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;
    unsigned char reserved;
} fcgi_header;

字段解釋下:
version標識FastCGI協議版本。
type 標識FastCGI記錄類型,也就是記錄執行的通常職能。
requestId標識記錄所屬的FastCGI請求。
contentLength記錄的contentData組件的字節數。
關於上面的xxB1xxB0的協議說明:當兩個相鄰的結構組件除了後綴「B1」和「B0」以外命名相同時,它表示這兩個組件可視爲估值爲B1<<8 + B0的單個數字。該單個數字的名字是這些組件減去後綴的名字。這個約定概括了一個由超過兩個字節表示的數字的處理方式。

好比協議頭中requestIdcontentLength表示的最大值就是65535

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

int main()
{
   unsigned char requestIdB1 = UCHAR_MAX;
   unsigned char requestIdB0 = UCHAR_MAX;
   printf("%d\n", (requestIdB1 << 8) + requestIdB0); // 65535
}

你可能會想到若是一個消息體長度超過65535怎麼辦,則分割爲多個相同類型的消息發送便可。

FCGI_BEGIN_REQUEST 的定義

typedef struct _fcgi_begin_request {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} fcgi_begin_request;

字段解釋

role表示Web服務器指望應用扮演的角色。分爲三個角色(而咱們這裏討論的狀況通常都是響應器角色)

typedef enum _fcgi_role {
    FCGI_RESPONDER    = 1,
    FCGI_AUTHORIZER    = 2,
    FCGI_FILTER        = 3
} fcgi_role;

FCGI_BEGIN_REQUEST中的flags組件包含一個控制線路關閉的位:flags & FCGI_KEEP_CONN:若是爲0,則應用在對本次請求響應後關閉線路。若是非0,應用在對本次請求響應後不會關閉線路;Web服務器爲線路保持響應性。

FCGI_END_REQUEST 的定義

typedef struct _fcgi_end_request {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} fcgi_end_request;

字段解釋
appStatus組件是應用級別的狀態碼。
protocolStatus組件是協議級別的狀態碼;protocolStatus的值多是:

FCGI_REQUEST_COMPLETE:請求的正常結束。
FCGI_CANT_MPX_CONN:拒絕新請求。這發生在Web服務器經過一條線路嚮應用發送併發的請求時,後者被設計爲每條線路每次處理一個請求。
FCGI_OVERLOADED:拒絕新請求。這發生在應用用完某些資源時,例如數據庫鏈接。
FCGI_UNKNOWN_ROLE:拒絕新請求。這發生在Web服務器指定了一個應用不能識別的角色時。

protocolStatus在 PHP 中的定義以下

typedef enum _fcgi_protocol_status {
    FCGI_REQUEST_COMPLETE    = 0,
    FCGI_CANT_MPX_CONN        = 1,
    FCGI_OVERLOADED            = 2,
    FCGI_UNKNOWN_ROLE        = 3
} dcgi_protocol_status;

須要注意dcgi_protocol_statusfcgi_role各個元素的值都是 FastCGI 協議裏定義好的,而非 PHP 自定義的。

消息通信樣例

爲了簡單的表示,消息頭只顯示消息的類型和消息的 id,其餘字段都不予以顯示。下面的例子來自於官網

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_STDIN,           1, "quantity=100&item=3047936"}
{FCGI_STDOUT,          1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
{FCGI_END_REQUEST,     1, {0, FCGI_REQUEST_COMPLETE}}

配合上面各個結構體,則能夠大體想到 FastCGI 響應器的解析和響應流程:

首先讀取消息頭,獲得其類型爲FCGI_BEGIN_REQUEST,而後解析其消息體,得知其須要的角色就是FCGI_RESPONDERflag爲0,表示請求結束後關閉線路。而後解析第二段消息,得知其消息類型爲FCGI_PARAMS,而後直接將消息體裏的內容以回車符切割後存入環境變量。與之相似,處理完畢以後,則返回了FCGI_STDOUT消息體和FCGI_END_REQUEST消息體供 Web 服務器解析。

PHP 中的 FastCGI 的實現

下面對代碼的解讀筆記只是我我的知識的一個梳理提煉,若有勘誤,請你們指出。對不熟悉該代碼的同窗來講多是一個引導,初步認識,若是以爲很模糊不清晰,那麼仍是須要本身逐行去閱讀。

php-src/sapi/cgi/cgi_main.c爲例進行分析說明,假設開發環境爲 unix 環境。main 函數中一些變量的定義,以及 sapi 的初始化,咱們就不討論在這裏討論了,只說明關於 FastCGI 相關的內容。

1.開啓一個 socket 監聽服務

fcgi_fd = fcgi_listen(bindpath, 128);

從這裏開始監聽,而fcgi_listen函數裏面則完成 socket 服務前三步socket,bind,listen

2.初始化請求對象

fcgi_request對象分配內存,綁定監聽的 socket 套接字。

fcgi_init_request(&request, fcgi_fd);

整個請求從輸入到返回,都圍繞着fcgi_request結構體對象在進行。

typedef struct _fcgi_request {
    int            listen_socket;
    int            fd;
    int            id;
    int            keep;
    int            closed;

    int            in_len;
    int            in_pad;

    fcgi_header   *out_hdr;
    unsigned char *out_pos;
    unsigned char  out_buf[1024*8];
    unsigned char  reserved[sizeof(fcgi_end_request_rec)];

    HashTable     *env;
} fcgi_request;

3.建立多個 CGI 解析器子進程

這裏子進程的個數默認是0,從配置文件中讀取設置到環境變量,而後在程序中讀取,而後建立指定數目的子進程來等待處理 Web 服務器的請求。

if (getenv("PHP_FCGI_CHILDREN")) {
    char * children_str = getenv("PHP_FCGI_CHILDREN");
    children = atoi(children_str);
    ...
}

do {
    pid = fork();
    switch (pid) {
    case 0:
        parent = 0; // 將子進程中的父進程標識改成0,防止循環 fork

        /* don't catch our signals */
        sigaction(SIGTERM, &old_term, 0);
        sigaction(SIGQUIT, &old_quit, 0);
        sigaction(SIGINT,  &old_int,  0);
        break;
    case -1:
        perror("php (pre-forking)");
        exit(1);
        break;
    default:
        /* Fine */
        running++;
        break;
    }
} while (parent && (running < children));

4.在子進程中接收請求

到這裏一切都仍是 socket 的服務的套路。接受請求,而後調用了fcgi_read_request

fcgi_accept_request(&request)
int fcgi_accept_request(fcgi_request *req)
{
    int listen_socket = req->listen_socket;
    sa_t sa;
    socklen_t len = sizeof(sa);
    req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);

    ...

    if (req->fd >= 0) {
        // 採用多路複用的機制
        struct pollfd fds;
        int ret;

        fds.fd = req->fd;
        fds.events = POLLIN;
        fds.revents = 0;
        do {
            errno = 0;
            ret = poll(&fds, 1, 5000);
        } while (ret < 0 && errno == EINTR);
        if (ret > 0 && (fds.revents & POLLIN)) {
            break;
        }
        // 僅僅是關閉 socket 鏈接,不清空 req->env
        fcgi_close(req, 1, 0);
    }

    ...

    if (fcgi_read_request(req)) {
        return req->fd;
    }
}

而且把request放入全局變量sapi_globals.server_context,這點很重要,方便了在其餘地方對請求的調用。

SG(server_context) = (void *) &request;

5.讀取數據

下面的代碼刪除一些異常狀況的處理,只顯示了正常狀況下執行順序。
fcgi_read_request中則完成咱們在消息通信樣例中的消息讀取,而其中不少的len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;操做,已經在前面的FastCGI 消息頭中解釋過了。
這裏是解析 FastCGI 協議的關鍵。

static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count)
{
    int    ret;
    size_t n = 0;

    do {
        errno = 0;
        ret = read(req->fd, ((char*)buf)+n, count-n);
        n += ret;
    } while (n != count);
    return n;
}
static int fcgi_read_request(fcgi_request *req)
{
    ...

    if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
        return 0;
    }

    len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
    padding = hdr.paddingLength;

    req->id = (hdr.requestIdB1 << 8) + hdr.requestIdB0;

    if (hdr.type == FCGI_BEGIN_REQUEST && len == sizeof(fcgi_begin_request)) {
        char *val;

        if (safe_read(req, buf, len+padding) != len+padding) {
            return 0;
        }

        req->keep = (((fcgi_begin_request*)buf)->flags & FCGI_KEEP_CONN);
        
        switch ((((fcgi_begin_request*)buf)->roleB1 << 8) + ((fcgi_begin_request*)buf)->roleB0) {
            case FCGI_RESPONDER:
                val = estrdup("RESPONDER");
                zend_hash_update(req->env, "FCGI_ROLE", sizeof("FCGI_ROLE"), &val, sizeof(char*), NULL);
                break;
            ...
            default:
                return 0;
        }

        if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
            return 0;
        }

        len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
        padding = hdr.paddingLength;

        while (hdr.type == FCGI_PARAMS && len > 0) {
            if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
                req->keep = 0;
                return 0;
            }
            len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
            padding = hdr.paddingLength;
        }
        
        ...
    }
}

6.執行腳本

假設這次請求爲PHP_MODE_STANDARD則會調用php_execute_script執行PHP文件。這裏就不展開了。

7.結束請求

fcgi_finish_request(&request, 1);
int fcgi_finish_request(fcgi_request *req, int force_close)
{
    int ret = 1;

    if (req->fd >= 0) {
        if (!req->closed) {
            ret = fcgi_flush(req, 1);
            req->closed = 1;
        }
        fcgi_close(req, force_close, 1);
    }
    return ret;
}

fcgi_finish_request中調用fcgi_flushfcgi_flush中封裝一個FCGI_END_REQUEST消息體,再經過safe_write寫入 socket 鏈接的客戶端描述符。

8.標準輸入標準輸出的處理

標準輸入和標準輸出在上面沒有一塊兒討論,實際在cgi_sapi_module結構體中有定義,可是cgi_sapi_module這個sapi_module_struct結構體與其餘代碼耦合太多,我本身也沒深刻的理解,這裏簡單作下比較,但願其餘網友予以指點、補充。

cgi_sapi_module中定義了sapi_cgi_read_post來處理POST數據的讀取.

while (read_bytes < count_bytes) {
    fcgi_request *request = (fcgi_request*) SG(server_context);
    tmp_read_bytes = fcgi_read(request, buffer + read_bytes, count_bytes - read_bytes);
    read_bytes += tmp_read_bytes;
}

fcgi_read中則對FCGI_STDIN的數據進行讀取。
同時cgi_sapi_module中定義了sapi_cgibin_ub_write來接管輸出處理,而其中又調用了sapi_cgibin_single_write,最後實現了FCGI_STDOUT FastCGI 數據包的封裝.

fcgi_write(request, FCGI_STDOUT, str, str_length);

寫在最後

把 FastCGI 的知識學習理解的過程作了這樣一篇筆記,把本身理解的內容(自我認爲)有條理地寫出來,可以讓別人比較容易看明白也是一件不挺不容易的事。同時也讓本身對這個知識點的理解又深刻了一層。對 PHP 代碼學習理解中還有不少困惑的地方還須要我本身後期慢慢消化和理解。

本文都是本身的一些理解,水平有限,若有勘誤,但願你們予以指正。

本文已合併到 http://www.php-internals.com/...
個人微博 http://weibo.com/zmkang 對本文有問題能夠和我溝通

堅持看完本的都是老司機,說實話,後面有些太枯燥了!若是能把每一個知識點真正理解消化,絕對獲益良多。

加個硬廣告 PHP 程序員技能包裏該來點硬貨了!

最近老鐵開了直播,歡迎來捧場!

相關文章
相關標籤/搜索