FastCGI

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

CGI 简介

CGI全称是“通用网关接口”(Common Gateway Interface),它可以让一个客户端,从网页浏览器向执行在Web服务器上的程序请求数据。CGI描述了客户端和这个程序之间传输数据的一种标准。CGI的一个目的是要独立于任何语言的,所以CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。如php,perl,tcl等。

CGI 的运行原理

  • 客户端访问某个 URL 地址之后,通过 GET/POST/PUT 等方式提交数据,并通过 HTTP 协议向 Web 服务器发出请求。
  • 服务器端的 HTTP Daemon(守护进程)启动一个子进程。然后在子进程中,将 HTTP 请求里描述的信息通过标准输入 stdin 和环境变量传递给 URL 指定的 CGI 程序,并启动此应用程序进行处理,处理结果通过标准输出 stdout 返回给 HTTP Daemon 子进程。
  • 再由 HTTP Daemon 子进程通过 HTTP 协议返回给客户端。
    上面的这段话理解可能还是比较抽象,下面我们就通过一次 GET 请求为例进行详细说明。

图2.7 CGI 运行原理示举例示意图
图2.7 CGI 运行原理示举例示意图

如图所示,本次请求的流程如下:

  • 客户端访问 http://127.0.0.1:9003/cgi-bin/user?id=1
  • 127.0.0.1 上监听 9003 端口的守护进程接受到该请求
  • 通过解析 HTTP 头信息,得知是 GET 请求,并且请求的是 /cgi-bin/ 目录下的 user 文件。
  • 将 uri 里的 id=1 通过存入 QUERY_STRING 环境变量。
  • Web 守护进程 fork 一个子进程,然后在子进程中执行 user 程序,通过环境变量获取到id
  • 执行完毕之后,将结果通过标准输出返回到子进程。
  • 子进程将结果返回给客户端。

下面是演示代码:

