一、FastCGI 协议
1、FastCGI报文格式
FastCGI报文格式如下:
typedef struct {
unsigned char version; //版本
unsigned char type; //类型
unsigned char requestIdB1; //请求Id
unsigned char requestIdB0;
unsigned char contentLengthB1; //负载长度
unsigned char contentLengthB0;
unsigned char paddingLength; //填充长度
unsigned char reserved; //保留字节
unsigned char contentData[contentLength]; //负载数据
unsigned char paddingData[paddingLength]; //填充数据
} FCGI_Record;
FastCGI报文是8字节对齐的,其中报头为前8个字节为报头,其中报头可以用下面结构表示:
typedef struct {
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;
2、FastCGI报文解析
1) version : 表示FastCGI版本
有如下值:
#define FCGI_VERSION_1 1
2) type:表示报文的类型
有如下值:
/#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)
其中主要关注:
FCGI_BEGIN_REQUEST: 请求开始,由客户端发起的请求。
FCGI_PARAMS:fastcgi参数,即一些服务器变量,如HTTP_USER_AGENT。应该是由web服务器发送的。
FCGI_STDIN:请求数据。由客户端post的数据。
FCGI_STDOUT:应答数据。由我们作出的响应。
FCGI_STDERR:出错数据。由我们作为的错误响应。
FCGI_END_REQUEST:请求结束。由我们响应。 当我们发送该报文,表示一次http请求结束。
3)requestId:请求id
当一次请求时,请求id相同。
其中requestIdB1为高8位,requestIdB0为低8位。
requestId = requestIdB1 << 8 + requestIdB0
4)contentLength:负载数据长度
其中contentLengthB1为高8位,contentLengthB0为低8位。
contentLength = contentLengthB1 << 8 + contentLengthB0
5)paddingLength:填充数据长度
因为FastCGI报文是8字节对齐的,当负载数据长度%8!=0时,需要填充数据使其8字节对齐。
paddingLength = (8 - contentLength % 8) % 8;
可知paddingLength<8。
6)reserved:保留字节
该字节保留,使FastCGI报头长度为8字节。
7)contentData:负载数据
长度为contentLength。
8)paddingData:填充数据
长度为paddingLength。
3、请求开始报文
当报头类型type为FCGI_BEGIN_REQUEST时,该报文为请求开始报文,当用户发起一个http请求时,开始到达的第一个报文。请求开始报文的负载规定为8个字节。格式如下:
typedef struct {
unsigned char roleB1;
unsigned char roleB0;
unsigned char flags;
unsigned char reserved[5];
} FCGI_BeginRequestBody;
1)role
roleB1为高8位,roleB0为低8位,role可以取以下值:
/#define FCGI_RESPONDER 1
/#define FCGI_AUTHORIZER 2
/#define FCGI_FILTER 3
一般处理FCGI_RESPONDER即可。
2)flags
当flags为0时,表示本次请求完毕后关闭链接,为1时则不关闭链接。
3)reserved:保留字节
使8字节对齐,无需填充字节。可知报头paddingLength=0
4、fastcgi参数报文
报头类型type为FCGI_PARAMS时表示该报文为fastcgi报文,fastcgi参数报文由web代理服务器整理发送的(应该就是这样的)。为多个name-value对,负载格式如下:
typedef struct { //该定义非C++标准定义
unsigned char nameLengthB3;
[
unsigned char nameLengthB2;
unsigned char nameLengthB1;
unsigned char nameLengthB0;
]
unsigned char valueLengthB3;
[
unsigned char valueLengthB2;
unsigned char valueLengthB1;
unsigned char valueLengthB0;
]
unsigned char nameData[nameLength];
unsigned char valueData[valueLength];
}FCGI_NameValuePair;
1)nameLength:名字长度
当nameLength的值大于127,则nameLength用4字节存储
nameLength = (nameLengthB3 << 24) + (nameLengthB2 << 16) + (nameLengthB1 << 8) + nameLengthB0;
当nameLength的值不超过127,则nameLength用1字节存储。
nameLength = nameLengthB3;
2)valueLength:值长度
类似nameLength。
当valueLength的值大于127,则valueLength用4字节存储
valueLength= (valueLengthB3 << 24) + (valueLengthB2 << 16) + (valueLengthB1 << 8) + valueLengthB0;
当valueLength的值不超过127,则valueLength用1字节存储。
valueLength= valueLengthB3;
所以nameLength+valueLength的所占的字节数可以为1+1=2, 1+4=5, 4+1=5, 4+4=8。
3)nameData,valueData
name数据和value数据。如在nginx做反向代理时,一般的fastCGI参数可以
在/usr/local/nginx/conf/fastcgi_param文件看到。大致解析如下:
vi /usr/local/nginx/conf/fastcgi_param
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; #脚本文件请求的路径
fastcgi_param QUERY_STRING $query_string; #请求的参数;如?app=123
fastcgi_param REQUEST_METHOD $request_method; #请求的动作(GET,POST)
fastcgi_param CONTENT_TYPE $content_type; #请求头中的Content-Type字段
fastcgi_param CONTENT_LENGTH $content_length; #请求头中的Content-length字段。
fastcgi_param SCRIPT_NAME $fastcgi_script_name; #脚本名称
fastcgi_param REQUEST_URI $request_uri; #请求的地址不带参数
fastcgi_param DOCUMENT_URI $document_uri; #与$uri相同。
fastcgi_param DOCUMENT_ROOT $document_root; #网站的根目录。在server配置中root指令中指定的值
fastcgi_param SERVER_PROTOCOL $server_protocol; #请求使用的协议,通常是HTTP/1.0或HTTP/1.1。
fastcgi_param GATEWAY_INTERFACE CGI/1.1; #cgi 版本
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; #nginx 版本号,可修改、隐藏
fastcgi_param REMOTE_ADDR $remote_addr; #客户端IP
fastcgi_param REMOTE_PORT $remote_port; #客户端端口
fastcgi_param SERVER_ADDR $server_addr; #服务器IP地址
fastcgi_param SERVER_PORT $server_port; #服务器端口
fastcgi_param SERVER_NAME $server_name; #服务器名,域名在server配置中指定的server_name
#fastcgi_param PATH_INFO $path_info; #可自定义变量
5、请求数据报文
报头类型type为FCGI_STDIN时表示该报文为请求数据的报文,即用户通过http协议post上来的数据。直接获取数据即可,数据长度为contentLength,而填充数据则抛弃。
FastCGI报文的负载的最大长度为0xFFFF(因为contentLength为2字节的)。当一个报文不够大,则会有多个,所以fastcgi参数报文、请求数据报文、应答数据报文、出错数据报文都可能有多个。除了请求开始报文和请求结束报文外,当某类报文发送完成后,都会发送一个该类的一个空报文。
6、应答数据报文
应答数据报文为我们做出应答的报文。将type设为FCGI_STDOUT,requestId设为与请求id相同,负载为返回的数据。注意负载大于0xFFFF时要分块。当发送完成后,加一个空的FCGI_STDOUT报文表示应答报文发送完毕。
7、出错数据报文
出错数据报文为我们做出应答的出错报文。将type设为FCGI_STDERR,requestId设为与请求id相同,负载为返回的出错数据。注意负载大于0xFFFF时要分块。当发送完成后,加一个空的FCGI_STDERR报文表示应答报文发送完毕。
8、请求结束报文
该报文为我们做出全部应答后发送给客户端的报文,表示该请求结束。请求报文的负载固定为下面格式:
typedef struct {
unsigned char appStatusB3;
unsigned char appStatusB2;
unsigned char appStatusB1;
unsigned char appStatusB0;
unsigned char protocolStatus;
unsigned char reserved[3];
} FCGI_EndRequestBody;
1)appStatus:返回的状态码
appStatus占4个字节,B3为最高字节,B0位最低字节,appStatus默认为0即可。
2)protocolStatus:协议状态
值为:
/#define FCGI_REQUEST_COMPLETE 0
/#define FCGI_CANT_MPX_CONN 1
/#define FCGI_OVERLOADED 2
/#define FCGI_UNKNOWN_ROLE 3
一般为FCGI_REQUEST_COMPLETE即可。
3)reserved:保留的3个字节
二、nginx反向代理及FastCGI的实现
1、nginx反向代理
设置为unix域套接字,nginx将用户的http请求通过FastCGI协议发送到unix套接字上,C++程序通过监听及读取套接字来进行操作。
伪代码如下:
int main(int argc, char *argv[])
{
fd = socket(AF_LOCAL); //创建unix域套接字
bind(fd, unix_path);
sockfd = accept(fd); //http请求到来,建立了链接
FCGI_Record record;
read(&record); //得到请求开始报文
doParseRequestBegin(record); //解析请求开始报文
do
{
read(&record); //得到param报文
doParseRequestParam(record); //解析和合并1个或多个param报文
} while(参数对报文还没读完); //param报文长度不为0
do
{
read(&record); //得到请求数据报文
doParseRequestStdin(record); //解析和合并1个或多个请求数据报文
} while(stdin报文还没读完); //stdin报文的长度不为0
string outData;
processOutData(outData); //加工响应数据
processStdout(recordList, outData); //将数据分块(如果超出报文最大长度)
for (int i = 0; i < recordList; i++) //发送数据块给客户端
write(recordList.get(i));
generateEmptyStdout(record); //生成空的stdout报文
write(record); //发送空stdout表示stdout结束
string errData;
processErrData(errData); //加工出错数据(如果有的话)
processStdout(recordList, errData); //出错数据分块
for (int i = 0; i < recordList; i++)
write(recordList.get(i)); //发送数据块给客户端
generateEmptyStderr(record); //生成空的stderr报文
write(record); //发送空stderr表示stderr结束(即使没有出错数据也发送)
generateRequestEnd(record); //生成请求结束报文
write(record); //发送请求结束报文
//一次请求结束
}