CA88新登入 3

WEB编程基础,协议的运行原理

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

目录

谈论WEB编程的时候常说天天在写CGI,那么CGI是什么呢?可能很多时候并不会去深究这些基础概念,再比如除了CGI还有FastCGI,
wsgi,
uwsgi等,那这些又有什么区别呢?为了总结这些这些WEB编程基础知识,于是写了此文,如有错误,恳请指正,示例代码见
web-basis

传统 CGI 工作原理分析

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

上面的这段话理解可能还是比较抽象,下面我们就通过一次GET请求为例进行详细说明。

CA88新登入 1

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

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

1 CGI

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

介绍

在用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
协议的应用就能够实现相互的通信。

1.1 CGI原理

在说明CGI是什么之前,我们先来说说CGI不是什么。

  • CGI不是一门编程语言。它的实现对编程语言没有限定,你可以用python,php,perl,shell,C语言等。
  • CGI不是一个编程模式。你可以使用任何你熟悉的方式实现它。
  • CGI也不复杂,不需要你是一个编程老鸟,菜鸟一样可以愉快的写自己的CGI。

那么CGI到底是什么?CGI全称是Common Gateway Interface,即通用网关接口。我们可能对API(Application
Programming
Interface)会很熟悉,CGI就是WEB服务器的API。WEB服务器顾名思义,就是发送网页给浏览器的软件,浏览器称之为web
client,WEB服务器是web
server。浏览器作为客户端,它做的工作就是向WEB服务器请求文件(比如HTML文档,图片,样式文件以及任何其他文件等),一般的WEB服务器的功能就是发送存储在服务器上的静态文件给发送请求的客户端。

那么问题来了,有些时候,我们需要发送动态的数据给客户端,这就需要我们写程序来动态生成数据并返回,这就是CGI用处所在。需要强调的是,WEB服务器和客户端之间是不能交互的,CGI程序不能要求用户输入一些参数,处理并返回输出,然后要求用户继续输入,这也是CGI能够保持简单的原因之一。CGI程序每次只能最多获取一次用户输入,然后处理并返回一次输出。那么CGI如何获取用户输入呢?

CGI程序获取用户输入依赖浏览器发送请求的方式。一般来说,浏览器的HTTP请求会以GET或者POST的方式发送。浏览器使用HTML表格获取用户输入,HTML表格可以指定浏览器发送请求的方法是GET还是POST,它们不同在于GET方法会将用户输入参数作为URL一部分,而POST的优势在于:

  • 你可以发送更多的数据(URL长度是有限制的)
  • 发送数据不会在URL中被记录(例如你要发送密码放到URL中是不太安全的),也不会出现在浏览器的地址栏中。

那么CGI程序如何知道客户端请求是哪种方法呢?在WEB服务器加载你的CGI程序前,会设置一些环境变量让CGI程序知道去哪里获取用户输入数据以及数据大小。比如
REQUEST_METHOD这个环境变量会设置为客户端的请求方法如GET/POST/HEAD等。而CONTENT_LENGTH环境变量会告诉你应该从stdin中读取多少字节数据。CONTENT_TYPE则是告诉你客户端数据类型,是来自表单还是其他来源。

当CGI程序读取到了用户输入数据后,可以处理数据并将响应发送到stdout。CGI程序可以返回HTML数据或者其他类型数据如GIF图片等。这也是为什么你在返回数据前要先在第一行说明你返回数据的类型,如Content-type: text/html,然后加两个CRLF后(HTTP协议的规定),再返回真正的输出数据。

如上代码中的重点:

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

深入CGI协议

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

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

1.2 CGI实现

在现实应用中,WEB服务器常用的有nginx和apache。apache提供了很多模块,可以直接加载CGI程序,和上一章提到的方式基本一致。而nginx是不能加载CGI程序的,必须另外单独运行一个CGI程序处理器来处理CGI请求,先来看下CGI实现,WEB服务器代码cgi.c。编译并运行:

$ gcc -o cgi cgi.c
$ ./cgi

CGI程序如下,可以为C语言编写,如
cgi_hello.c,也可以是shell,python等其他语言,如
cgi_hello.sh。编译cgi_CA88新登入,hello.c,放到cgi.c同一个目录下面。

$ gcc -o cgi_hello cgi_hello.c

使用C实现一个cgi服务器,其实就是WEB服务器并附带调用cgi程序功能。根据URL中的路径获取cgi程序名,并执行该cgi程序获取返回结果并返回给客户端。注意,是在WEB服务器程序中设置的环境变量,通过execl执行cgi程序,cgi程序因为是fork+exec执行的,子进程是会复制父进程环境变量表到自己的进程空间的,所以可以读取环境变量QUERY_STRING。在浏览器输入
http://192.168.56.18:6006/cgi_hello?name=ssj(测试机ip为192.168.56.18)
可以看到返回 Hello: ssj

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 进程,处理完成后立即关闭。

