ylg娱乐官网:浓重了然 法斯特CGI 公约以致在 PHP 中的达成

3.创建多个 CGI 解析器子进程

这里子进程的个数默认是0,从配置文件中读取设置到环境变量,然后在程序中读取,然后创建指定数目的子进程来等待处理
Web 服务器的请求。

if (getenv("PHP_FCGI_CHILDREN")) {
    char * children_str = getenv("PHP_FCGI_CHILDREN");
    children = atoi(children_str);
    ...
}

do {
    pid = fork();
    switch (pid) {
    case 0:
        parent = 0; // 将子进程中的父进程标识改为0,防止循环 fork

        /* don't catch our signals */
        sigaction(SIGTERM, &old_term, 0);
        sigaction(SIGQUIT, &old_quit, 0);
        sigaction(SIGINT,  &old_int,  0);
        break;
    case -1:
        perror("php (pre-forking)");
        exit(1);
        break;
    default:
        /* Fine */
        running++;
        break;
    }
} while (parent && (running < children));

深入FastCGI协议

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

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

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

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

4.1 uWSGI安装配置

uWSGI是一个WEB服务器,它实现了WSGI协议、uwsgi协议、http协议等。这里要区分下:uWSGI是WEB服务器,而小写的uwsgi是协议。安装uWSGI的步骤比较简单,如下:

# sudo apt-get install build-essential python-dev
# sudo pip install uwsgi

然后我们可以编写一个简单的符合WSGI规范的python程序:

# foobar.py
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"]

运行:

# uwsgi --http :9090 --wsgi-file foobar.py

此时,我们就可以在浏览器输入
http://127.0.0.1:9090来访问了。指定参数--http则是以HTTP服务器方式运行,在实际项目中,通常会以socket的方式运行,nginx负责处理静态资源,动态请求则由nginx通过uwsgi协议与uWSGI服务器交互。

配置nginx如下:

# /etc/nginx/sites-enabled/uwsgi
server {
    listen 9090;
    location / {
        include uwsgi_params;
         uwsgi_pass 127.0.0.1:3031;
    }
}

以socket的方式运行uWSGI如下(加了进程和线程数配置):

uwsgi --socket 127.0.0.1:3031 --wsgi-file foobar.py --master --processes 4 --threads 2

为了方便,可以将启动参数放到配置文件 config.ini中,然后
uwsgi config.ini即可。

## config.ini示例
[uwsgi]
uid = nobody
gid = nogroup
socket = 127.0.0.1:3031
chdir = /home/vagrant/project/uwsgi
wsgi-file = foobar.py
processes = 4
threads = 2

如果nginx里面配置的是proxy_pass http://127.0.0.1:3031,则此时需要将uwsgi以
http-socket的方式运行,即

uwsgi --http-socket 127.0.0.1:3031 --wsgi-file foobar.py --master --processes 4 --threads 2 

2、FastCGI

FCGI_END_REQUEST 的定义

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

CGI协议的缺陷

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

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

3 WSGI

在php5_module定义的结构中,php_dir_cmds是模块定义的所有的指令集合,定义的内容如下:

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

再看 FastCGI 协议

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

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

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

服务器/网关

服务器/网关每次从 HTTP
客户端收到一个请求,就调用一次应用对象。为了便于说明,这里有个简单的CGI网关的例子
server.py,接收请求并调用应用对象app处理请求,实际负责处理请求的地方在handles.py中。

以cgi模式和apache2服务器为例,它们的启动方法如下:

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;

深入CGI协议

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

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

3.2 WSGI细节

 

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

为什么需要在消息头发送 RequestID 这个标识?

如果是每个连接仅处理一个请求,发送 RequestID 则略显多余。

但是我们的 Web 服务器和 FastCGI
进程之间的连接可能处理多个请求,即一个连接可以处理多个请求。所以才需要采用数据包协议而不是直接使用单个数据流的原因:以实现「多路复用」。

因此,由于每个数据包都包含唯一的 RequestID,所以 Web
服务器才能在一个连接上发送任意数量的请求,并且 FastCGI
进程也能够从一个连接上接收到任意数量的请求数据包。

