fastcgi协议是在cgi协议的基础上发展出来的,如果想了解cgi协议,可以看我另一篇文章:动态web技术(二) --- cgi,fastcgi程序本身监听某个socket然后等待来自web服务器的连接,而不是像cgi程序是由web服务器 fork-exec,所以fastcgi本身是一个服务端程序,而web服务器对它来说则是客户端。
fastcgi程序和web服务器之间通过可靠的流式传输(unix domain socket或tcp)来通信,相对于传统的cgi程序,有环境变量和标准输入输出,而fastcgi程序和web服务器之间则只有一条socket连接来传输数据,所以它把数据分成以下多种消息类型:
#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)
以上由web服务器向fastcgi程序传输的消息类型有以下几种:
fcgi_begin_request 表示一个请求的开始,
fcgi_abort_request 表示服务器希望终止一个请求
fcgi_params 对应于cgi程序的环境变量,php $_server 数组中的数据绝大多数来自于此
fcgi_stdin 对应cgi程序的标准输入,fastcgi程序从此消息获取 http请求的post数据
此外 fcgi_data 和 fcgi_get_values 这里不做介绍。
由fastcgi程序返回给web服务器的消息类型有以下几种:
fcgi_stdout 对应cgi程序的标准输出,web服务器会把此消息当作html返回给浏览器
fcgi_stderr 对应cgi程序的标准错误输出, web服务器会把此消息记录到错误日志中
fcgi_end_request 表示该请求处理完毕
fcgi_unknown_type fastcgi程序无法解析该消息类型
此外还有 fcgi_get_values_result 这里不做介绍
web服务器和fastcgi程序每传输一个消息,首先会传输一个8字节固定长度的消息头,这个消息头记录了随后要传输的这个消息的 类型,长度等等属性,消息头的结构体如下:
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;
} ;
version 表示fastcgi协议版本
type 表示消息的类型,就是前面提到的多种消息类型之一,如 fcgi_begin_request、fcgi_params 等等
requestidb1 和 requestidb0 这两个字节组合来表示 requestid (对于每个请求web服务器会分配一个requestid), requestidb1 是requestid的高八位,requestidb0是低八位,所以最终的 requestid = (requestidb1 << 8) requestidb0,因为是两个字节来表示,requestid最大取值为65535, 同理 contentlengthb1 和 contentlengthb0 共同来表示消息体的长度,对于超过65535的消息体,可以切割成多个消息体来传输
paddinglength 为了使消息8字节对齐,提高传输效率,可以在消息上添加一些字节数来达到消息对齐的目的,paddinglength 为添加的字节数,这些字节是无用数据,读出来可以直接丢弃。
reserved 保留字段,暂时无用
比如在传输 fcgi_begin_request 消息之前,首先会传输一个消息头类似如下:
0x0000080001000101 粗体 08 对应 requestidb0 ,粗体00 对应 requestidb1 ,所以后续要传输的这个消息的长度是八字节,粗体01指代该消息类型为 fcgi_begin_request
对于 fcgi_begin_request 和 fcgi_end_request 消息类型,fastcgi协议分别定义了一个结构体如下,而对于其他类型的消息体,没有专门结构体与之对应,消息体就是普通的二进制数据。
struct fcgi_beginrequestbody {
unsigned char roleb1;
unsigned char roleb0;
unsigned char flags;
unsigned char reserved[5];
} ;
struct fcgi_endrequestbody {
unsigned char appstatusb3;
unsigned char appstatusb2;
unsigned char appstatusb1;
unsigned char appstatusb0;
unsigned char protocolstatus;
unsigned char reserved[3];
};
从这个结构体可以知道 fcgi_begin_request 和 fcgi_end_request 消息体的长度都是固定的8个字节。
fcgi_beginrequestbody 的 roleb1 和 roleb0 两个字节组合指代 web服务器希望fastcgi程序充当的角色,目前fastcgi协议仅定义了三种角色:
#define fcgi_responder 1
#define fcgi_authorizer 2
#define fcgi_filter 3
常见的fastcgi程序基本都是作为 fcgi_responder (响应器角色),所以roleb1的值总是0, roleb0的值可取1~3三个值,但 常见都是1,其他两种角色这里不做讨论。
flags 是一个8位掩码, web服务器可以利用该掩码告知fastcgi程序在处理完一个请求后是否关闭socket连接 (最初协议的设计者可能还预留了该掩码的其他作用,只是目前只有这一个作用)
flags & fcgi_keep_conn 的值为1,则fastcgi程序请求结束不关闭连接,为0 则关键连接
其中 fcgi_keep_conn 是一个宏,定义如下:
#define fcgi_keep_conn 1
以下是协议对于响应器角色的解释:
responder fastcgi应用程序具有与cgi / 1.1程序相同的用途:它接收与http请求相关的所有信息并生成http响应。
以下解释cgi / 1.1的每个元素和响应者角色的消息类型的对应关系:
- responder应用程序通过fcgi_params从web服务器接收cgi / 1.1环境变量。
- 接下来,responder应用程序通过fcgi_stdin从web服务器接收cgi / 1.1 stdin数据。在接收流结束指示之前,应用程序最多从此流接收content_length个字节。(仅当http客户端无法提供时,应用程序才会收到少于content_length的字节,例如,因为客户端崩溃了。)
- 响应者应用程序通过fcgi_stdout发送cgi / 1.1 标准输出数据到web服务器,通过fcgi_stderr发送cgi / 1.1 标准错误数据。应用程序并发地发送这些,而不是一个接一个地发送。应用程序必须等待读完fcgi_params数据之后才可以写fcgi_stdout和fcgi_stderr,但它不需要等待读完fcgi_stdin才可以写这两个流。
- 发送所有stdout和stderr数据后,responder应用程序将发送fcgi_end_request记录。应用程序将protocolstatus组件设置为fcgi_request_complete,将appstatus组件设置为cgi程序通过退出系统调用返回的状态代码。
处理post请求的响应方应比较fcgi_stdin上接收到的字节数和content_length,如果两个数不相等,则中止请求。
fcgi_endrequestbody 中 appstatus是应用级别的状态码。每个角色记录其对appstatus的使用情况,不做深入讨论
所述的protocolstatus 是协议级别的状态码,可能的protocolstatus值是:
#define fcgi_request_complete 0
#define fcgi_cant_mpx_conn 1
#define fcgi_overloaded 2
#define fcgi_unknown_role 3
fcgi_request_complete:请求的正常结束,典型的应该都是该个值。
fcgi_cant_mpx_conn:拒绝新的请求。当web服务器通过一个连接将并发请求发送到每个连接一次处理一个请求的应用程序时,会发生这种情况。
fcgi_overloaded:拒绝新的请求。当应用程序耗尽某些资源(例如数据库连接)时会发生这种情况。
fcgi_unknown_role:拒绝新的请求。当web服务器指定了应用程序未知的角色时,会发生这种情况。
所以,当fastcgi程序从web服务器读取数据时,总是先读取一个8字节的消息头,然后得到消息的类型和长度信息,然后再读取消息体,一种消息过长可以切割成多个消息传输,当一个消息头里的 contentlength 为0(也即 contentlengthb1和contentlengthb0 的值都为0) 时,则表明这种消息传输完毕,然后我们可以把之前读到的这种类型的多个消息合并得到最终完整的消息。反之,当我们要从fastcgi程序向web服务器返回数据时,总是每发送一个8字节消息头,紧接发送一次消息体,循环往复,直到最后发送 fcgi_end_request类型的消息头 和消息体结束请求。
总结一下web服务器和fastcgi程序之间大概的消息发送流程:
1、web服务器向fastcgi程序发送一个 8 字节 type=fcgi_begin_request的消息头和一个8字节 fcgi_beginrequestbody 结构的 消息体,标志一个新请求的开始
2、web服务器向fastcgi程序发送一个 8 字节 type=fcgi_params 的消息头 和一个消息头中指定长度的fcgi_params类型消息体
3、根据fcgi_params消息的长度可能重复步骤 2 多次,最终发送一个 8 字节 type=fcgi_params 并且 contentlengthb1 和 contentlengthb0 都为 0 的消息头 标志 fcgi_params 消息发送结束
4、以和步骤2、3相同的方式 发送 fcgi_stdin 消息
5、fastcgi程序处理完请求后 以和步骤2、3相同的方式 发送 fcgi_stdout消息 和 fcgi_stderr 消息返回给服务器
6、fastcgi程序 发送一个 type= fcgi_end_request 的消息头 和 一个8字节 fcgi_endrequestbody 结构的消息体,标志此次请求结束
fastcgi协议的完整规范请查看: https://fastcgi-archives.github.io/fastcgi_specification.html
下面用c 编写一个简单的实现fastcgi协议的demo,这个demo主要是用来向大家更直观的展示fastcgi协议,错误处理和内存泄漏检测都不到位,专业的协议实现可以看官方提供的 fastcgi developer‘s kit : https://fastcgi-archives.github.io/fastcgi_developers_kit_fastcgi.html ,其中封装的c库在 libfcgi 文件中。
完整代码托管在github上: https://github.com/zhyee/fastcgi-demo
#include
#include
#include
#include
#include
#include
#include
#include
#define head_len 8 //消息头长度固定为8个字节
#define buflen 4096
#define fcgi_version_1 1 //版本号
// 消息类型
enum fcgi_request_type {
fcgi_begin_request = 1,
fcgi_abort_request = 2,
fcgi_end_request = 3,
fcgi_params = 4,
fcgi_stdin = 5,
fcgi_stdout = 6,
fcgi_stderr = 7,
fcgi_data = 8,
fcgi_get_values = 9,
fcgi_get_values_result = 10,
fcgi_unkown_type = 11
};
// 服务器希望fastcgi程序充当的角色, 这里只讨论 fcgi_responder 响应器角色
enum fcgi_role {
fcgi_responder = 1,
fcgi_authorizer = 2,
fcgi_filter = 3
};
//消息头
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;
};
//请求开始发送的消息体
struct fcgi_beginrequestbody {
unsigned char roleb1;
unsigned char roleb0;
unsigned char flags;
unsigned char reserved[5];
};
//请求结束发送的消息体
struct fcgi_endrequestbody {
unsigned char appstatusb3;
unsigned char appstatusb2;
unsigned char appstatusb1;
unsigned char appstatusb0;
unsigned char protocolstatus;
unsigned char reserved[3];
};
// protocolstatus
enum protocolstatus {
fcgi_request_complete = 0,
fcgi_cant_mpx_conn = 1,
fcgi_overloaded = 2,
fcgi_unknown_role = 3
};
// 打印错误并退出
void halterror(char *type, int errnum)
{
fprintf(stderr, "%s: %s\n", type, strerror(errnum));
exit(exit_failure);
}
// 存储键值对的结构体
struct paramnamevalue {
char **pname;
char **pvalue;
int maxlen;
int curlen;
};
// 初始化一个键值结构体
void init_paramnv(struct paramnamevalue *nv)
{
nv->maxlen = 16;
nv->curlen = 0;
nv->pname = (char **)malloc(nv->maxlen * sizeof(char *));
nv->pvalue = (char **)malloc(nv->maxlen * sizeof(char *));
}
// 扩充一个结键值构体的容量为之前的两倍
void extend_paramnv(struct paramnamevalue *nv)
{
nv->maxlen *= 2;
nv->pname = realloc(nv->pname, nv->maxlen * sizeof(char *));
nv->pvalue = realloc(nv->pvalue, nv->maxlen * sizeof(char *));
}
// 释放一个键值结构体
void free_paramnv(struct paramnamevalue *nv)
{
int i;
for(i = 0; i < nv->curlen; i )
{
free(nv->pname[i]);
free(nv->pvalue[i]);
}
free(nv->pname);
free(nv->pvalue);
}
// 获取指定 paramname 的值
char *getparamvalue(struct paramnamevalue *nv, char *paramname)
{
int i;
for(i = 0; i < nv->curlen; i )
{
if (strncmp(paramname, nv->pname[i], strlen(paramname)) == 0)
{
return nv->pvalue[i];
}
}
return null;
}
int main(){
int servfd, connfd;
int ret, i;
struct sockaddr_in servaddr, cliaddr;
socklen_t slen, clen;
struct fcgi_header header, headerbuf;
struct fcgi_beginrequestbody brbody;
struct paramnamevalue paramnv;
struct fcgi_endrequestbody erbody;
ssize_t rdlen;
int requestid, contentlen;
unsigned char paddinglen;
int paramnamelen, paramvaluelen;
char buf[buflen];
unsigned char c;
unsigned char lenbuf[3];
char *paramname, *paramvalue;
char *htmlhead, *htmlbody;
/*socket bind listen*/
servfd = socket(af_inet, sock_stream, 0);
if (servfd == -1)
{
halterror("socket", errno);
}
slen = clen = sizeof(struct sockaddr_in);
bzero(&servaddr, slen);
//这里让 fastcgi程序监听 127.0.0.1:9000 和 php-fpm 监听的地址相同, 方便我们用 nginx 来测试
servaddr.sin_family = af_inet;
servaddr.sin_port = htons(9000);
servaddr.sin_addr.s_addr = htonl(inaddr_loopback);
ret = bind(servfd, (struct sockaddr *)&servaddr, slen);
if (ret == -1)
{
halterror("bind", errno);
}
ret = listen(servfd, 16);
if (ret == -1)
{
halterror("listen", errno);
}
while (1)
{
bzero(&cliaddr, clen);
connfd = accept(servfd, (struct sockaddr *)&cliaddr, &clen);
if (connfd == -1)
{
halterror("accept", errno);
break;
}
fcntl(connfd, f_setfl, o_nonblock); // 设置socket为非阻塞
init_paramnv(¶mnv);
while (1) {
//读取消息头
bzero(&header, head_len);
rdlen = read(connfd, &header, head_len);
if (rdlen == -1)
{
// 无数据可读
if (errno == eagain)
{
break;
}
else
{
halterror("read", errno);
}
}
if (rdlen == 0)
{
break; //消息读取结束
}
headerbuf = header;
requestid = (header.requestidb1 << 8) header.requestidb0;
contentlen = (header.contentlengthb1 << 8) header.contentlengthb0;
paddinglen = header.paddinglength;
printf("version = %d, type = %d, requestid = %d, contentlen = %d, paddinglength = %d\n",
header.version, header.type, requestid, contentlen, paddinglen);
printf("%lx\n", header);
switch (header.type) {
case fcgi_begin_request:
printf("******************************* begin request *******************************\n");
//读取开始请求的请求体
bzero(&brbody, sizeof(brbody));
read(connfd, &brbody, sizeof(brbody));
printf("role = %d, flags = %d\n", (brbody.roleb1 << 8) brbody.roleb0, brbody.flags);
break;
case fcgi_params:
printf("begin read params...\n");
// 消息头中的contentlen = 0 表明此类消息已发送完毕
if (contentlen == 0)
{
printf("read params end...\n");
}
//循环读取键值对
while (contentlen > 0)
{
/*
fcgi_params 以键值对的方式传送,键和值之间没有'=',每个键值对之前会分别用1或4个字节来标识键和值的长度 例如:
\x0b\x02server_port80\x0b\x0eserver_addr199.170.183.42
上面的长度是用十六进制表示的 \x0b = 11 正好为server_port的长度, \x02 = 2 为80的长度
*/
// 获取paramname的长度
rdlen = read(connfd, &c, 1); //先读取一个字节,这个字节标识 paramname 的长度
contentlen -= rdlen;
if ((c & 0x80) != 0) //如果 c 的值大于 128,则该 paramname 的长度用四个字节表示
{
rdlen = read(connfd, lenbuf, 3);
contentlen -= rdlen;
paramnamelen = ((c & 0x7f) << 24) (lenbuf[0] << 16) (lenbuf[1] << 8) lenbuf[2];
} else
{
paramnamelen = c;
}
// 同样的方式获取paramvalue的长度
rdlen = read(connfd, &c, 1);
contentlen -= rdlen;
if ((c & 0x80) != 0)
{
rdlen = read(connfd, lenbuf, 3);
contentlen -= rdlen;
paramvaluelen = ((c & 0x7f) << 24) (lenbuf[0] << 16) (lenbuf[1] << 8) lenbuf[2];
}
else
{
paramvaluelen = c;
}
//读取paramname
paramname = (char *)calloc(paramnamelen 1, sizeof(char));
rdlen = read(connfd, paramname, paramnamelen);
contentlen -= rdlen;
//读取paramvalue
paramvalue = (char *)calloc(paramvaluelen 1, sizeof(char));
rdlen = read(connfd, paramvalue, paramvaluelen);
contentlen -= rdlen;
printf("read param: %s=%s\n", paramname, paramvalue);
if (paramnv.curlen == paramnv.maxlen)
{
// 如果键值结构体已满则把容量扩充一倍
extend_paramnv(¶mnv);
}
paramnv.pname[paramnv.curlen] = paramname;
paramnv.pvalue[paramnv.curlen] = paramvalue;
paramnv.curlen ;
}
if (paddinglen > 0)
{
rdlen = read(connfd, buf, paddinglen);
contentlen -= rdlen;
}
break;
case fcgi_stdin:
printf("begin read post...\n");
if(contentlen == 0)
{
printf("read post end....\n");
}
if (contentlen > 0)
{
while (contentlen > 0)
{
if (contentlen > buflen)
{
rdlen = read(connfd, buf, buflen);
}
else
{
rdlen = read(connfd, buf, contentlen);
}
contentlen -= rdlen;
fwrite(buf, sizeof(char), rdlen, stdout);
}
printf("\n");
}
if (paddinglen > 0)
{
rdlen = read(connfd, buf, paddinglen);
contentlen -= rdlen;
}
break;
case fcgi_data:
printf("begin read data....\n");
if (contentlen > 0)
{
while (contentlen > 0)
{
if (contentlen > buflen)
{
rdlen = read(connfd, buf, buflen);
}
else
{
rdlen = read(connfd, buf, contentlen);
}
contentlen -= rdlen;
fwrite(buf, sizeof(char), rdlen, stdout);
}
printf("\n");
}
if (paddinglen > 0)
{
rdlen = read(connfd, buf, paddinglen);
contentlen -= rdlen;
}
break;
}
}
/* 以上是从web服务器读取数据,下面向web服务器返回数据 */
headerbuf.version = fcgi_version_1;
headerbuf.type = fcgi_stdout;
htmlhead = "content-type: text/html\r\n\r\n"; //响应头
htmlbody = getparamvalue(¶mnv, "script_filename"); // 把请求文件路径作为响应体返回
printf("html: %s%s\n",htmlhead, htmlbody);
contentlen = strlen(htmlhead) strlen(htmlbody);
headerbuf.contentlengthb1 = (contentlen >> 8) & 0xff;
headerbuf.contentlengthb0 = contentlen & 0xff;
headerbuf.paddinglength = (contentlen % 8) > 0 ? 8 - (contentlen % 8) : 0; // 让数据 8 字节对齐
write(connfd, &headerbuf, head_len);
write(connfd, htmlhead, strlen(htmlhead));
write(connfd, htmlbody, strlen(htmlbody));
if (headerbuf.paddinglength > 0)
{
write(connfd, buf, headerbuf.paddinglength); //填充数据随便写什么,数据会被服务器忽略
}
free_paramnv(¶mnv);
//回写一个空的 fcgi_stdout 表明 该类型消息已发送结束
headerbuf.type = fcgi_stdout;
headerbuf.contentlengthb1 = 0;
headerbuf.contentlengthb0 = 0;
headerbuf.paddinglength = 0;
write(connfd, &headerbuf, head_len);
// 发送结束请求消息头
headerbuf.type = fcgi_end_request;
headerbuf.contentlengthb1 = 0;
headerbuf.contentlengthb0 = 8;
headerbuf.paddinglength = 0;
bzero(&erbody, sizeof(erbody));
erbody.protocolstatus = fcgi_request_complete;
write(connfd, &headerbuf, head_len);
write(connfd, &erbody, sizeof(erbody));
close(connfd);
printf("******************************* end request *******************************\n");
}
close(servfd);
return 0;
}
运行结果:
先运行fastcgi程序,为了方便测试程序监听和php-fpm相同的端口,所以启程序前先关闭php-fpm
[root@localhost fastcgi-demo]# make
gcc fastcgi.c -o fastcgi
[root@localhost fastcgi-demo]# ls
fastcgi fastcgi.c makefile readme.md
[root@localhost fastcgi-demo]# ./fastcgi
然后请求一个php页面:
[root@localhost hello]# curl -i '127.0.0.1/test.php?user=tom&password=123456' -d 'gender=male&weight=60kg'
http/1.1 200 ok
server: nginx/1.11.3
date: wed, 27 dec 2017 12:16:38 gmt
content-type: text/html
transfer-encoding: chunked
connection: keep-alive
/usr/share/nginx/html/test.php[root@localhost hello]#
然后可以看到fastcgi程序的打印结果:
[root@localhost fastcgi-demo]# ./fastcgi
version = 1, type = 1, requestid = 1, contentlen = 8, paddinglength = 0
80001000101
******************************* begin request *******************************
role = 1, flags = 0
version = 1, type = 4, requestid = 1, contentlen = 717, paddinglength = 3
3cd0201000401
begin read params...
read param: script_filename=/usr/share/nginx/html/test.php
read param: query_string=user=tom&password=123456
read param: request_method=post
read param: content_type=application/x-www-form-urlencoded
read param: content_length=23
read param: script_name=/test.php
read param: request_uri=/test.php?user=tom&password=123456
read param: document_uri=/test.php
read param: document_root=/usr/share/nginx/html
read param: server_protocol=http/1.1
read param: gateway_interface=cgi/1.1
read param: server_software=nginx/1.11.3
read param: remote_addr=127.0.0.1
read param: remote_port=56896
read param: server_addr=127.0.0.1
read param: server_port=80
read param: server_name=_
read param: redirect_status=200
read param: http_user_agent=curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 nss/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
read param: http_host=127.0.0.1
read param: http_accept=*/*
read param: http_content_length=23
read param: http_content_type=application/x-www-form-urlencoded
version = 1, type = 4, requestid = 1, contentlen = 0, paddinglength = 0
1000401
begin read params...
read params end...
version = 1, type = 5, requestid = 1, contentlen = 23, paddinglength = 1
1170001000501
begin read post...
gender=male&weight=60kg
version = 1, type = 5, requestid = 1, contentlen = 0, paddinglength = 0
1000501
begin read post...
read post end....
html: content-type: text/html
/usr/share/nginx/html/test.php
******************************* end request *******************************
写在后面:大家都知道 php-fpm 实现了fastcgi协议,但php-fpm所做的事远不止于此,他还负责 进程管理(fastcgi进程数控制,重启down调的fastcgi子进程等等),初始化 php 运行环境,以及执行 php 脚本,php-fpm的实现原理值得另写一篇文章来介绍它。