2 FastCGI协议

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 的下一个连接。

CA88新登入 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
    开销降低了网络的吞吐量,造成了资源的浪费,在大并发时会产生严重的性能问题。

2.1 FastCGI原理

如前面提到的,nginx是不能直接加载CGI程序的,由此需要一个专门的CGI程序管理器,nginx通过unix-socket或tcp-socket与CGI程序管理器通信。如php常用php-fpm,python常用uWSGI等,不过它们的协议不同,php-fpm用的是fastcgi协议,而uWSGI用的是uwsgi协议。nginx对这两种协议都支持,nginx配置文件/etc/nginx/fastcgi_params/etc/nginx/uwsgi_params就是分别针对这两种协议的。

先来看看FastCGI协议。顾名思义,FastCGI协议不过是CGI协议的变种,不同之处仅仅在于WEB服务器和CGI程序的交互方式。CGI协议中WEB服务器和CGI程序是通过环境变量来传递信息,WEB服务器fork+exec来执行CGI程序,CGI程序将输出打印到标准输出,执行完成后即退出。而FastCGI做的事情几乎和CGI一样,不同点在于FastCGI是通过进程间通信来传递信息,比如unix
socket或tcp
socket。那么,如果只是这么小的不同,FastCGI协议的意义何在呢?FastCGI的意义在于可以让WEB应用程序架构完全变化,CGI协议下,应用程序的生命周期是一次http请求,而在FastCGI协议里面,应用程序可以一直存在,处理多个http请求再退出,大幅提升了WEB应用程序性能。

FastCGI协议是一个交互协议,尽管底层传输机制是面向连接的,但是它本身不是面向连接的。WEB服务器和CGI程序管理器之间通过FastCGI的消息通信,消息由header和body两部分组成。其中header包含的字段如下:

Version: FastCGI协议版本号,目前一般是1.
Type: 标识消息类型。后面会有提到。
Request ID: 标识消息数据包所属的请求。
Content Length: 该数据包中body长度

FastCGI主要的消息类型如下:

  • BEGIN_REQUEST:WEB服务器 => 应用程序,请求开始时发送。
  • ABORT_REQUEST:WEB服务器 =>
    应用程序,准备终止正在运行的请求时发送。常见情况是用户点击了浏览器的停止按钮。
  • END_REQUEST:应用程序 =>
    WEB服务器,请求处理完成后发送。这种消息的body会包含一个return
    code,标识请求成功还是失败。
  • PARAMS:WEB服务器 => 应用程序,称之为“stream
    packet”,一个请求里面可能发送多个PARAMS类型的消息。最后一个body长度为0的消息标识这类消息结束。PARAMS类型消息里面包含的数据正是CGI里面设置到环境变量里面的那些变量。
  • STDIN: WEB服务器 => 应用程序,这也是一个“stream
    packet”,POST相关数据会在STDIN消息中发送。在发送完POST数据后,会发送一个空的STDIN消息以标识STDIN类型消息结束。
  • STDOUT: 应用程序 => WEB服务器,这也是一个“stream
    packet”,是应用程序发送给WEB服务器的包含用户请求对应的响应数据。响应数据发送完成后,也会发送一个空的STDOUT消息以标识STDOUT类型消息结束。

WEB服务器和FastCGI应用程序之间交互流程通常是这样的:

  • WEB服务器接收到一个需要FastCGI应用程序处理的客户端请求。因此,WEB服务器通过unix-socket或者TCP-socket连接到FastCGI程序。
  • FastCGI程序看到了到来的连接,它可以选择拒绝或者接收该连接。若接收连接,则FastCGI程序开始从连接的数据流中读取数据包。
  • 如果FastCGI程序没有在预期时间内接收连接,则请求失败。否则,WEB服务器会发送一个
    BEGIN_REQUEST
    的消息给FastCGI程序,该消息有一个唯一的请求ID。接下来的消息都用这个在header中声明的同样的ID。接着,WEB服务器会发送一定数目的PARAMS消息给FastCGI程序,当变量都发送完成时,WEB服务器再发送一个空的PARAMS消息关闭PARAMS数据流。而且,WEB服务器会将收到的来自客户端的POST数据通过STDIN消息传给FastCGI程序,当所有POST数据传输完成,一样也会发送一个空的STDIN类型的消息以标识结束。
  • 同时,当FastCGI程序接收到BEGIN_REQUEST包后,它可以回复一个END_REQUEST包拒绝该请求,也可以接收并处理该请求。如果接收请求,则它会等到PARAMSSTDIN包都接收完成再一起处理,响应结果会通过STDOUT包发送回WEB服务器,最终会发送END_REQUEST包给WEB服务器让其知道请求是成功还是失败了。