另外我们还需要明确一点就是 Web 服务器 与 FastCGI 进程间通信是
无序的。即使我们在交互过程中看起来一个请求是有序的,但是我们的 Web
服务器也有可能在同一时间发出几十个 BEGIN_REQUEST
类型的数据包,以此类推。

缓冲和流

一般来说,应用程序通过缓冲输出并一次发送全部数据来实现最佳吞吐量。在Zope等现有框架中,这是一种常见的方法:输出缓存在一个StringIO或类似的对象中,然后与响应头一起传输。

1、调用socket函数创建一个TCP用的流式套接字;

2.初始化请求对象

fcgi_request对象分配内存,绑定监听的 socket 套接字。

fcgi_init_request(&request, fcgi_fd);

整个请求从输入到返回,都围绕着fcgi_request结构体对象在进行。

typedef struct _fcgi_request {
    int            listen_socket;
    int            fd;
    int            id;
    int            keep;
    int            closed;

    int            in_len;
    int            in_pad;

    fcgi_header   *out_hdr;
    unsigned char *out_pos;
    unsigned char  out_buf[1024*8];
    unsigned char  reserved[sizeof(fcgi_end_request_rec)];

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

3.3 实现

总体上看来,WSGI服务器端就是接收请求,设置好环境变量,然后调用应用对象处理请求。而应用对象调用start_response函数设置头部(注意,此时还没有返回响应给客户端),然后应用对象返回一个可迭代对象(如Python的列表)给服务器端。服务端对应用对象返回的迭代数据进行输出,输出前会先调用send_headers()来发送响应头部。

完整的示例代码参见
web-basis-wsgi,代码基本来源于Python自带的wsgiref和http相关模块。

typedef struct module_struct module;
struct module_struct {
    int version;
    int minor_version;
    int module_index;
    const char *name;
    void *dynamic_load_handle;
    struct module_struct *next;
    unsigned long magic;
    void (*rewrite_args) (process_rec *process);
    void *(*create_dir_config) (apr_pool_t *p, char *dir);
    void *(*merge_dir_config) (apr_pool_t *p, void *base_conf, void *new_conf);
    void *(*create_server_config) (apr_pool_t *p, server_rec *s);
    void *(*merge_server_config) (apr_pool_t *p, void *base_conf, void 
*new_conf);
    const command_rec *cmds;
    void (*register_hooks) (apr_pool_t *p);
}

写在最后

把 FastCGI
的知识学习理解的过程做了这样一篇笔记,把自己理解的内容(自我认为)有条理地写出来,能够让别人比较容易看明白也是一件不挺不容易的事。同时也让自己对这个知识点的理解又深入了一层。对
PHP 代码学习理解中还有很多困惑的地方还需要我自己后期慢慢消化和理解。

本文都是自己的一些理解,水平有限,如有勘误,希望大家予以指正。

坚持看完本的都是老司机,说实话,后面有些太枯燥了!如果能把每个知识点真正理解消化,绝对获益良多。

目录

环境变量

environ字典中必须包含CGI规范中定义的变量,包括下面这些:

  • REQUEST_METHOD
  • SCRIPT_NAME
  • PATH_INFO
  • QUERY_STRING
  • CONTENT_TYPE
  • CONTENT_LENGTH
  • SERVER_NAME, SERVER_PORT
  • SERVER_PROTOCOL
  • HTTP_Variables

除了CGI定义的环境变量之外,environ字典中还要包含下面几个变量:

  • wsgi.version:WSGI版本,元组(1,0)表示版本为1.0.
  • wsgi.url_scheme:URL模式,值通常为http或者https。
  • wsgi.input:可以读取HTTP请求体的输入流。(当被应用对象请求时,服务器/网关执行
    read
    ,可以预读取请求体,缓存到内存或者磁盘中,或者用其他处理输入流的技术)
  • wsgi.errors:错误输出流。在许多服务器中,wsgi.errors通常是服务器的日志。
  • wsgi.multithread:应用对象如果支持多线程,则设置为true。
  • wsgi.multiprocess:应用对象如果支持多进程,则设置为true。
  • wsgi.run_once:如果服务器/网关希望应用对象在包含它的进程中仅执行一次这个请求,它的值为true。正常情况下,只有是基于CGI的网关才是true。

最后,environ字典也可能包含服务器定义的变量。这些变量只能使用小写字母,数字,点和下划线来命名,并且应该用该服务器/网关唯一的名称作为前缀。例如,mod_python可能会定义名称为mod_python.some_variable的变量。

  Step3:当客户端请求到达Web
Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web
Server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi

5.读取数据

下面的代码删除一些异常情况的处理,只显示了正常情况下执行顺序。

fcgi_read_request中则完成我们在消息通讯样例中的消息读取,而其中很多的len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;操作,已经在前面的FastCGI
消息头中解释过了。

这里是解析 FastCGI 协议的关键。

static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count)
{
    int    ret;
    size_t n = 0;

    do {
        errno = 0;
        ret = read(req->fd, ((char*)buf)+n, count-n);
        n += ret;
    } while (n != count);
    return n;
}

