摘要:與傳統(tǒng)模式的區(qū)別之一則是服務(wù)器不是直接執(zhí)行程序了,而是通過(guò)與響應(yīng)器進(jìn)程管理器進(jìn)行交互,服務(wù)器需要將接口數(shù)據(jù)封裝在遵循協(xié)議包中發(fā)送給響應(yīng)器程序。正是由于進(jìn)程管理器是基于通信的,所以也是分布式的,服務(wù)器和響應(yīng)器服務(wù)器分開(kāi)部署。
廣告
很多工程師在工作1~3年的時(shí)候最容易遇到瓶頸,不知道自己應(yīng)該學(xué)習(xí)什么,面試總是吃閉門(mén)羹。那么 PHP 后面應(yīng)該怎么學(xué)呢?安利一波我的系列直播 《PHP 進(jìn)階之路》
在討論 FastCGI 之前,不得不說(shuō)傳統(tǒng)的 CGI 的工作原理,同時(shí)應(yīng)該大概了解 CGI 1.1 協(xié)議
傳統(tǒng) CGI 工作原理分析客戶(hù)端訪問(wèn)某個(gè) URL 地址之后,通過(guò) GET/POST/PUT 等方式提交數(shù)據(jù),并通過(guò) HTTP 協(xié)議向 Web 服務(wù)器發(fā)出請(qǐng)求,服務(wù)器端的 HTTP Daemon(守護(hù)進(jìn)程)將 HTTP 請(qǐng)求里描述的信息通過(guò)標(biāo)準(zhǔn)輸入 stdin 和環(huán)境變量(environment variable)傳遞給主頁(yè)指定的 CGI 程序,并啟動(dòng)此應(yīng)用程序進(jìn)行處理(包括對(duì)數(shù)據(jù)庫(kù)的處理),處理結(jié)果通過(guò)標(biāo)準(zhǔn)輸出 stdout 返回給 HTTP Daemon 守護(hù)進(jìn)程,再由 HTTP Daemon 進(jìn)程通過(guò) HTTP 協(xié)議返回給客戶(hù)端。
上面的這段話(huà)理解可能還是比較抽象,下面我們就通過(guò)一次GET請(qǐng)求為例進(jìn)行詳細(xì)說(shuō)明。
下面用代碼來(lái)實(shí)現(xiàn)圖中表述的功能。Web 服務(wù)器啟動(dòng)一個(gè) socket 監(jiān)聽(tīng)服務(wù),然后在本地執(zhí)行 CGI 程序。后面有比較詳細(xì)的代碼解讀。
#include如上代碼中的重點(diǎn):#include #include #include #include #include #include #include #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("接收錯(cuò)誤 "); continue; } cin = fdopen(cfd, "r"); setbuf(cin, (char *)0); fgets(buf,1024,cin); //讀取第一行 printf(" %s", buf); //============================ cgi 環(huán)境變量設(shè)置演示 ============================ // 例如 "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 環(huán)境變量設(shè)置演示 ============================ 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 Content-Type:text/html Content-Length: %d Server: mengkang %s"; sprintf(res,html_response_template,strlen(buf),buf); return res; }
66~81行找到CGI程序的相對(duì)路徑(我們?yōu)榱撕?jiǎn)單,直接將其根目錄定義為Web程序的當(dāng)前目錄),這樣就可以在子進(jìn)程中執(zhí)行 CGI 程序了;同時(shí)設(shè)置環(huán)境變量,方便CGI程序運(yùn)行時(shí)讀??;
94~95行將 CGI 程序的標(biāo)準(zhǔn)輸出結(jié)果寫(xiě)入 Web 服務(wù)器守護(hù)進(jìn)程的緩存中;
97行則將包裝后的 html 結(jié)果寫(xiě)入客戶(hù)端 socket 描述符,返回給連接Web服務(wù)器的客戶(hù)端。
CGI 程序(user.c)#include#include // 通過(guò)獲取的 id 查詢(xún)用戶(hù)的信息 int main(void){ //============================ 模擬數(shù)據(jù)庫(kù) ============================ typedef struct { int id; char *username; int age; } user; user users[] = { {}, { 1, "mengkang.zhou", 18 } }; //============================ 模擬數(shù)據(jù)庫(kù) ============================ char *query_string; int id; query_string = getenv("QUERY_STRING"); if (query_string == NULL) { printf("沒(méi)有輸入數(shù)據(jù)"); } else if (sscanf(query_string,"id=%d",&id) != 1) { printf("沒(méi)有輸入id"); } else { printf("用戶(hù)信息查詢(xún)
學(xué)號(hào): %d
姓名: %s
年齡: %d",id,users[id].username,users[id].age); } return 0; }
將上面的 CGI 程序編譯成gcc user.c -o user.cgi,放在上面web程序的同級(jí)目錄。
代碼中的第28行,從環(huán)境變量中讀取前面在Web服務(wù)器守護(hù)進(jìn)程中設(shè)置的環(huán)境變量,是我們演示的重點(diǎn)。
相對(duì)于 CGI/1.1 規(guī)范在 Web 服務(wù)器在本地 fork 一個(gè)子進(jìn)程執(zhí)行 CGI 程序,填充 CGI 預(yù)定義的環(huán)境變量,放入系統(tǒng)環(huán)境變量,把 HTTP body 體的 content 通過(guò)標(biāo)準(zhǔn)輸入傳入子進(jìn)程,處理完畢之后通過(guò)標(biāo)準(zhǔn)輸出返回給 Web 服務(wù)器。FastCGI 的核心則是取締傳統(tǒng)的 fork-and-execute 方式,減少每次啟動(dòng)的巨大開(kāi)銷(xiāo)(后面以 PHP 為例說(shuō)明),以常駐的方式來(lái)處理請(qǐng)求。
FastCGI 工作流程如下:
FastCGI 進(jìn)程管理器自身初始化,啟動(dòng)多個(gè) CGI 解釋器進(jìn)程,并等待來(lái)自 Web Server 的連接。
Web 服務(wù)器與 FastCGI 進(jìn)程管理器進(jìn)行 Socket 通信,通過(guò) FastCGI 協(xié)議發(fā)送 CGI 環(huán)境變量和標(biāo)準(zhǔn)輸入數(shù)據(jù)給 CGI 解釋器進(jìn)程。
CGI 解釋器進(jìn)程完成處理后將標(biāo)準(zhǔn)輸出和錯(cuò)誤信息從同一連接返回 Web Server。
CGI 解釋器進(jìn)程接著等待并處理來(lái)自 Web Server 的下一個(gè)連接。
FastCGI 與傳統(tǒng) CGI 模式的區(qū)別之一則是 Web 服務(wù)器不是直接執(zhí)行 CGI 程序了,而是通過(guò) socket 與 FastCGI 響應(yīng)器(FastCGI 進(jìn)程管理器)進(jìn)行交互,Web 服務(wù)器需要將 CGI 接口數(shù)據(jù)封裝在遵循 FastCGI 協(xié)議包中發(fā)送給 FastCGI 響應(yīng)器程序。正是由于 FastCGI 進(jìn)程管理器是基于 socket 通信的,所以也是分布式的,Web服務(wù)器和CGI響應(yīng)器服務(wù)器分開(kāi)部署。
再啰嗦一句,F(xiàn)astCGI 是一種協(xié)議,它是建立在CGI/1.1基礎(chǔ)之上的,把CGI/1.1里面的要傳遞的數(shù)據(jù)通過(guò)FastCGI協(xié)議定義的順序、格式進(jìn)行傳遞。
準(zhǔn)備工作可能上面的內(nèi)容理解起來(lái)還是很抽象,這是由于第一對(duì)FastCGI協(xié)議還沒(méi)有一個(gè)大概的認(rèn)識(shí),第二沒(méi)有實(shí)際代碼的學(xué)習(xí)。所以需要預(yù)先學(xué)習(xí)下 FastCGI 協(xié)議的內(nèi)容,不一定需要完全看懂,可大致了解之后,看完本篇再結(jié)合著學(xué)習(xí)理解消化。
FastCGI 協(xié)議分析http://www.fastcgi.com/devkit... (英文原版)
http://andylin02.iteye.com/bl... (中文版)
下面結(jié)合 PHP 的 FastCGI 的代碼進(jìn)行分析,不作特殊說(shuō)明以下代碼均來(lái)自于 PHP 源碼。
FastCGI 消息類(lèi)型FastCGI 將傳輸?shù)南⒆隽撕芏囝?lèi)型的劃分,其結(jié)構(gòu)體定義如下:
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;消息的發(fā)送順序
下圖是一個(gè)簡(jiǎn)單的消息傳遞流程
最先發(fā)送的是FCGI_BEGIN_REQUEST,然后是FCGI_PARAMS和FCGI_STDIN,由于每個(gè)消息頭(下面將詳細(xì)說(shuō)明)里面能夠承載的最大長(zhǎng)度是65535,所以這兩種類(lèi)型的消息不一定只發(fā)送一次,有可能連續(xù)發(fā)送多次。
FastCGI 響應(yīng)體處理完畢之后,將發(fā)送FCGI_STDOUT、FCGI_STDERR,同理也可能多次連續(xù)發(fā)送。最后以FCGI_END_REQUEST表示請(qǐng)求的結(jié)束。
需要注意的一點(diǎn),FCGI_BEGIN_REQUEST和FCGI_END_REQUEST分別標(biāo)識(shí)著請(qǐng)求的開(kāi)始和結(jié)束,與整個(gè)協(xié)議息息相關(guān),所以他們的消息體的內(nèi)容也是協(xié)議的一部分,因此也會(huì)有相應(yīng)的結(jié)構(gòu)體與之對(duì)應(yīng)(后面會(huì)詳細(xì)說(shuō)明)。而環(huán)境變量、標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出、錯(cuò)誤輸出,這些都是業(yè)務(wù)相關(guān),與協(xié)議無(wú)關(guān),所以他們的消息體的內(nèi)容則無(wú)結(jié)構(gòu)體對(duì)應(yīng)。
由于整個(gè)消息是二進(jìn)制連續(xù)傳遞的,所以必須定義一個(gè)統(tǒng)一的結(jié)構(gòu)的消息頭,這樣以便讀取每個(gè)消息的消息體,方便消息的切割。這在網(wǎng)絡(luò)通訊中是非常常見(jiàn)的一種手段。
FastCGI 消息頭如上,F(xiàn)astCGI 消息分10種消息類(lèi)型,有的是輸入有的是輸出。而所有的消息都以一個(gè)消息頭開(kāi)始。其結(jié)構(gòu)體定義如下:
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標(biāo)識(shí)FastCGI協(xié)議版本。
type 標(biāo)識(shí)FastCGI記錄類(lèi)型,也就是記錄執(zhí)行的一般職能。
requestId標(biāo)識(shí)記錄所屬的FastCGI請(qǐng)求。
contentLength記錄的contentData組件的字節(jié)數(shù)。
關(guān)于上面的xxB1和xxB0的協(xié)議說(shuō)明:當(dāng)兩個(gè)相鄰的結(jié)構(gòu)組件除了后綴“B1”和“B0”之外命名相同時(shí),它表示這兩個(gè)組件可視為估值為B1<<8 + B0的單個(gè)數(shù)字。該單個(gè)數(shù)字的名字是這些組件減去后綴的名字。這個(gè)約定歸納了一個(gè)由超過(guò)兩個(gè)字節(jié)表示的數(shù)字的處理方式。
比如協(xié)議頭中requestId和contentLength表示的最大值就是65535
#include#include #include int main() { unsigned char requestIdB1 = UCHAR_MAX; unsigned char requestIdB0 = UCHAR_MAX; printf("%d ", (requestIdB1 << 8) + requestIdB0); // 65535 }
你可能會(huì)想到如果一個(gè)消息體長(zhǎng)度超過(guò)65535怎么辦,則分割為多個(gè)相同類(lèi)型的消息發(fā)送即可。
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表示W(wǎng)eb服務(wù)器期望應(yīng)用扮演的角色。分為三個(gè)角色(而我們這里討論的情況一般都是響應(yīng)器角色)
typedef enum _fcgi_role { FCGI_RESPONDER = 1, FCGI_AUTHORIZER = 2, FCGI_FILTER = 3 } fcgi_role;
而FCGI_BEGIN_REQUEST中的flags組件包含一個(gè)控制線路關(guān)閉的位:flags & FCGI_KEEP_CONN:如果為0,則應(yīng)用在對(duì)本次請(qǐng)求響應(yīng)后關(guān)閉線路。如果非0,應(yīng)用在對(duì)本次請(qǐng)求響應(yīng)后不會(huì)關(guān)閉線路;Web服務(wù)器為線路保持響應(yīng)性。
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組件是應(yīng)用級(jí)別的狀態(tài)碼。
protocolStatus組件是協(xié)議級(jí)別的狀態(tài)碼;protocolStatus的值可能是:
FCGI_REQUEST_COMPLETE:請(qǐng)求的正常結(jié)束。
FCGI_CANT_MPX_CONN:拒絕新請(qǐng)求。這發(fā)生在Web服務(wù)器通過(guò)一條線路向應(yīng)用發(fā)送并發(fā)的請(qǐng)求時(shí),后者被設(shè)計(jì)為每條線路每次處理一個(gè)請(qǐng)求。
FCGI_OVERLOADED:拒絕新請(qǐng)求。這發(fā)生在應(yīng)用用完某些資源時(shí),例如數(shù)據(jù)庫(kù)連接。
FCGI_UNKNOWN_ROLE:拒絕新請(qǐng)求。這發(fā)生在Web服務(wù)器指定了一個(gè)應(yīng)用不能識(shí)別的角色時(shí)。
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_status和fcgi_role各個(gè)元素的值都是 FastCGI 協(xié)議里定義好的,而非 PHP 自定義的。
消息通訊樣例為了簡(jiǎn)單的表示,消息頭只顯示消息的類(lèi)型和消息的 id,其他字段都不予以顯示。下面的例子來(lái)自于官網(wǎng)
{FCGI_BEGIN_REQUEST, 1, {FCGI_RESPONDER, 0}} {FCGI_PARAMS, 1, "