热搜:NVER node 开发 php

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

2024-07-24 21:05:01
FastCGI 协议分析以及 FastCGI 在 PHP 中的实现

在讨论 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 #include #include #include #include #include #include #include    #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 #include // 通过获取的 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("用户信息查询
学号: %d
姓名: %s
年龄: %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\n ... "}{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<<8 + B0的单个数字。该单个数字的名字是这些组件减去后缀的名字。这个约定归纳了一个由超过两个字节表示的数字的处理方式。

比如协议头中 requestId 和 contentLength 表示的最大值就是 65535

#include #include #include 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 自定义的。