Home  >  Article  >  Backend Development  >  FastCGI 协议分析以及 FastCGI 在 PHP 中的实现

FastCGI 协议分析以及 FastCGI 在 PHP 中的实现

WBOY
WBOYOriginal
2016-06-23 13:16:36724browse

在讨论 FastCGI 之前,不得不说传统的 CGI 的工作原理,同时应该大概了解 CGI 1.1 协议

传统 CGI 工作原理分析

客户端访问某个 URL 地址之后,通过 GET/POST/PUT 等方式提交数据,并通过 HTTP 协议向 Web 服务器发出请求,服务器端的 HTTP Daemon(守护进程)将 HTTP 请求里描述的信息通过标准输入 stdin 和环境变量(environment variable)传递给主页指定的 CGI 程序,并启动此应用程序进行处理(包括对数据库的处理),处理结果通过标准输出 stdout 返回给 HTTP Daemon 守护进程,再由 HTTP Daemon 进程通过 HTTP 协议返回给客户端。

上面的这段话可能还是比较抽象,下面通过一次GET请求为例说明。

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 9003char* 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;}

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程序的同级目录。

FastCGI 工作原理分析

相对于 CGI/1.1 规范在 Web 服务器在本地 fork 一个子进程执行 CGI 程序,填充 CGI 预定义的环境变量,放入系统环境变量,把 HTTP body 体的 content 通过标准输入传入子进程,处理完毕之后通过标准输出返回给 Web 服务器。 FastCGI 的核心则是取缔传统的 fork-and-execute 方式,减少每次启动的巨大开销(后面以 PHP 为例说明),以常驻的方式来处理请求。

FastCGI 与传统 CGI 模式的区别在于 Web 服务器不是直接执行 CGI 程序了,而是通过 socket 与 FastCGI 响应器(FastCGI 进程管理器)进行交互,Web 服务器需要将 CGI 接口数据封装在遵循 FastCGI 协议包中发送给 FastCGI 响应器程序。正是由于 FastCGI 进程管理器是基于 socket 的,所以也是分布式的,Web服务器和CGI响应器服务器分开部署。

FastCGI 协议消息流程

在官方的介绍文档中列举了一些例子 http://www.fastcgi.com/devkit/doc/fcgi-spec.html#SB

下面我们从比较简单的流程入手理解,如下图所示

例如下面的例子

{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 请求的各个阶段在 PHP 中的定义

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;

FastCGI 头在 PHP 里的定义

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组件的字节数。

关于上面的 xxB1 和 xxB0 的协议说明:当两个相邻的结构组件除了后缀“B1”和“B0”之外命名相同时,它表示这两个组件可视为估值为B1

比如协议头中 requestId 和 contentLength 表示的最大值就是 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}

FCGI_BEGIN_REQUEST 在 PHP 里的定义

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

Web服务器发送FCGI_BEGIN_REQUEST记录开始一个请求。

字段解释

role 表示Web服务器期望应用扮演的角色。分为三个角色在 PHP 里也有定义

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

flags 组件包含一个控制线路关闭的位: flags & FCGI_KEEP_CONN :如果为0,则应用在对本次请求响应后关闭线路。如果非0,应用在对本次请求响应后不会关闭线路;Web服务器为线路保持响应性。

FCGI_END_REQUEST 在 PHP 中的定义

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_status 和 fcgi_role 各个元素的值都是 FastCGI 协议里定义好的,而非 PHP 自定义的。

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Previous article:php无法开启php_curlNext article:获取接口的参数