有人可能会有点奇怪,为什么消息头中需要一个Request ID,如果一个请求一个连接,那这个字段是多余的。也许你猜到了,一个连接可能包含多个请求,这样就需要标识消息数据包是属于哪个请求,这也是FastCGI为什么要采用面向数据包的协议,而不是面向数据流的协议。一个连接中可能混合多个请求,在软件工程里面也称之为多路传输。由于每个数据包都有一个请求ID,所以WEB服务器可以在一个连接中同时传输任意个数据包给FastCGI应用程序。而且,FastCGI程序可以同时接收大量的连接,每个连接可以同时包含多个请求。

此外,上面描述的通信流程并不是顺序的。也就是说,WEB服务器可以先发送20个BEGIN_REQUEST包,然后再发送一些PARAMS包,接着发送一些STDIN包,然后又发送一些PARAMS包等等。

准备工作

可能上面的内容理解起来还是很抽象,这是由于第一对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 协议采用 进程间通信
来处理用户的请求,下面我们就来看看它的运行原理。

2.2 FastCGI实例分析

FastCGI 协议分析

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

FastCGI协议运行原理

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

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

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

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

测试环境配置和抓包

FastCGI实现方式很多,如PHP的php-fpm,或者比较简单的fcgiwrap,在这里,我用fcgiwrap这个比较简单的实现来分析FastCGI协议,验证上一节说的原理。

先安装fcgiwrap,可以源码安装,如果是ubuntu/debian系统也可以直接apt-get安装。通过/etc/init.d/fcgiwrap start启动fcgiwrap默认会以unix-socket方式运行,如果要改成tcp-socket运行,可以fcgiwrap -f -s tcp:ip:port这样运行。

# sudo apt-get install fcgiwrap

在测试的nginx配置的server段里面添加一行

include /etc/nginx/fcgi.conf;

其中fcgi.conf文件内容见
fcgi.conf。

测试用的cgi程序都放在 /usr/share/nginx/cgi-bin目录下面。测试cgi程序为
fcgi_hello.sh:

在浏览器输入http://192.168.56.18/cgi-bin/fcgi_hello.sh?foo=bar可以看到返回结果。

为了避免其他干扰,我没用tcp-socket运行fcgiwrap,这样为了抓unix-socket的包,需要使用socat这个工具。为了抓包,需要简单改下nginx的配置,将
/etc/nginx/fcgi.conf中的fastcgi_pass这一行修改下,如下所示。

# fastcgi_pass  unix:/var/run/fcgiwrap.socket;
fastcgi_pass  unix:/var/run/fcgiwrap.socket.socat;

reload nginx并在命令行打开socat命令

socat -t100 -x -v UNIX-LISTEN:/var/run/fcgiwrap.socket.socat,mode=777,reuseaddr,fork UNIX-CONNECT:/var/run/fcgiwrap.socket

此时,在浏览器输入http://192.168.56.18/cgi-bin/fcgi_hello.sh?foo=bar可以看到socat命令会有输出如下:

> 2018/01/30 06:16:42.309659  length=960 from=0 to=959
01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00  ................
01 04 00 01 03 92 06 00 0c 07 51 55 45 52 59 5f  ..........QUERY_
53 54 52 49 4e 47 66 6f 6f 3d 62 61 72 0e 03 52  STRINGfoo=bar..R
45 51 55 45 53 54 5f 4d 45 54 48 4f 44 47 45 54  EQUEST_METHODGET
......
66 72 3b 71 3d 30 2e 36 00 00 00 00 00 00 01 04  fr;q=0.6........
00 01 00 00 00 00 01 05 00 01 00 00 00 00        ..............
--
< 2018/01/30 06:16:42.312909  length=136 from=0 to=135
01 06 00 01 00 61 07 00 53 74 61 74 75 73 3a 20  .....a..Status: 
32 30 30 0d 0a                                   200..
43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 74 65  Content-Type: te
78 74 2f 70 6c 61 69 6e 0d 0a                    xt/plain..
0d 0a                                            ..
52 45 51 55 45 53 54 20 4d 45 54 48 4f 44 3a 20  REQUEST METHOD: 
20 47 45 54 0a                                    GET.
50 41 54 48 5f 49 4e 46 4f 3a 20 0a              PATH_INFO: .
51 55 45 52 59 5f 53 54 52 49 4e 47 3a 20 20 66  QUERY_STRING:  f
6f 6f 3d 62 61 72 0a                             oo=bar.
00 00 00 00 00 00 00 01 06 00 01 00 00 00 00 01  ................
03 00 01 00 08 00 00 00 00 00 00 00 00 00 00     ...............