static int fcgi_read_request(fcgi_request *req)
{
    ...

    if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
        return 0;
    }

    len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
    padding = hdr.paddingLength;

    req->id = (hdr.requestIdB1 << 8) + hdr.requestIdB0;

    if (hdr.type == FCGI_BEGIN_REQUEST && len == sizeof(fcgi_begin_request)) {
        char *val;

        if (safe_read(req, buf, len+padding) != len+padding) {
            return 0;
        }

        req->keep = (((fcgi_begin_request*)buf)->flags & FCGI_KEEP_CONN);

        switch ((((fcgi_begin_request*)buf)->roleB1 << 8) + ((fcgi_begin_request*)buf)->roleB0) {
            case FCGI_RESPONDER:
                val = estrdup("RESPONDER");
                zend_hash_update(req->env, "FCGI_ROLE", sizeof("FCGI_ROLE"), &val, sizeof(char*), NULL);
                break;
            ...
            default:
                return 0;
        }

        if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
            return 0;
        }

        len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
        padding = hdr.paddingLength;

        while (hdr.type == FCGI_PARAMS && len > 0) {
            if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
                req->keep = 0;
                return 0;
            }
            len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
            padding = hdr.paddingLength;
        }

        ...
    }
}

消息类型定义

  • BEGIN_REQUEST: 从 Web 服务器发送到 Web
    应用,表示开始处理新的请求。

  • ABORT_REQUEST: 从 Web 服务器发送到 Web
    应用,表示中止一个处理中的请求。比如,用户在浏览器发起请求后按下浏览器上的「停止按钮」时,会触发这个消息。

  • END_REQUEST: 从 Web 应用发送给 Web
    服务器,表示该请求处理完成。返回数据包里包含「返回的代码」,它决定请求是否成功处理。

  • PARAMS: 「流数据包」,从 Web 服务器发送到 Web
    应用。此时可以发送多个数据包。发送结束标识为从 Web
    服务器发出一个长度为 0 的空包。且 PARAMS 中的数据类型和 CGI
    协议一致。即我们使用 $_SERVER 获取到的系统环境等。

  • STDIN: 「流数据包」,用于 Web 应用从标准输入中读取出用户提交的
    POST 数据。

  • STDOUT: 「流数据报」,从 Web
    应用写入到标准输出中,包含返回给用户的数据。

2.3 fcgiwrap分析

fcgiwrap用到了libfcgi库,libfcgi库提供了一些函数封装,以方便实现fastcgi管理器。fcgiwrap启动参数如下:

fcgiwrap -f -s unix:/var/run/fcgiwrap.socket -c 2

其中-s指定socket类型,若要用tcp-socket则用
-s tcp:ip:port。-c参数指定子进程数目,这里为2个。

fcgiwrap的核心代码如下,即先创建一个listen
socket,然后将该socket通过dup2复制到文件描述符0,因为libfcgi库里面固定从fd
0来监听网络数据。prefork是创建参数指定数目的子进程数目,然后父进程通过pause()调用停止运行,接着每个子进程继续往下执行fcgiwrap_main()函数。