Web 服务器程序
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/types.h>
  5. #include <sys/socket.h>
  6. #include <arpa/inet.h>
  7. #include <netinet/in.h>
  8. #include <string.h>
  9.  
  10. #define SERV_PORT 9003
  11.  
  12. char *str_join(char *str1, char *str2);
  13.  
  14. char *html_response(char *res, char *buf);
  15.  
  16. int main(void) {
  17. int lfd, cfd;
  18. struct sockaddr_in serv_addr, clin_addr;
  19. socklen_t clin_len;
  20. char buf[1024], web_result[1024];
  21. int len;
  22. FILE *cin;
  23.  
  24. if ((lfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
  25. perror("create socket failed");
  26. exit(1);
  27. }
  28.  
  29. memset(&serv_addr, 0, sizeof(serv_addr));
  30. serv_addr.sin_family = AF_INET;
  31. serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  32. serv_addr.sin_port = htons(SERV_PORT);
  33.  
  34. if (bind(lfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) == -1) {
  35. perror("bind error");
  36. exit(1);
  37. }
  38.  
  39. if (listen(lfd, 128) == -1) {
  40. perror("listen error");
  41. exit(1);
  42. }
  43.  
  44. signal(SIGCLD, SIG_IGN);
  45.  
  46. while (1) {
  47. clin_len = sizeof(clin_addr);
  48. if ((cfd = accept(lfd, (struct sockaddr *) &clin_addr, &clin_len)) == -1) {
  49. perror("接收错误\n");
  50. continue;
  51. }
  52.  
  53. cin = fdopen(cfd, "r");
  54. setbuf(cin, (char *) 0);
  55. fgets(buf, 1024, cin); //读取第一行
  56. printf("\n%s", buf);
  57.  
  58. //============================ cgi 环境变量设置演示 ============================
  59.  
  60. // 例如 "GET /cgi-bin/user?id=1 HTTP/1.1";
  61.  
  62. char *delim = " ";
  63. char *p;
  64. char *method, *filename, *query_string;
  65. char *query_string_pre = "QUERY_STRING=";
  66.  
  67. method = strtok(buf, delim); // GET
  68. p = strtok(NULL, delim); // /cgi-bin/user?id=1
  69. filename = strtok(p, "?"); // /cgi-bin/user
  70.  
  71. if (strcmp(filename, "/favicon.ico") == 0) {
  72. continue;
  73. }
  74.  
  75. query_string = strtok(NULL, "?"); // id=1
  76. putenv(str_join(query_string_pre, query_string));
  77.  
  78. //============================ cgi 环境变量设置演示 ============================
  79.  
  80. int pid = fork();
  81.  
  82. if (pid > 0) {
  83. close(cfd);
  84. }
  85. else if (pid == 0) {
  86. close(lfd);
  87. FILE *stream = popen(str_join(".", filename), "r");
  88. fread(buf, sizeof(char), sizeof(buf), stream);
  89. html_response(web_result, buf);
  90. write(cfd, web_result, sizeof(web_result));
  91. pclose(stream);
  92. close(cfd);
  93. exit(0);
  94. }
  95. else {
  96. perror("fork error");
  97. exit(1);
  98. }
  99. }
  100.  
  101. close(lfd);
  102.  
  103. return 0;
  104. }
  105.  
  106. char *str_join(char *str1, char *str2) {
  107. char *result = malloc(strlen(str1) + strlen(str2) + 1);
  108. if (result == NULL) exit(1);
  109. strcpy(result, str1);
  110. strcat(result, str2);
  111.  
  112. return result;
  113. }
  114.  
  115. char *html_response(char *res, char *buf) {
  116. 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";
  117.  
  118. sprintf(res, html_response_template, strlen(buf), buf);
  119.  
  120. return res;
  121. }

如上代码中的重点:

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

CGI 程序(user.c)
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. // 通过获取的 id 查询用户的信息
  5. int main(void) {
  6.  
  7. //============================ 模拟数据库 ============================
  8. typedef struct {
  9. int id;
  10. char *username;
  11. int age;
  12. } user;
  13.  
  14. user users[] = {
  15. {},
  16. {
  17. 1,
  18. "mengkang.zhou",
  19. 18
  20. }
  21. };
  22. //============================ 模拟数据库 ============================
  23.  
  24.  
  25. char *query_string;
  26. int id;
  27.  
  28. query_string = getenv("QUERY_STRING");
  29.  
  30. if (query_string == NULL) {
  31. printf("没有输入数据");
  32. } else if (sscanf(query_string, "id=%d", &id) != 1) {
  33. printf("没有输入id");
  34. } else {
  35. printf("用户信息查询<br>学号: %d<br>姓名: %s<br>年龄: %d", id, users[id].username, users[id].age);
  36. }
  37.  
  38. return 0;
  39. }

将上面的 CGI 程序编译成gcc user.c -o user,放在上面web程序的 ./cgi-bin/ 目录下。代码中的第28行,从环境变量中读取前面在Web服务器守护进程中设置的环境变量,是我们演示的重点。

FastCGI 简介

FastCGI是Web服务器和处理程序之间通信的一种协议,是CGI的一种改进方案,FastCGI像是一个常驻(long-lived)型的CGI,它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI最为人诟病的fork-and-execute模式)。正是因为他只是一个通信协议,它还支持分布式的运算,所以 FastCGI 程序可以在网站服务器以外的主机上执行,并且可以接受来自其它网站服务器的请求。

FastCGI 是与语言无关的、可伸缩架构的 CGI 开放扩展,将 CGI 解释器进程保持在内存中,以此获得较高的性能。CGI 程序反复加载是 CGI 性能低下的主要原因,如果 CGI 程序保持在内存中并接受 FastCGI 进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over 特性等。

FastCGI 工作流程如下:

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

图2.8 FastCGI 运行原理示举例示意图
图2.8 FastCGI 运行原理示举例示意图

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

FastCGI 协议

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

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

FastCGI 消息类型

FastCGI 将传输的消息做了很多类型的划分,其结构体定义如下:

  1. typedef enum _fcgi_request_type {
  2. FCGI_BEGIN_REQUEST = 1, /* [in] */
  3. FCGI_ABORT_REQUEST = 2, /* [in] (not supported) */
  4. FCGI_END_REQUEST = 3, /* [out] */
  5. FCGI_PARAMS = 4, /* [in] environment variables */
  6. FCGI_STDIN = 5, /* [in] post data */
  7. FCGI_STDOUT = 6, /* [out] response */
  8. FCGI_STDERR = 7, /* [out] errors */
  9. FCGI_DATA = 8, /* [in] filter data (not supported) */
  10. FCGI_GET_VALUES = 9, /* [in] */
  11. FCGI_GET_VALUES_RESULT = 10 /* [out] */
  12. } fcgi_request_type;

消息的发送顺序

下图是一个比较常见消息传递流程

图2.9 FastCGI 消息传递流程示意图
图2.9 FastCGI 消息传递流程示意图

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

