澳门新葡亰553311b > 新葡亰Web前端 > CGI 和 FastCGI 协议的运行原理

CGI 和 FastCGI 协议的运行原理
2020-01-15 16:49

在讨论 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请求为例进行详细说明。

图片 1

下面用代码来实现图中表述的功能。Web 服务器启动一个 socket 监听服务,然后在本地执行 CGI 程序。后面有比较详细的代码解读。

  • 介绍
  • 深入CGI协议
    • CGI的运行原理
    • CGI协议的缺陷
  • 深入FastCGI协议
    • FastCGI协议运行原理
    • 为什么是 FastCGI 而非 CGI 协议
    • CGI 与 FastCGI 架构
    • 再看 FastCGI 协议
    • Web 服务器和 FastCGI 交互过程
    • 为什么需要在消息头发送 RequestID 这个标识?
  • PHP-FPM

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 OKrnContent-Type:text/htmlrnContent-Length: %drnServer: mengkangrnrn%s";

    sprintf(res,html_response_template,strlen(buf),buf);

    return res;
}

介绍

在用PHP开发的过程中,我们常常使用Nginx或者Apache作为我们的Web服务器。但是PHP是如何与这些Web服务器通信的呢?

  • Apache把PHP作为一个模块集成到Apache进程运行,这种mod_php的运行模式与PHP-CGI没有任何关系。

  • Nginx是通过FastCGI来实现与PHP的通信。

要谈FastCGI就必须先说说CGI。那什么是CGI?

CGI(Common Gateway Interface:通用网关接口)是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据库API 与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。--百度百科

CGI协议同 HTTP 协议一样是一个「应用层」协议,它的 功能 是为了解决 Web 服务器与 PHP 应用(或其他 Web 应用)之间的通信问题。

既然它是一个「协议」,换言之它与语言无关,即只要是实现类 CGI 协议的应用就能够实现相互的通信。

如上代码中的重点:

  • 66~81行找到CGI程序的相对路径(我们为了简单,直接将其根目录定义为Web程序的当前目录),这样就可以在子进程中执行 CGI 程序了;同时设置环境变量,方便CGI程序运行时读取;
  • 94~95行将 CGI 程序的标准输出结果写入 Web 服务器守护进程的缓存中;
  • 97行则将包装后的 html 结果写入客户端 socket 描述符,返回给连接Web服务器的客户端。

深入CGI协议

我们已经知道了 CGI 协议是为了完成 Web 服务器和应用之间进行数据通信这个问题。那么,这一节我们就来看看究竟它们之间是如何进行通信的。

简单来讲 CGI 协议它描述了 Web 服务器和应用程序之间进行数据传输的格式,并且只要我们的编程语言支持标准输入、标准输出以及环境变量等处理,你就可以使用它来编写一个 CGI 程序。

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服务器守护进程中设置的环境变量,是我们演示的重点。

CGI的运行原理

  • 当用户访问我们的 Web 应用时,会发起一个 HTTP 请求。最终 Web 服务器接收到这个请求。

  • Web 服务器创建一个新的 CGI 进程。在这个进程中,将 HTTP 请求数据已一定格式解析出来,并通过标准输入和环境变量传入到 URL 指定的 CGI 程序(PHP 应用 $_SERVER)。

  • Web 应用程序处理完成后将返回数据写入到标准输出中,Web 服务器进程则从标准输出流中读取到响应,并采用 HTTP 协议返回给用户响应。

一句话就是 Web 服务器中的 CGI 进程将接收到的 HTTP 请求数据读取到环境变量中,通过标准输入转发给 PHP 的 CGI 程序;当 PHP 程序处理完成后,Web 服务器中的 CGI 进程从标准输出中读取返回数据,并转换回 HTTP 响应消息格式,最终将页面呈献给用户。然后 Web 服务器关闭掉这个 CGI 进程。

可以说 CGI 协议特别擅长处理 Web 服务器和 Web 应用的通信问题。然而,它有一个严重缺陷,对于每个请求都需要重新 fork 出一个 CGI 进程,处理完成后立即关闭。

FastCGI 工作原理分析

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

FastCGI 工作流程如下:

  1. FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程,并等待来自 Web Server 的连接。
  2. Web 服务器与 FastCGI 进程管理器进行 Socket 通信,通过 FastCGI 协议发送 CGI 环境变量和标准输入数据给 CGI 解释器进程。
  3. CGI 解释器进程完成处理后将标准输出和错误信息从同一连接返回 Web Server。
  4. CGI 解释器进程接着等待并处理来自 Web Server 的下一个连接。

图片 2

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

再啰嗦一句,FastCGI 是一种协议,它是建立在CGI/1.1基础之上的,把CGI/1.1里面的要传递的数据通过FastCGI协议定义的顺序、格式进行传递。

