CGI

CGI是外部应用程序(CGI程序)与Web服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的规程。

环境变量

CGI程序处理流程:

  • Web Server收到用户请求,并交给CGI处理
  • CGI程序把处理结果传送到Web Server
  • Web Server把结果送回给用户

CGI协议要求CGI服务程序和web服务器在同一台机器上,二者之间的参数传递是通过环境变量。以下是常用的环境变量及其含义。

与请求相关的环境变量

变量名称 含义
REQUEST_METHOD 服务器与CGI程序之间的信息传输方式,一般是GET或POST
QUERY_STRING 采用GET时所传输的信息,URL中问号后的内容。
CONTENT_LENGTH POST方法输入的数据的字节数。
CONTENT_TYPE POST发送,一般为application/xwww-form-urlencoded。
PATH_INFO 浏览器用GET方式发送数据时的附加路径。
PATH_TRANSLATED CGI程序的完整路径名。
SCRIPT_NAME 所调用的CGI程序的名字。

与服务器相关的环境变量

变量名称 含义
GETWAY_INTERFACE CGI程序的版本,在UNIX下为 CGI/1.1。
SERVER_NAME 运行CGI序为机器名或IP地址。
SERVER_INTERFACE WWW服务器的类型,如:CERN型或NCSA型。
SERVER_PROTOCOL 通信协议,应当是HTTP/1.0。
SERVER_PORT TCP端口,一般说来web端口是80。

与客户端相关的环境变量

变量名称 含义
REMOTE_HOST 发送程序的主机名,不能确定该值。
REMOTE_ADDR 发送程序的机器的IP地址。
REMOTE_USER 发送程序的人名。
HTTP_ACCEPT HTTP定义的浏览器能够接受的数据类型。
HTTP_REFERER 发送表单的文件URL。(并非所有的浏览器都传送这一变量)
HTTP_USER-AGENT 发送表单的浏览器的有关信息。
IF_MODIFIED_SINGCE 当用get方式请求并且只有当文档比指定日期更早时才返回数据

这些环境变量在CGI程序启动时初始化,在结束时销毁。

CGI程序实现步骤

CGI获取请求数据

当Web Server收到CGI请求,就查找执行CGI程序,并将请求数据按照保存到CGI进程环境变量中。

接下来到了CGI程序执行流程。CGI程序查询与本进程相应的环境变量,获取用户输入的数据。这个步骤首先查询REQUEST_METHOD,读取请求的提交的方式。如果是POST,就读取环境变量CONTENT_LENGTH,然后到该进程取出CONTENT_LENGTH长的数据;如果是GET,则请求数据就在QUERY_STRING中。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int get_inputs()
{
    int length = 0; //请求数据的长度
    char* method;
    char *input; // 请求数据

    method = getenv("REQUEST_METHOD");
    if (method == NULL)
    {
        return 1;
    }

    if (!strcmp(method, "POST"))
    {
        length = atoi(getenv("CONTENT_LENGTH"));
        if (length)
        {
            input = (char *)malloc(sizeof(char)*length + 1);
            fread(input, sizeof(char), length, stdin);
        }
    }
    else if (!strcmp(method, "GET"))
    {
        input = getenv("QUERY_STRING");
        length = strlen(input);
    }

    if (length == 0)
        return 0;

    return 1;
}

CGI数据输出

CGI程序如何将信息处理结果返回给客户端?这实际上是CGI格式化输出。

在CGI程序中的标准输出stdout是经过重定义了的,它并没有在服务器上产生任何的输出内容,而是被重定向直接输出到客户浏览器(??)。所以,我们可以用打印来实现客户端新的HTML页面的生成,比如C的printf函数。

CGI的格式输出内容必须组织成标题/内容的形式。CGI标准规定了CGI程序可以使用的三个HTTP标题。标题必须占据第一行输出,而且必须随后带有一个空行!。
标题

标题 描述
Content_type (内容类型) 设定随后输出数据所用的MIME类型
Location (地址) 设定输出为另外一个文档(URL)
Status (状态) 指定HTTP状态码

如果使用Location标题,可以使当前用户转而访问同一服务器上的另外一个程序,甚至可以访问另外一个URL,但服务器对他们的处理方式不一样。这就是重定向的实现方式。

#include <stdio.h>
#include <string.h>
int main()
{
    printf("Contenttype:text/html\n\n");
    printf("<html>\n");
    printf("<head><title >An HTML Page From a CGI</title></head>\n");
    printf("<body>\n");
    printf("<h2> This is an HTML page generated from with i n a CGI program.. .</h2>\n");
    printf("<hr><p>\n");
    printf("<a href=\"../output.html#two\"><b> Go back to out put.html page </b></a>\n");
    printf("</body>\n");
    printf("</html>\n");
    fflush(stdout);
}

CGI缺点

CGI程序可以生成动态的网页内容,使用CGI函数库实现相关的功能也比较简单。但是:

  • CGI程序不是常驻内存的,每次执行CGI程序都需要进行程序的启动和销毁,代价比较大,效率低;
  • 当请求数比较多的时候,会导致服务器压力大增,并发能力差。
  • CGI服务程序和web服务器只能在同一台机器之内,这也是一个很大的限制。