FastCGI 响应体处理完毕之后,将发送FCGI_STDOUTFCGI_STDERR,同理也可能多次连续发送。最后以FCGI_END_REQUEST表示请求的结束。需要注意的一点,FCGI_BEGIN_REQUESTFCGI_END_REQUEST分别标识着请求的开始和结束,与整个协议息息相关,所以他们的消息体的内容也是协议的一部分,因此也会有相应的结构体与之对应(后面会详细说明)。而环境变量、标准输入、标准输出、错误输出,这些都是业务相关,与协议无关,所以他们的消息体的内容则无结构体对应。

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

FastCGI 消息头

如上,FastCGI 消息分10种消息类型,有的是输入有的是输出。而所有的消息都以一个消息头开始。其结构体定义如下:

  1. typedef struct _fcgi_header {
  2. unsigned char version;
  3. unsigned char type;
  4. unsigned char requestIdB1;
  5. unsigned char requestIdB0;
  6. unsigned char contentLengthB1;
  7. unsigned char contentLengthB0;
  8. unsigned char paddingLength;
  9. unsigned char reserved;
  10. } fcgi_header;

字段解释下:

version标识FastCGI协议版本。type 标识FastCGI记录类型,也就是记录执行的一般职能。requestId标识记录所属的FastCGI请求。contentLength记录的contentData组件的字节数。

关于上面的xxB1xxB0的协议说明:当两个相邻的结构组件除了后缀“B1”和“B0”之外命名相同时,它表示这两个组件可视为估值为B1<<8 + B0的单个数字。该单个数字的名字是这些组件减去后缀的名字。这个约定归纳了一个由超过两个字节表示的数字的处理方式。

比如协议头中requestIdcontentLength表示的最大值就是 65535。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <limits.h>
  4.  
  5. int main()
  6. {
  7. unsigned char requestIdB1 = UCHAR_MAX;
  8. unsigned char requestIdB0 = UCHAR_MAX;
  9. printf("%d\n", (requestIdB1 << 8) + requestIdB0); // 65535
  10. }

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

FCGI_BEGIN_REQUEST 的定义

  1. typedef struct _fcgi_begin_request {
  2. unsigned char roleB1;
  3. unsigned char roleB0;
  4. unsigned char flags;
  5. unsigned char reserved[5];
  6. } fcgi_begin_request;

字段解释:role表示Web服务器期望应用扮演的角色。分为三个角色(而我们这里讨论的情况一般都是响应器角色)

  1. typedef enum _fcgi_role {
  2. FCGI_RESPONDER = 1,
  3. FCGI_AUTHORIZER = 2,
  4. FCGI_FILTER = 3
  5. } fcgi_role;

FCGI_BEGIN_REQUEST中的flags组件包含一个控制线路关闭的位:flags & FCGI_KEEP_CONN:如果为0,则应用在对本次请求响应后关闭线路。如果非0,应用在对本次请求响应后不会关闭线路;Web服务器为线路保持响应性。

FCGI_END_REQUEST 的定义

  1. typedef struct _fcgi_end_request {
  2. unsigned char appStatusB3;
  3. unsigned char appStatusB2;
  4. unsigned char appStatusB1;
  5. unsigned char appStatusB0;
  6. unsigned char protocolStatus;
  7. unsigned char reserved[3];
  8. } fcgi_end_request;

字段解释:

appStatus组件是应用级别的状态码。protocolStatus组件是协议级别的状态码;protocolStatus的值可能是:

  1. FCGI_REQUEST_COMPLETE:请求的正常结束。
  2. FCGI_CANT_MPX_CONN:拒绝新请求。这发生在Web服务器通过一条线路向应用发送并发的请求时,后者被设计为每条线路每次处理一个请求。
  3. FCGI_OVERLOADED:拒绝新请求。这发生在应用用完某些资源时,例如数据库连接。
  4. FCGI_UNKNOWN_ROLE:拒绝新请求。这发生在Web服务器指定了一个应用不能识别的角色时。

protocolStatus在 PHP 中的定义如下

  1. typedef enum _fcgi_protocol_status {
  2. FCGI_REQUEST_COMPLETE = 0,
  3. FCGI_CANT_MPX_CONN = 1,
  4. FCGI_OVERLOADED = 2,
  5. FCGI_UNKNOWN_ROLE = 3
  6. } dcgi_protocol_status;

需要注意dcgi_protocol_statusfcgi_role各个元素的值都是 FastCGI 协议里定义好的,而非 PHP 自定义的。

