ホームページ >バックエンド開発 >PHPチュートリアル >FastCGI プロトコルの PHP 実装についての深い理解
FastCGI について説明する前に、従来の CGI の動作原理について説明する必要があります。同時に、CGI 1.1 プロトコルについて一般的に理解しておく必要があります
クライアントがURL アドレスを GET/POST/PUT などのデータで送信し、HTTP プロトコルを介して Web サーバーにリクエストを送信します。サーバー側の HTTP デーモンは、HTTP リクエストに記述された情報を指定された CGI プログラムに渡します。標準入力 stdin および環境変数を介してホームページにアクセスし、アプリケーションを起動します。プログラムは処理 (データベースの処理を含む) を実行し、処理結果は標準出力 stdout を介して HTTP デーモン デーモン プロセスに返され、次に HTTP が返されます。デーモン プロセスは、HTTP プロトコルを通じてクライアントにそれを返します。
上記の段落はまだ理解するのが比較的抽象的かもしれません。詳しく説明するために GET リクエストを例として使用してみましょう。
次のコードは、図で説明されている関数を実装するために使用されます。 Web サーバーはソケット リスニング サービスを開始し、CGI プログラムをローカルで実行します。より詳細なコード解釈については後ほど説明します。
#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; }
行 66 ~ 81 は CGI プログラムの相対パスを見つけます (簡単にするために、ルート ディレクトリを Web プログラムの現在のディレクトリとして直接定義します) )、CGI プログラムが子プロセスで実行されるように、CGI プログラムの実行時に読みやすくするために環境変数も同時に設定されます。 Web サーバー デーモンのキャッシュ;
97 行目は、ラップされた HTML 結果をクライアント ソケット記述子に書き込み、Web サーバーに接続されているクライアントに返します。
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; }
gcc user.c -o user.cgi
FastCGI の動作原理の分析
CGI/1.1 仕様と比較すると、Web サーバーは CGI プログラムを実行するためにローカルで子プロセスをフォークし、CGI の事前定義された環境変数を埋めてシステム環境変数に入れ、子プロセスにHTTPボディの内容を標準入力Passで渡し、処理完了後に標準出力でWebサーバーに返します。 FastCGI の核心は、従来のフォークして実行する方法を排除し、各起動時の膨大なオーバーヘッド (後で PHP を例として説明します) を削減し、常駐方式でリクエストを処理することです。
FastCGI プロセス マネージャーは自身を初期化し、複数の CGI インタプリタ プロセスを開始し、Web サーバーからの接続を待ちます。
Web サーバーは、Socket を通じて FastCGI プロセス マネージャーと通信し、FastCGI プロトコルを通じて CGI 環境変数と標準入力データを CGI インタープリター プロセスに送信します。
CGI インタプリタプロセスは処理完了後、同じ接続から Web サーバーに標準出力とエラー情報を返します。
CGI インタープリタ プロセスは、Web サーバーからの次の接続を待機して処理します。
もう一つ、FastCGI は CGI/1.1 に基づいたプロトコルです。CGI/1.1 で転送されるデータは、FastCGI プロトコルで定義された順序と形式で転送されます。
準備おそらく、上記の内容はまだ理解するのが非常に抽象的です。これは、第一に、FastCGI プロトコルについての一般的な理解がなく、第二に、実際のコードの学習がないためです。したがって、FastCGI プロトコルの内容を事前に学習する必要があります。必ずしも完全に理解する必要はありません。概要を理解した後、この記事を読んで学習と理解を組み合わせることができます。 FastCGI プロトコルの解析 以下は PHP の FastCGI コードと組み合わせて解析します。特に説明はありませんが、以下のコードはすべて PHP のソース コードです。 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;メッセージ送信シーケンス 次の図は、単純なメッセージ配信プロセスです 。
最先发送的是FCGI_BEGIN_REQUEST
,然后是FCGI_PARAMS
和FCGI_STDIN
,由于每个消息头(下面将详细说明)里面能够承载的最大长度是65535,所以这两种类型的消息不一定只发送一次,有可能连续发送多次。
FastCGI 响应体处理完毕之后,将发送FCGI_STDOUT
、FCGI_STDERR
,同理也可能多次连续发送。最后以FCGI_END_REQUEST
表示请求的结束。
需要注意的一点,FCGI_BEGIN_REQUEST
和FCGI_END_REQUEST
分别标识着请求的开始和结束,与整个协议息息相关,所以他们的消息体的内容也是协议的一部分,因此也会有相应的结构体与之对应(后面会详细说明)。而环境变量、标准输入、标准输出、错误输出,这些都是业务相关,与协议无关,所以他们的消息体的内容则无结构体对应。
由于整个消息是二进制连续传递的,所以必须定义一个统一的结构的消息头,这样以便读取每个消息的消息体,方便消息的切割。这在网络通讯中是非常常见的一种手段。
如上,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组件的字节数。
关于上面的xxB1
和xxB0
的协议说明:当两个相邻的结构组件除了后缀“B1”和“B0”之外命名相同时,它表示这两个组件可视为估值为B1<<8 + B0的单个数字。该单个数字的名字是这些组件减去后缀的名字。这个约定归纳了一个由超过两个字节表示的数字的处理方式。
比如协议头中requestId
和contentLength
表示的最大值就是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怎么办,则分割为多个相同类型的消息发送即可。
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服务器为线路保持响应性。
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_status
和fcgi_role
各个元素的值都是 FastCGI 协议里定义好的,而非 PHP 自定义的。
为了简单的表示,消息头只显示消息的类型和消息的 id,其他字段都不予以显示。下面的例子来自于官网
{FCGI_BEGIN_REQUEST, 1, {FCGI_RESPONDER, 0}} {FCGI_PARAMS, 1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "} {FCGI_STDIN, 1, "quantity=100&item=3047936"} {FCGI_STDOUT, 1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "} {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
配合上面各个结构体,则可以大致想到 FastCGI 响应器的解析和响应流程:
首先读取消息头,得到其类型为FCGI_BEGIN_REQUEST
,然后解析其消息体,得知其需要的角色就是FCGI_RESPONDER
,flag
为0,表示请求结束后关闭线路。然后解析第二段消息,得知其消息类型为FCGI_PARAMS
,然后直接将消息体里的内容以回车符切割后存入环境变量。与之类似,处理完毕之后,则返回了FCGI_STDOUT
消息体和FCGI_END_REQUEST
消息体供 Web 服务器解析。
下面对代码的解读笔记只是我个人知识的一个梳理提炼,如有勘误,请大家指出。对不熟悉该代码的同学来说可能是一个引导,初步认识,如果觉得很模糊不清晰,那么还是需要自己逐行去阅读。
以php-src/sapi/cgi/cgi_main.c
为例进行分析说明,假设开发环境为 unix 环境。main 函数中一些变量的定义,以及 sapi 的初始化,我们就不讨论在这里讨论了,只说明关于 FastCGI 相关的内容。
fcgi_fd = fcgi_listen(bindpath, 128);
从这里开始监听,而fcgi_listen
函数里面则完成 socket 服务前三步socket
,bind
,listen
。
为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;
这里子进程的个数默认是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));
到这里一切都还是 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;
下面的代码删除一些异常情况的处理,只显示了正常情况下执行顺序。
在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; } ... } }
假设此次请求为PHP_MODE_STANDARD
则会调用php_execute_script
执行PHP文件。这里就不展开了。
fcgi_finish_request(&request, 1);
int fcgi_finish_request(fcgi_request *req, int force_close) { int ret = 1; if (req->fd >= 0) { if (!req->closed) { ret = fcgi_flush(req, 1); req->closed = 1; } fcgi_close(req, force_close, 1); } return ret; }
在fcgi_finish_request
中调用fcgi_flush
,fcgi_flush
中封装一个FCGI_END_REQUEST
消息体,再通过safe_write
写入 socket 连接的客户端描述符。
标准输入和标准输出在上面没有一起讨论,实际在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);
以上がFastCGI プロトコルの PHP 実装についての深い理解の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。