int main(int argc, char **argv) {
    fd = setup_socket(socket_url);
    prefork(nchildren);
    fcgiwrap_main();
}

fcgiwrap_main()核心代码如下,即不停的通过
FCGI_Accept()函数监听连接并处理请求。其中FCGI_Accept()函数是libfcgi库提供的,主要作用就是监听listen
socket上的请求,然后根据fastcgi协议读取数据并解析为方便处理的结构,设置环境变量environ等,这样handle_fcgi_request()就能跟cgi程序一样通过读取环境变量还获取cgi文件名等内容。

static void fcgiwrap_main(void)
{
   ...... //略去了一些信号处理代码
   inherited_environ = environ;

    while (FCGI_Accept() >= 0 && !sigint_received) {
        handle_fcgi_request();
    }
}

handle_fcgi_request()就是处理请求的函数了,先是fork出子进程去执行CGI程序,将执行结果写入到管道中,而父进程则读取管道中的数据并返回给WEB服务器。这里有几点注意下:

  • 子进程中代码dup2(pipe_in[0], 0)执行后,子进程从pipe_in[0]作为标准输入,而父进程设置了
    fc.fd_stdin = pipe_in[1],在函数fcgi_pass()中,会先调用子函数fcgi_pass_request()读取FCGI_stdin中的数据(也就是前一节提到的STDIN类型的消息,也就是POST中的表单数据)并写入fc.fd_stdin,也就是写入到了pipe_in管道中,则子进程此时就可以从标准输入中(因为前面的dup2)读取到数据。同理,子进程中代码dup2(pipe_out[1], 1)即说明子进程的标准输出会输出到管道pipe_out中,父进程在fcgi_pass()中同理可以通过管道读取到子进程的运行输出结果(这里fcgi_pass()使用了select()方式来轮询fd_stdout和fd_stderr文件描述符)。父进程读取到输出结果后,返回STDOUTFCGI_END_REQUEST消息给nginx服务器,完成本次请求。

static void handle_fcgi_request(void)
{
    int pipe_in[2];
    int pipe_out[2];
    int pipe_err[2];
    char *filename;
    char *last_slash;
    char *p;
    pid_t pid;

    struct fcgi_context fc;

    switch((pid = fork())) {
        case -1:
            goto err_fork;

        case 0: /* child */
            close(pipe_in[1]);
            close(pipe_out[0]);
            close(pipe_err[0]);

            dup2(pipe_in[0], 0);
            dup2(pipe_out[1], 1);
            dup2(pipe_err[1], 2);

            close(pipe_in[0]);
            close(pipe_out[1]);
            close(pipe_err[1]);

            close(FCGI_fileno(FCGI_stdout));

            signal(SIGCHLD, SIG_DFL);
            signal(SIGPIPE, SIG_DFL);

            filename = get_cgi_filename();
            inherit_environment();
            ...... //省略了检查文件是否存在和文件权限的代码

            execl(filename, filename, (void *)NULL);
            cgi_error("502 Bad Gateway", "Cannot execute script", filename);

        default: /* parent */
            close(pipe_in[0]);
            close(pipe_out[1]);
            close(pipe_err[1]);

            fc.fd_stdin = pipe_in[1];
            fc.fd_stdout = pipe_out[0];
            fc.fd_stderr = pipe_err[0];
            fc.reply_state = REPLY_STATE_INIT;
            fc.cgi_pid = pid;

            fcgi_pass(&fc);
    }
    return;

   ...... // 省略部分错误处理代码
    FCGI_puts("Status: 502 Bad GatewaynContent-type: text/plainn");
    FCGI_puts("System error");
}

实际应用中,像php-fpm(fpm是fastcgi process
manager的意思)这种Fastcgi进程管理器,它会有master进程和worker进程,然后统一由master进程来分发请求管理worker,但是用的都是fastcgi协议,与本文分析的一致。

这里的cgi_sapi_module是sapi_module_struct结构体的静态变量。它的startup方法指向php_cgi_startup函数指针。在这个结构体中除了startup函数指针,还有许多其他方法或字段,这些结构在服务器的接口实现中都有定义

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 而非 CGI 协议

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

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