消息通讯样例

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

  1. {FCGI_BEGIN_REQUEST, 1, {FCGI_RESPONDER, 0}}
  2. {FCGI_PARAMS, 1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
  3. {FCGI_STDIN, 1, "quantity=100&item=3047936"}
  4. {FCGI_STDOUT, 1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
  5. {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

配合上面各个结构体,则可以大致想到 FastCGI 响应器的解析和响应流程:首先读取消息头,得到其类型为FCGI_BEGIN_REQUEST,然后解析其消息体,得知其需要的角色就是FCGI_RESPONDERflag为0,表示请求结束后关闭线路。然后解析第二段消息,得知其消息类型为FCGI_PARAMS,然后直接将消息体里的内容以回车符切割后存入环境变量。与之类似,处理完毕之后,则返回了FCGI_STDOUT消息体和FCGI_END_REQUEST消息体供 Web 服务器解析。

PHP中的CGI实现

PHP的CGI实现了FastCGI协议,是一个TCP或UDP协议的服务器接受来自Web服务器的请求,当启动时创建TCP/UDP协议的服务器的socket监听,并接收相关请求进行处理。随后就进入了PHP的生命周期:模块初始化,sapi初始化,处理PHP请求,模块关闭,sapi关闭等就构成了整个CGI的生命周期。

以TCP为例,在TCP的服务端,一般会执行这样几个操作步骤:

  • 调用socket函数创建一个TCP用的流式套接字;
  • 调用bind函数将服务器的本地地址与前面创建的套接字绑定;
  • 调用listen函数将新创建的套接字作为监听,等待客户端发起的连接,当客户端有多个连接连接到这个套接字时,可能需要排队处理;
  • 服务器进程调用accept函数进入阻塞状态,直到有客户进程调用connect函数而建立起一个连接;
  • 当与客户端创建连接后,服务器调用read_stream函数读取客户的请求;
  • 处理完数据后,服务器调用write函数向客户端发送应答。
    TCP上客户-服务器事务的时序如图2.6所示:

图2.6 TCP上客户-服务器事务的时序
图2.6 TCP上客户-服务器事务的时序

PHP的CGI实现从cgi_main.c文件的main函数开始,在main函数中调用了定义在fastcgi.c文件中的初始化,监听等函数。对比TCP的流程,我们查看PHP对TCP协议的实现,虽然PHP本身也实现了这些流程,但是在main函数中一些过程被封装成一个函数实现。对应TCP的操作流程,PHP首先会执行创建socket,绑定套接字,创建监听:

  1. if (bindpath) {
  2. fcgi_fd = fcgi_listen(bindpath, 128); // 实现socket监听,调用fcgi_init初始化
  3. ...
  4. }

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

  1. if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 ||
  2. ...
  3. bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 ||
  4. listen(listen_socket, backlog) < 0) {
  5. ...
  6. }

当服务端初始化完成后,进程调用accept函数进入阻塞状态,在main函数中我们看到如下代码:

  1. while (parent) {
  2. do {
  3. pid = fork(); // 生成新的子进程
  4. switch (pid) {
  5. case 0: // 子进程
  6. parent = 0;
  7.  
  8. /* don't catch our signals */
  9. sigaction(SIGTERM, &old_term, 0); // 终止信号
  10. sigaction(SIGQUIT, &old_quit, 0); // 终端退出符
  11. sigaction(SIGINT, &old_int, 0); // 终端中断符
  12. break;
  13. ...
  14. default:
  15. /* Fine */
  16. running++;
  17. break;
  18. } while (parent && (running < children));
  19.  
  20. ...
  21. while (!fastcgi || fcgi_accept_request(&request) >= 0) {
  22. SG(server_context) = (void *) &request;
  23. init_request_info(TSRMLS_C);
  24. CG(interactive) = 0;
  25. ...
  26. }

如上的代码是一个生成子进程,并等待用户请求。在fcgi_accept_request函数中,程序会调用accept函数阻塞新创建的进程。当用户的请求到达时,fcgi_accept_request函数会判断是否处理用户的请求,其中会过滤某些连接请求,忽略受限制客户的请求,如果程序受理用户的请求,它将分析请求的信息,将相关的变量写到对应的变量中。其中在读取请求内容时调用了safe_read方法。如下所示:[main() -> fcgi_accept_request() -> fcgi_read_request() -> safe_read()]

  1. static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count)
  2. {
  3. size_t n = 0;
  4. do {
  5. ... // 省略 对win32的处理
  6. ret = read(req->fd, ((char*)buf)+n, count-n); // 非win版本的读操作
  7. ... // 省略
  8. } while (n != count);
  9.  
  10. }

如上对应服务器端读取用户的请求数据。

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

在处理完用户的请求后,服务器端将返回信息给客户端,此时在main函数中调用的是fcgi_finish_request(&request, 1);fcgi_finish_request函数定义在fastcgi.c文件中,其代码如下:

  1. int fcgi_finish_request(fcgi_request *req, int force_close)
  2. {
  3. int ret = 1;
  4.  
  5. if (req->fd >= 0) {
  6. if (!req->closed) {
  7. ret = fcgi_flush(req, 1);
  8. req->closed = 1;
  9. }
  10. fcgi_close(req, force_close, 1);
  11. }
  12. return ret;
  13. }

如上,当socket处于打开状态,并且请求未关闭,则会将执行后的结果刷到客户端,并将请求的关闭设置为真。将数据刷到客户端的程序调用的是fcgi_flush函数。在此函数中,关键是在于答应头的构造和写操作。程序的写操作是调用的safe_write函数,而safe_write函数中对于最终的写操作针对win和linux环境做了区分,在Win32下,如果是TCP连接则用send函数,如果是非TCP则和非win环境一样使用write函数。如下代码:

  1. #ifdef _WIN32
  2. if (!req->tcp) {
  3. ret = write(req->fd, ((char*)buf)+n, count-n);
  4. } else {
  5. ret = send(req->fd, ((char*)buf)+n, count-n, 0);
  6. if (ret <= 0) {
  7. errno = WSAGetLastError();
  8. }
  9. }
  10. #else
  11. ret = write(req->fd, ((char*)buf)+n, count-n);
  12. #endif

在发送了请求的应答后,服务器端将会执行关闭操作,仅限于CGI本身的关闭,程序执行的是fcgi_close函数。fcgi_close函数在前面提的fcgi_finish_request函数中,在请求应答完后执行。同样,对于win平台和非win平台有不同的处理。其中对于非win平台调用的是write函数。

以上是一个TCP服务器端实现的简单说明。这只是我们PHP的CGI模式的基础,在这个基础上PHP增加了更多的功能。在前面的章节中我们提到了每个SAPI都有一个专属于它们自己的sapi_module_struct结构:cgi_sapi_module,其代码定义如下:

  1. /* {{{ sapi_module_struct cgi_sapi_module
  2. */
  3. static sapi_module_struct cgi_sapi_module = {
  4. "cgi-fcgi", /* name */
  5. "CGI/FastCGI", /* pretty name */
  6.  
  7. php_cgi_startup, /* startup */
  8. php_module_shutdown_wrapper, /* shutdown */
  9.  
  10. sapi_cgi_activate, /* activate */
  11. sapi_cgi_deactivate, /* deactivate */
  12.  
  13. sapi_cgibin_ub_write, /* unbuffered write */
  14. sapi_cgibin_flush, /* flush */
  15. NULL, /* get uid */
  16. sapi_cgibin_getenv, /* getenv */
  17.  
  18. php_error, /* error handler */
  19.  
  20. NULL, /* header handler */
  21. sapi_cgi_send_headers, /* send headers handler */
  22. NULL, /* send header handler */
  23.  
  24. sapi_cgi_read_post, /* read POST data */
  25. sapi_cgi_read_cookies, /* read Cookies */
  26.  
  27. sapi_cgi_register_variables, /* register server variables */
  28. sapi_cgi_log_message, /* Log message */
  29. NULL, /* Get request time */
  30. NULL, /* Child terminate */
  31.  
  32. STANDARD_SAPI_MODULE_PROPERTIES
  33. };
  34. /* }}} */

同样,以读取cookie为例,当我们在CGI环境下,在PHP中调用读取Cookie时,最终获取的数据的位置是在激活SAPI时。它所调用的方法是read_cookies。由SAPI实现来实现获取cookie,这样各个不同的SAPI就能根据自己的需要来实现一些依赖环境的方法。

  1. SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);

所有使用PHP的场合都需要定义自己的SAPI,例如在第一小节的Apache模块方式中,sapi_module是apache2_sapi_module,其对应read_cookies方法的是php_apache_sapi_read_cookies函数,而在我们这里,读取cookie的函数是sapi_cgi_read_cookies。从sapi_module结构可以看出flush对应的是sapi_cli_flush,在win或非win下,flush对应的操作不同,在win下,如果输出缓存失败,则会和嵌入式的处理一样,调用php_handle_aborted_connection进入中断处理程序,而其它情况则是没有任何处理程序。这个区别通过cli_win.c中的PHP_CLI_WIN32_NO_CONSOLE控制。

参考资料

原文: http://www.php-internals.com/book?p=chapt02/02-02-03-fastcgi