CGI协议的缺陷

  • 每次处理用户请求,都需要重新 fork CGI 子进程、销毁 CGI 子进程。

  • 一系列的 I/O 开销降低了网络的吞吐量,造成了资源的浪费,在大并发时会产生严重的性能问题。

准备工作

可能上面的内容理解起来还是很抽象,这是由于第一对FastCGI协议还没有一个大概的认识,第二没有实际代码的学习。所以需要预先学习下 FastCGI 协议的内容,不一定需要完全看懂,可大致了解之后,看完本篇再结合着学习理解消化。

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

深入FastCGI协议

从功能上来讲,CGI 协议已经完全能够解决 Web 服务器与 Web 应用之间的数据通信问题。但是由于每个请求都需要重新 fork 出 CGI 子进程导致性能堪忧,所以基于 CGI 协议的基础上做了改进便有了 FastCGI 协议,它是一种常驻型的 CGI 协议。

本质上来将 FastCGI 和 CGI 协议几乎完全一样,它们都可以从 Web 服务器里接收到相同的数据,不同之处在于采取了不同的通信方式。

再来回顾一下 CGI 协议每次接收到 HTTP 请求时,都需要经历 fork 出 CGI 子进程、执行处理并销毁 CGI 子进程这一系列工作。

FastCGI 协议采用 进程间通信 来处理用户的请求,下面我们就来看看它的运行原理。

FastCGI 协议分析

下面结合 PHP 的 FastCGI 的代码进行分析,不作特殊说明以下代码均来自于 PHP 源码。

FastCGI协议运行原理

  • FastCGI 进程管理器启动时会创建一个 主 进程和多个 CGI 解释器进程(Worker 进程),然后等待 Web 服务器的连接。

  • Web 服务器接收 HTTP 请求后,将 CGI 报文通过 套接字(UNIX 或 TCP Socket)进行通信,将环境变量和请求数据写入标准输入,转发到 CGI 解释器进程。

  • CGI 解释器进程完成处理后将标准输出和错误信息从同一连接返回给 Web 服务器。

  • CGI 解释器进程等待下一个 HTTP 请求的到来。

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;

为什么是 FastCGI 而非 CGI 协议

如果仅仅因为工作模式的不同,似乎并没有什么大不了的。并没到非要选择 FastCGI 协议不可的地步。

然而,对于这个看似微小的差异,但意义非凡,最终的结果是实现出来的 Web 应用架构上的差异。

消息的发送顺序

下图是一个简单的消息传递流程

图片 3

最先发送的是FCGI_BEGIN_REQUEST,然后是FCGI_PARAMSFCGI_STDIN,由于每个消息头(下面将详细说明)里面能够承载的最大长度是65535,所以这两种类型的消息不一定只发送一次,有可能连续发送多次。

FastCGI 响应体处理完毕之后,将发送FCGI_STDOUTFCGI_STDERR,同理也可能多次连续发送。最后以FCGI_END_REQUEST表示请求的结束。

需要注意的一点,FCGI_BEGIN_REQUESTFCGI_END_REQUEST分别标识着请求的开始和结束,与整个协议息息相关,所以他们的消息体的内容也是协议的一部分,因此也会有相应的结构体与之对应(后面会详细说明)。而环境变量、标准输入、标准输出、错误输出,这些都是业务相关,与协议无关,所以他们的消息体的内容则无结构体对应。

由于整个消息是二进制连续传递的,所以必须定义一个统一的结构的消息头,这样以便读取每个消息的消息体,方便消息的切割。这在网络通讯中是非常常见的一种手段。

CGI 与 FastCGI 架构

在 CGI 协议中,Web 应用的生命周期完全依赖于 HTTP 请求的声明周期。

对每个接收到的 HTTP 请求,都需要重启一个 CGI 进程来进行处理,处理完成后必须关闭 CGI 进程,才能达到通知 Web 服务器本次 HTTP 请求处理完成的目的。

但是在 FastCGI 中完全不一样。

FastCGI 进程是常驻型的,一旦启动就可以处理所有的 HTTP 请求,而无需直接退出。

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("%dn", (requestIdB1 << 8) + requestIdB0); // 65535
}

你可能会想到如果一个消息体长度超过65535怎么办,则分割为多个相同类型的消息发送即可。

再看 FastCGI 协议

通过前面的讲解,我们相比已经可以很准确的说出来 FastCGI 是一种通信协议 这样的结论。现在,我们就将关注的焦点挪到协议本身,来看看这个协议的定义。

同 HTTP 协议一样,FastCGI 协议也是有消息头和消息体组成。

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服务器为线路保持响应性。

消息头信息

主要的消息头信息如下:

  • Version: 用于表示 FastCGI 协议版本号。

  • Type: 用于标识 FastCGI 消息的类型 - 用于指定处理这个消息的方法。

  • RequestID: 标识出当前所属的 FastCGI 请求。

  • Content Length: 数据包包体所占字节数。

上一篇:深入理解 PHP opcode 优化 下一篇:没有了