在ubuntu/debian上通过
sudo apt-get install libfcgi-dev后,可以在/usr/local/fastcgi.h中找到各个类型的消息的定义,接下来我们对照上一节说的FastCGI类型逐个分析下。

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
应用架构上的差异。

分析

WEB服务器和FastCGI之间通常的交互流程是这样的,下面会通过抓包详细分析。

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\007QUERY_STRINGfoo=bar"}
{FCGI_PARAMS,          1, ""}
{FCGI_STDIN,           1, "id=1&name=ssj"}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

消息的发送顺序

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

CA88新登入 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
请求,而无需直接退出。

WEB服务器发送给FastCGI程序的数据包:

  • 第一个消息是
    BEGIN_REQUEST,可以看到第1个字节为01,也就是version为1,第2个字节为01,即消息类型是
    BEGIN_REQUEST,接着3-4字节0001是requestId为1。再接着5-6字节0008是消息体长度为8。然后7-8字节0000是保留字段和填充字段。接着8个字节就是消息体了,9-10字节0001为role值,表示FCGI_RESPONDER,也就是这是一个需要响应的消息。11字节00为flag,表示应用在本次请求后关闭连接。然后12-16的5个字节0000000000为保留字段。

  • 第二个消息的第1个字节是01,也是version为1,第2个字节为04,表示消息类型为PARAMS。接着3-4字节为0001是requestId也是1。5-6字节0x0392消息体长度为914字节。后面7-8是0600位填充字段6字节。后面的为消息体内容,也就是QUERY_STRING, REQUEST_METHOD这些在CGI中设置到环境变量中的变量和值。接下来是PARAMS消息体。PARAMS消息用的是Name-Value对这种形式组织的数据结构,先是变量名称长度,然后是变量值长度,接着才是名字和值的具体数据。注意,名和值的长度如果超过1字节,则用4个字节来存储,具体是1字节还是4字节根据长度值的第一个字节的最高位来区分,如果为1则是4字节,如果为0则是1字节。如此可以分析PARAMS消息体了,头两个字节0c07表示名字长度为12,值长度为7,然后就是13个字节的变量名QUERY_STRING,7字节的值foo=bar,以此类推,接着的2个字节0e03就是名字长度为14,值长度为3,变量名是REQUEST_METHOD,值为GET…后续数据就是剩下的其他变量。最后面的6个字节000000000000是填充字节。

  • 第三个消息也是PARAMS,这是一个空的PARAMS消息。第1字节为01,第2字节为04表示PARAMS,3-4字节0001是requestId为1,5-6字节0000表示消息体长度为0,7-8字节0000表示填充和保留字节为0。

  • 第四个消息为STDIN,第1个字节01是version,第2个字节05表示类型为STDIN,接下来是3-4字节0001是requestId为1,5-6字节表示消息体长度为0,因为我们没有POST数据。后面7-8字节为0。(如果有POST数据,则STDIN这里消息体长度不为0,而它的消息体就是POST的数据,注意STDIN不是Name-Value对,它是直接将POST的数据字段连在一起的,如这样id=1&name=ssj)。到此,WEB服务器发送给FastCGI程序的数据包结束。

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

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

再看 FastCGI 协议

通过前面的讲解,我们相比已经可以很准确的说出来 FastCGI
是一种通信协议

这样的结论。现在,我们就将关注的焦点挪到协议本身,来看看这个协议的定义。

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

FastCGI程序发送给WEB服务器的数据包:

  • 第一个消息是 STDOUT
    。第1个字节还是01为version,第2个字节06表示类型为STDOUT,接着3-4字节0001还是requestId,5-6字节0061为消息体长度97,7-8字节0700表示填充字段为7字节。接下来消息体就是返回的内容Status: 200\r\n...

  • 第二个消息还是
    STDOUT,不过是空的STDOUT消息,用来标识STDOUT消息结束。

  • 第三个消息是
    END_REQUEST。第1个字节01还是version,第2个字节03标识类型
    END_REQUEST,3-4字节为requestId为1,5-6字节为消息体大小为8,7-8字节0000为填充字节长度。后面消息体内容为8个0字节。也就是说appStatus为0,protocolStatus也为0.其中protocalStatus是协议级的状态码,为0表示
    REQUEST_COMPLETE,即请求正常完成。

// 消息类型定义
#define FCGI_BEGIN_REQUEST       1
#define FCGI_ABORT_REQUEST       2
#define FCGI_END_REQUEST         3
#define FCGI_PARAMS              4
#define FCGI_STDIN               5
#define FCGI_STDOUT              6
#define FCGI_STDERR              7
#define FCGI_DATA                8
#define FCGI_GET_VALUES          9
#define FCGI_GET_VALUES_RESULT  10
#define FCGI_UNKNOWN_TYPE       11
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)