4.3 uWSGI与应用框架组合使用

uWSGI可以与Flask,Django,web2py等框架组合使用,在我们的实际项目中,架构通常是nginx + uWSGI + Flask,即静态请求由nginx处理,动态请求转发到uWSGI,然后组合使用Flask框架来编写业务逻辑,当然里面通常还会用到gevent等。

uWSGI与Flask组合使用也很简单。因为Flask框架将WSGI的可调用应用对象暴露出来了,我们只要在启动参数中指明入口的app即可。先安装flask模块:

# sudo pip install flask

然后编写flask程序如下:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return "I am app 1"

然后启动uWSGI(比之前只是多一个 --callable 参数):

uwsgi --socket 127.0.0.1:3031 --wsgi-file flaskapp.py --callable app --processes 4 --threads 2

  在发送了请求的应答后,服务器端将会执行关闭操作,仅限于CGI本身的关闭,程序执行的是fcgi_close函数。

1.开启一个 socket 监听服务

fcgi_fd = fcgi_listen(bindpath, 128);

从这里开始监听,而fcgi_listen函数里面则完成 socket
服务前三步socket,bind,listen

Web 服务器和 FastCGI 交互过程

  • Web 服务器接收用户请求,但最终处理请求由 Web 应用完成。此时,Web
    服务器尝试通过套接字(UNIX 或 TCP 套接字,具体使用哪个由 Web
    服务器配置决定)连接到 FastCGI 进程。

  • FastCGI
    进程查看接收到的连接。选择「接收」或「拒绝」连接。如果是「接收」连接,则从标准输入流中读取数据包。

  • 如果 FastCGI
    进程在指定时间内没有成功接收到连接,则该请求失败。否则,Web
    服务器发送一个包含唯一的RequestID 的 BEGIN_REQUEST 类型消息给到
    FastCGI 进程。后续所有数据包发送都包含这个 RequestID。 然后,Web
    服务器发送任意数量的 PARAMS 类型消息到 FastCGI
    进程。一旦发送完毕,Web 服务器通过发送一个空PARAMS
    消息包,然后关闭这个流。 另外,如果用户发送了 POST 数据 Web
    服务器会将其写入到 标准输入 发送给 FastCGI 进程。当所有 POST
    数据发送完成,会发送一个空的 标准输入 来关闭这个流。

  • 同时,FastCGI 进程接收到 BEGINREQUEST 类型数据包。它可以通过响应
    ENDREQUEST
    来拒绝这个请求。或者接收并处理这个请求。如果接收请求,FastCGI
    进程会等待接收所有的 PARAMS 和 标准输入数据包。
    然后,在处理请求并将返回结果写入 标准输出
    流。处理完成后,发送一个空的数据包到标准输出来关闭这个流,并且会发送一个
    END_REQUEST 类型消息通知 Web 服务器,告知它是否发生错误异常。

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程序的数据包结束。

1、在PHP生命周期的各个阶段,一些与服务相关的操作都是通过SAPI接口实现。这些内置实现的物理位置在PHP源码的SAPI目录。这个目录存放了PHP对各个服务器抽象层的代码,例如命令行程序的实现,Apache的mod_php模块实现以及fastcgi的实现等等

消息通讯样例

为了简单的表示,消息头只显示消息的类型和消息的
id,其他字段都不予以显示。下面的例子来自于官网

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "1302SERVER_PORT801316SERVER_ADDR199.170.183.42 ... "}
{FCGI_STDIN,           1, "quantity=100&item=3047936"}
{FCGI_STDOUT,          1, "Content-type: text/htmlrnrn<html>n<head> ... "}
{FCGI_END_REQUEST,     1, {0, FCGI_REQUEST_COMPLETE}}

配合上面各个结构体,则可以大致想到 FastCGI 响应器的解析和响应流程:

首先读取消息头,得到其类型为FCGI_BEGIN_REQUEST,然后解析其消息体,得知其需要的角色就是FCGI_RESPONDERflag为0,表示请求结束后关闭线路。然后解析第二段消息,得知其消息类型为FCGI_PARAMS,然后直接将消息体里的内容以回车符切割后存入环境变量。与之类似,处理完毕之后,则返回了FCGI_STDOUT消息体和FCGI_END_REQUEST消息体供
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 进程,处理完成后立即关闭。

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

LoadModule php5_module modules/mod_php5.so

FastCGI 协议分析

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

Nginx 服务器如何与 FastCGI 协同工作

Nginx 服务器无法直接与 FastCGI 服务器进行通信,需要启用
ngx_http_fastcgi_module 模块进行代理配置,才能将请求发送给 FastCGI
服务。

转载:

PHP和Apache是如何通信的?

Nginx+PHP-FPM运行原理详解

掌握CGI和FastCGI协议的运行原理

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_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

以上代码声明了pre_config,post_config,handler和child_init4个挂钩以及对应的处理函数。其中pre_config,post_config,child_init是启动挂钩,它们在服务器启动时调用。handler挂钩是请求挂钩,它在服务器处理请求时调用。其中在post_config挂钩中启动php。它通过php_apache_server_startup函数实现,php_apache_server_startup函数通过调用sapi_startup启动sapi,并通过调用php_apache2_startup来注册sapi
module
struct,最后调用php_module_startup初始化php,其中又会初始化Zend引擎,以及填充zend_module_struct中的treat_data成员(通过php_startup_sapi_content_types)等。

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-FPM

PHP-FPM即PHP-FastCGI Process Manager.

PHP-FPM是FastCGI的实现,并提供了进程管理的功能。

进程包含 master 进程和 worker 进程两种进程。