FastCGI

FastCGI解决了CGI的如下问题

  • 通过进程常驻的方式,避免了进程的频繁启动和注销,提升了效率
  • 通过监听端口的方式,实现了Web服务器和FastCGI服务器的分布式部署,更加的方便和简单
  • 通过传输线路的复用,实现并发支持

以下是我们使用简称WS表示Web Server,使用FS表示FastCGI Server,也就是所谓的应用服务器。

FastCGI的环境变量

FastCGI定义了一个唯一的环境变量FCGI_WEB_SERVER_ADDRS,它定义了WS的一个白名单列表,用于检查接入请求的IP地址是否被允许。但是这个环境变量一般是空的,因为有更好的方式控制,比如通过一些运维工具,所以这个环境变量基本被废弃。

基于记录的二进制协议

FastCGI是WS和FS之间通讯的协议,本质上是二进制的,其内容是一些列记录(Record)。WS和FS的对话可能需要传递多个记录。

记录是FastCGI协议基本的通讯的单位。它分成头信息和体信息,即Record=Header+Body,所有Header都是固定的长度(8个字节),Body可能是定长,也可能是变长的。

为了避免大小端的问题,所有的长字节整数(比如4字节或者2字节整数)都被简化为多字节的整数。在应用端,多字节整数可以通过简单的方式合并为长字节整数

Header中存储Body的元信息,包括信息类型,request_id信息和长度信息等。Header中类型信息规定了Body内容的解析方式和使用方式;request_id信息用于多路复用,request_id==0表明是管理信息。

Header中的类型信息定义:

FCGI_BEGIN_REQUEST(1) 表明从WS到FS开始传输有效的记录
FCGI_ABORT_REQUEST(2) 表明从WS终止对FS的请求
FCGI_END_REQUEST(3) FS发向WS,指明本次请求结束
FCGI_PARAMS(4) WS发向FS的参数类型
FCGI_STDIN(5) WS发向FS的数据类型,用于Responser角色
FCGI_STDOUT(6) FS发向WS的数据类型,结果数据
FCGI_STDERR(7) FS发向WS的数据类型,表明是错误的数据
FCGI_DATA(8) WS发向FS的数据类型,用于Filter角色
FCGI_GET_VALUES(9) WS发向FS的数据类型,用于管理信息获取
FCGI_GET_VALUES_RESULT(10)FS发向WS的数据类型,用于管理信息发送

流式记录

记录分成2种,定长记录和变长记录。

定长记录

有3种类型:

  • 开始记录(对应于fastcgi协议中c语言定义的结构体FCGI_BeginRequestRecord),从WS发向FS,表明一次请求对话的开始,长度是16个字节
  • 结束记录(对应于fastcgi协议中c语言定义的结构体FCGI_EndRequestRecord),从FS发向WS,表明本次对话的结束,长度也是16个字节,传递了应用服务的状态和协议的状态信息。
  • 未知类型的记录(对应于fastcgi协议中c语言定义的结构体FCGI_UnknownTypeRecord),从FS发向WS,表明FS不认识WS的请求类型,响应一个错误的结果,长度也是16个字节。

变长记录

变长记录用于参数信息,数据信息等。这类记录的特点是长度不固定,和请求的内容和应答的内容相关。由于一个记录最大的长度是65535,因此如果数据量过大,可能需要多个记录完成一次数据传输,这样也产生了一个问题:一次数据传输需要使用多少记录?

变长记录使用了一种类似于C式字符串流的思路解决了这个问题,即使用一个长度为0的记录(只有Header,没有Body,因为Body的长度是0)表明本次数据传输的记录内容结束。从这个意义上讲,变长记录也被称为流式记录。

流式记录的好处是可以传输任意大的数据流,缺点是不到最后一个记录,不能确定是否结束,这往往需要多次读写,可能影响性能。

参数记录

对于流式记录来说,大部分记录的内容是不透明的,协议只是以二进制形式提供给FS或者WS,最终是应用本身解析的。只有一个例外,参数记录类型的二进制流是需要协议解析的。

参数记录以Key/Value字符串对的形式提供,具体表示为”Key的长度|Value的长度|Key的内容|Value的内容”。参数类型记录的Body,是若干个这样的KV集合。其中的长度信息使用了变长压缩的方法,即当长度<128的时候,使用单字节表示;否则使用4字节表示。对于绝大部分的参数来说,单字节足够了。

对于管理信息的对话,开始记录的Header信息结束以后,后续的Body信息就是KV集合;否则,使用一个8字节的Body结束开始记录,后续的记录就是带有Header信息的KV集合。

管理信息的应答结果也是参数记录类型。

管理信息对话

当开始记录中Header。request_id==0的时候,表明WS向FS请求管理信息。

