转载

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请求为例说明。

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

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; }

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

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

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

例如下面的例子

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

关于上面的 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 }

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

原文  http://mengkang.net/668.html
正文到此结束
Loading...