master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker
进程则一般有多个(具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP
解释器,是 PHP 代码真正执行的地方。

PHP-FPM 是 FastCGI 进程管理器(PHP FastCGI Process
Manager)( PHP 内核的
FastCGI 的大部分附加功能(或者说一种替代的 PHP FastCGI
实现),对于高负载网站是非常有用的。

处理Content-Length头部

如果应用程序提供Content-Length头,则服务器不应该发送比Content-Length更多的字节,并且应该在发送完Content-Length字节后停止发送响应,如果应用程序此时还继续尝试写入,则应该抛出错误)。

如果应用程序不提供Content-Length头部,则服务器或网关可以选择几种方法之一来处理它。最简单的是在响应完成时关闭客户端连接。但是,在某些情况下,服务器/网关可以生成一个Content-Length头部来避免关闭客户端连接。注意:应用程序和中间件的输出中不能使用任何类型的Transfer-Encoding,例如chunking或gzip,这些传输编码是Web服务器/网关的职责

ylg娱乐官网 1

8.标准输入标准输出的处理

标准输入和标准输出在上面没有一起讨论,实际在cgi_sapi_module结构体中有定义,但是cgi_sapi_module这个sapi_module_struct结构体与其他代码耦合太多,我自己也没深入的理解,这里简单做下比较,希望其他网友予以指点、补充。

cgi_sapi_module中定义了sapi_cgi_read_post来处理POST数据的读取.

while (read_bytes < count_bytes) {
    fcgi_request *request = (fcgi_request*) SG(server_context);
    tmp_read_bytes = fcgi_read(request, buffer + read_bytes, count_bytes - read_bytes);
    read_bytes += tmp_read_bytes;
}

fcgi_read中则对FCGI_STDIN的数据进行读取。
同时cgi_sapi_module中定义了sapi_cgibin_ub_write来接管输出处理,而其中又调用了sapi_cgibin_single_write,最后实现了FCGI_STDOUT FastCGI
数据包的封装.

fcgi_write(request, FCGI_STDOUT, str, str_length);

CGI 与 FastCGI 架构

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

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

但是在 FastCGI 中完全不一样。

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

中间件:可以扮演两种角色

中间件是这样一种对象,它既可以作为服务器端跟应用端交互,也可以作为应用端跟服务器端交互。中间件组件通常具备下面几个功能:

  • 在重写了环境变量后,根据目标URL将请求路由到不同的应用对象。
  • 允许多个应用或框架在同一个进程中依次执行。
  • 通过转发请求和响应,支持负载均衡和远程处理。
  • 支持对内容进行后续处理。

中间件的存在对于接口的“服务器/网关”和“应用/框架”这两端是透明的,并不需要特别的支持。大多数情况下,中间件必须符合WSGI的服务器和应用程序端的限制和要求。

3、调用listen函数将新创建的套接字作为监听,等待客户端发起的连接,当客户端有多个连接连接到这个套接字时,可能需要排队处理;

消息的发送顺序

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

ylg娱乐官网 2

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

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

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

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

介绍

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

分析

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

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

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

2、调用bind函数将服务器的本地地址与前面创建的套接字绑定;

PHP 中的 FastCGI 的实现

下面对代码的解读笔记只是我个人知识的一个梳理提炼,如有勘误,请大家指出。对不熟悉该代码的同学来说可能是一个引导,初步认识,如果觉得很模糊不清晰,那么还是需要自己逐行去阅读。

php-src/sapi/cgi/cgi_main.c为例进行分析说明,假设开发环境为 unix
环境。main 函数中一些变量的定义,以及 sapi
的初始化,我们就不讨论在这里讨论了,只说明关于 FastCGI 相关的内容。

PHP-FPM如何工作的?

PHP-FPM 进程管理器有两种进程组成,一个 Master 进程和多个 Worker
进程。Master 进程负责监听端口,接收来自 Web 服务器的请求,然后指派具体的
Worker 进程处理请求;worker 进程则一般有多个
(依据配置决定进程数),每个进程内部都嵌入了一个 PHP 解释器,用来执行 PHP
代码。

规范细节

应用对象必须接受两个位置参数。为了便于说明,我们将参数命名为environ和start_response,当然你也可以用其他的名称。服务器/网关必须使用位置参数(非关键字参数)调用应用对象,如result = application(environ, start_response)

environ参数是一个字典对象,包含CGI风格的环境变量。这个对象必须是一个内置的Python字典(不是子类、UserDict等),并允许应用程序修改字典。字典还必须包含某些WSGI必需的变量(在后面的章节中介绍),还可能包含特定的服务器的扩展变量,按照约定方式进行命名。

start_response参数是一个可调用的对象,它接受两个必填的位置参数和一个可选参数。这三个参数通常命名为status,response_headers和exc_info。应用程序通常通过start_response(status,response_headers)方式调用它。

status参数是形式为“200 OK”这样的状态字符串,response_headers是描述HTTP响应头的(header_name,header_value)元组列表。可选的exc_info参数仅在应用程序捕获错误并尝试向浏览器显示错误消息时使用。start_response必须返回一个write(body_data)的可调用对象,它接受一个位置参数,该参数作为HTTP响应主体的一部分。

当被服务器调用时,应用对象必须返回一个产生零个或多个字节串的迭代,比如一个Python列表。如果应用程序返回的迭代对象具有close()方法,则服务器/网关在结束当前请求前必须调用该方法,无论请求是正常完成还是由于迭代期间的因为浏览器断开连接产生了应用程序错误而提前终止。调用close()方法是为了释放应用程序的资源。

在配置文件中添加了所示的指令后,Apache在加载模块时会根据模块名查找模块并加载。Apache的每一个模块都是以module结构体的形式存在,module结构的name属性在最后是通过宏STANDARD20_MODULE_STUFF以__FILE__体现。通过之前的指令中指定的路径找到相关的动态链接库文件后,Apache通过内部的函数获取动态链接库中的内容,并将模块的内容加载到内存中指定变量中。

准备工作

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

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

FastCGI协议运行原理

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

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

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

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

start_response

start_response是传递给应用对象的第二个参数是可调用对象(通常就是个函数),start_response(status,response_headers,exc_info
= None)。
(与所有的WSGI可调用对象参数一样,这里必须是位置参数,不能用关键字参数)。start_response用于开始HTTP响应,它必须返回一个write(body_data)的可调用对象。

status参数就是”200 OK”或者”404 Not
Found”这种状态字符串,由状态码和状态说明组成的字符串,由一个空格分开,没有周围的空格或其他字符(更多请参见RFC2616第6.1.1节)。字符串不能包含控制字符,也不能以回车、换行符或它们的组合结束。

response_headers参数是(header_name,header_value)元组的列表。它必须是一个Python列表类型,并且服务器可以任意改变其内容。每个header_name必须是一个有效的HTTP头部字段名称(由RFC2616,第4.2节定义)。header_nameheader_value不能包含任何控制字符(包括回车符或换行符)。服务器/网关负责确保向客户端发送正确的响应头部:如果应用对象省略了HTTP响应所需的头部,则服务器/网关必须添加它。例如,HTTP的Date和Server头部通常由服务器/网关提供。(注意:HTTP头部字段不区分大小写,因此在检查应用程序提供的头部时务必考虑这一点!)。禁止应用对象使用
HTTP 1.1的 hop-by-hop 特性或者头(如Keep-Alive),以及任何在
HTTP/1.0中等价的特性,或任何影响客户端到 web服务器端持久化连接的头部。

服务器应该在调用start_response的时候检查头文件中的错误,以便应用程序仍在运行时抛出错误。但是,start_response实际上并不传输响应头。相反,它必须将它们存储在服务器/网关上,以便仅在应用返回值时或者在应用首次调用write()时传输。响应头传输的这种延迟是为了确保缓冲和异步应用程序可以用错误输出来替换它们原来预期的输出,直到最后的可能时刻。例如,如果在应用程序缓冲区内生成正文时发生错误,则应用程序可能需要将status从“200
OK”更改为“500 Internal Error”。

exc_info参数(如果提供)必须是Python sys.exc_info()元组。只有在错误处理程序调用start_response的情况下,应用程序才能提供此参数。如果提供了exc_info,并且还没有发送HTTP头,start_response应该用新提供的头部替换当前存储的HTTP头部,从而允许应用程序在发生错误时“改变主意”。但是,如果此时已经发送了HTTP头部,则start_response必须再次抛出异常。

4、服务器进程调用accept函数进入阻塞状态,直到有客户进程调用connect函数而建立起一个连接;

4.在子进程中接收请求

到这里一切都还是 socket
的服务的套路。接受请求,然后调用了fcgi_read_request

fcgi_accept_request(&request)

int fcgi_accept_request(fcgi_request *req)
{
    int listen_socket = req->listen_socket;
    sa_t sa;
    socklen_t len = sizeof(sa);
    req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);

    ...

    if (req->fd >= 0) {
        // 采用多路复用的机制
        struct pollfd fds;
        int ret;

        fds.fd = req->fd;
        fds.events = POLLIN;
        fds.revents = 0;
        do {
            errno = 0;
            ret = poll(&fds, 1, 5000);
        } while (ret < 0 && errno == EINTR);
        if (ret > 0 && (fds.revents & POLLIN)) {
            break;
        }
        // 仅仅是关闭 socket 连接,不清空 req->env
        fcgi_close(req, 1, 0);
    }

    ...

    if (fcgi_read_request(req)) {
        return req->fd;
    }
}

并且把request放入全局变量sapi_globals.server_context,这点很重要,方便了在其他地方对请求的调用。

SG(server_context) = (void *) &request;

消息头信息

主要的消息头信息如下:

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

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

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

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

2.2 FastCGI实例分析

在fastcgi.c文件中,fcig_listen函数主要用于创建、绑定socket并开始监听,它走完了前面所列TCP流程的前三个阶段,

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

ylg娱乐官网 3

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

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

测试环境配置和抓包

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类型逐个分析下。

在请求初始化完成,读取请求完毕后,就该处理请求的PHP文件了。假设此次请求为PHP_MODE_STANDARD则会调用php_execute_script执行PHP文件。在此函数中它先初始化此文件相关的一些内容,然后再调用zend_execute_scripts函数,对PHP文件进行词法分析和语法分析,生成中间代码,并执行zend_execute函数,从而执行这些中间代码。

发表评论

电子邮件地址不会被公开。 必填项已用*标注