在管理信息对话中,当Header。request_id==0的时候,Header。Type只能是FCGI_GET_VALUES;否则FS会向WS发送不能识别类型的错误信息(FCGI_UnknownTypeRecord)。

目前标准定义了三种管理信息的查询:

  • 最大连接数FCGI_MAX_CONNS
  • 最大请求数FCGI_MAX_REQS
  • 最大多路复用数FCGI_MPXS_CONNS

在当前fcgi库的实现版本中,最大连接数和最大请求数都是1,而最大多路复用数是0。

当然这个设定也许是不合理的;但是实际上,在成熟的WS中,基本上都不会发送管理信息,因为WS都有自己的特定配置方式,不会使用FastCGI协议的这个规范来定义这些信息。

所以说,管理信息这部分协议内容,在实践中基本上是被废弃的,不使用的。

应用信息对话

除了管理信息对话,剩下的就是应用信息对话。因为管理信息对话在实践中基本上是被废弃的,所以真正有价值的就是应用信息对话了。

一次有效的对话过程,总是以开始记录开始,以结束记录结束;否则都是无效的记录。FastCGI规范规定,无效的记录也应该被读取,但是会被忽略,直到遇到有效的记录,

定长的开始记录被读取以后,可以获取本次对话中FS承担的角色信息(role)。不同的角色,会导致本次对话中,WS后续向FS发送的记录内容和记录个数的不同。

  • 响应者(Responser)角色
    这个是最主要的角色,也几乎是在实践中唯一有效的角色。
    这个角色需要WS向FS传输2类内容:参数类型(FCGI_PARAMS,对应于CGI中的环境变量)和标准输入类型(FCGI_STDIN一般对应于HTTP POST的内容)。如果没有POST内容,那么标注输入类型的记录就是空记录(8个字节)。
    很多简单的以HTTP URL和HTTP Header为信息内容的请求,都在FastCGI协议下,被转化为参数类型的记录;而标注输入类型往往都是空记录。

  • 过滤器(Filter)角色
    WS要求FS对结果信息进行过滤。
    这个角色需要WS向FS传输3类内容:参数类型(FCGI_PARAMS),标准输入类型(FCGI_STDIN)和数据类型(FCGI_DATA)。

  • 验证器(Authorizer)角色
    WS要求FS对用户的角色进行验证。
    这个角色需要WS向FS传输1类内容:参数类型(FCGI_PARAMS)。

在实践中,开发者几乎唯一有效的编码就是针对响应者的角色进行对话。

响应者(Responser)角色

因为响应者的角色很重要,所以需要详细说明一下。

在响应者角色中,WS需要向FS发送开始记录,参数记录和标准输入记录。在没有POST内容的情况下,标准输入记录往往是空记录。

虽然是空的记录,但是仍然需要8个字节的数据量。其实FastCGI协议完全有更好的方法避免这个浪费,例如在开始记录中使用标志位来记录后续的记录类型;当是空记录的时候,完全可以省略掉。

不仅如此,在fcgi的FCGI_Accept函数中,为了保险,fcgi只是读取了3种角色共同的记录类型-参数类型,并没有读取标准输入类型或者数据类型。这会有什么问题吗?

在响应者角色中,如果存在POST数据,那么开发者为了读取进一步的内容,需要执行类似于FCGX_GetStr这样的函数得到进一步的内容,这样就需要知道POST内容的长度。虽然这个信息可以从前面得到PARAMS参数中得到,但是这个数值本来就可以从当前记录中得到。也就是说,存在一个数值多处定义的问题。一个是从数据内容获得(通过HTTP_CONTENT_LENGTH变量),一个是从协议本身得到(FastCGI的记录长度)。而且一般来说,从协议本身得到数据是更可靠的方式,从内容处得到的结果可以仅仅作为一个验证手段。

更主要的是,即便是HTTP_CONTENT_LENGTH==0,WS也会向FS发送一个空的记录,这个记录往往会被忽略了。也就是说,会有8个字节的数据会留在缓冲区中没有被读取。在短连接的情况下,这个没有什么问题,因为随着连接的断开,数据也会被丢弃。但是在长连接的情况下,下一次的数据读取,会导致无效记录的诞生。

在FastCGI协议中,无效记录会被抛弃,这一般也不会有问题。但是上一次的有效空数据,被下一次的流程当成无效的数据抛弃,逻辑上有些别扭。而且在某些特殊的情况,也可能会产生一些问题。比如在多线程并发环境下,也许这次遗留的数据会被当成一次无效的请求,从而产生一些性能浪费。

fcgi知道足够的信息,比如role的信息,那么它就应该知道在响应者角色中,除了读取参数记录,还应该读取标准输入记录,哪怕这个是空记录。fcgi内部分配了读取参数记录的缓冲区并且读取了参数记录的内容,虽然也分配读取标准输入记录的缓冲区但是没有读取标准输入记录的内容,所以不应该的无效记录产生了。

作为协议的实现者,fcgi应该更好的隐藏这些细节,使得开发者只是专注于应用逻辑的实现。

FastCGI应该有更好的实现,而且这也不难。