1.序言
最近在用c++实现一个简易web服务器。在实现之前大体理了一下一个web服务器主要咋样实现。当时单纯的认为只需要将浏览器发来的http请求解析,找到对应的文件,然后给浏览器一个响应,把其要获取的文件发给它就完事。可是写了没几天就遇到了瓶颈。因为我们的浏览器并不能解析动态的php文件,那么如果获取的http请求的请求文件为.php文件我们该杂么办呢?
2.将.php转换为.html的php-fpm
我们都知道php是一门解释型语言,当php在执行时,php内核都会为我们调用php解释器来帮我们解释程序。而我们的php-fpm相比php内核也具有此功能(事实上,php最新版本以把php-fpm当做一个补丁打在php内核之中了)
php-fpm的安装方式为
sudo apt-get install php5-fpm
显然php-fpm能够帮我们解决1中遇到的问题。具体解决过程为:当我们的web服务器收到.php文件时,就让php-fpm把其解释(翻译)成html格式的文件(事实上php-fpm正是干这个事的),然后我们的web服务器将翻译出来的.html文件发给浏览器就万事大吉了。。
3.php-fpm与webserver交互问题
我想我在2中已经用最少的文字阐述清楚了当我们收到.php文件应该杂么办了。道理的确简单,问题在于我们如何才能让php-fpm帮我们解析我们想要翻译成.html文件的.php文件呢?(要知道php-fpm可不是人,它可听不懂人话的)。为了能与php-fpm愉快的合作,我们就得使用它能听得懂的语言(协议)。说道这fastcgi就正式登场了,没错php-fpm所能听的懂的语言正是fastcgi,我们可以通过fastcgi协议来和php-fpm进行交互来达成我们的目的
4.fastcgi协议
如果读者之前有学习过http或者TCP/IP协议,那么我想通过我接下来简单粗暴的讲解你一定能很快掌握此协议
(1)消息头(请求头)
对于fastcgi这种数据收发协议来说,它所发送的每次请求或是回复(我之后的叙述中叫它们为消息)都有一个可提取的公共部分就是FCGI_Header(请求头),及不管每次发送的是什么消息,都必须会有一个如下格式的请求头
其c语言格式(其支持各种语言)定义如下
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; //保留字节
}FCGI_Header;
观察上面的消息请求头我们可以发现一个请求头只有8个字节大小,其每个字段的解释如下
.version:用来表示FCGI的版本信息,如果是web服务器给php-fpm发送的消息,请求头中只需要将其置0就可以
.type:此字段用来说明每次所发送消息的类型,其具体值可以为如下
type值 | 具体含义 |
---|---|
1 | 在与php-fpm建立连接之后发送的第一个消息中的type值就得为1,用来表明此消息为请求开始的第一个消息 |
2 | 异常断开与php-fpm的交互 |
3 | 在与php-fpm交互中所发的最后一个消息中type值为此,以表明交互的正常结束 |
4 | 在交互过程中给php-fpm传递环境参数时,将type设为此,以表明消息中包含的数据为某个name-value对 |
5 | web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为5 |
6 | php-fpm给web服务器回的正常响应消息的type就设为6 |
7 | php-fpm给web服务器回的错误响应设为7 |
还有一些其他的不常用的type值,我们这里就不提了,上述type值的详细含义可以在这里获得
戳这里
.requestId:此字段占俩个字节,它表示这某个特有的交互,因为php-fpm(可以理解为服务器)可以同时处理多个交互
.contentLength:此字段也占2个字节,它用来表示此消息中的消息体中数据的长度(我们上面一直说的请求头也可以叫其消息头),我们可以据此在读消息时,能够知道读多长能读出一条完整的消息
.paddingLength:填充长度的值,为了提高处理消息的能力,我们的每个消息大小都必须为8的倍数,此长度标示,我们在消息的尾部填充的长度
.reserved:保留字段
以上便是我们对消息头8字节中每个字节所表达的含义的分析
下图为php-fpm给web服务器传输的一个具体消息的消息头(8字节)内容
.序列0(对应version字段)的数值为01,代表php-fpm的版本信息
.序列1(对应type字段)的数值为03,根据上面对type值含义的解释,可以知道这个消息将标志这此次交互的结束
.序列2,3 00,01说明此次交互的请求ID为01
.序列4,5 00,08标示这在序列7之后的消息体的长度为8
.序列6标示填充字节为0,及本身消息体以是8的字节了
.序列7将消息的保留字节设为0
通过上述具体实例,我想大家大概可以明白当我们得到fastcgi协议的一个消息头,我们能获取那些信息
(2)消息体(请求体)
(1)中我们简单介绍了一个消息的消息头,由于所有的消息都共用这个消息头,所以介绍起来也简单,但是消息体就不同了。对于请求开始(及一次交互的第一个)的消息,有其自己的消息体格式,对于请求结束(一次交互的最后一个)的消息,有其自己的消息体格式,对于传递PARAMS参数……消息头type字段标示的消息类型不同,对应的消息体的格式就可能不同
接下来我会根据type值的不同来介绍各类消息体结构
1.type为1
读者对照上面介绍的type值的含义可知,此类消息为交互刚开始所发的第一个消息,其消息体结构c定义如下
typedef struct
{
unsigned char roleB1; //web服务器所期望php-fpm扮演的角色,具体取值下面有
unsigned char roleB0;
unsigned char flags; //确定php-fpm处理完一次请求之后是否关闭
unsigned char reserved[5]; //保留字段
}FCGI_BeginRequestBody;
根据上述可知type值为1的消息(标识开始请求)的消息的消息体为固定大小8字节,其中各个字段的具体含义如下
.role:此字段占2个字节,用来说明我们对php-fpm发起请求时,我们想让php-fpm为我们扮演什么角色(做什么,或理解为杂么做),其常见的3个取值如下:
role值 | 具体含义 |
---|---|
1 | 最常用的值,php-fpm接受我们的http所关联的信息,并产生个响应 |
2 | php-fpm会对我们的请求进行认证,认证通过的其会返回响应,认证不通过则关闭请求 |
3 | 过滤请求中的额外数据流,并产生过滤后的http响应 |
.flags:字段确定是否与php-fpm建立长连接,为1长连接,为0则在每次请求处理结束之后关闭连接
.reserved:保留字段
2.type值为3
type值为3表示结束消息,其消息体的c定义如下
typedef struct
{
unsigned char appStatusB3; //结束状态,0为正常
unsigned char appStatusB2;
unsigned char appStatusB1;
unsigned char appStatusB0;
unsigned char protocolStatus; //协议状态
unsigned char reserved[3];
}FCGI_EndRequestBody;
同样我们可以看出结束消息体也为固定8字节大小
其各字段的具体含义如下ph
.appStatus:此字段共4个字节,用来表示结束状态,0为正常结束
.protocolStatus:为协议所处的状态,0为正常状态
.reserved:为保留字节
3.type为4
此值表示此消息体为传递PARAMS(环境参数),环境参数其实就是name-value对,我们可以使用自己定义的name-value传给php-fpm或者传递php-fpm已有的name-value对,以下为我们后面实例将会使用到的php-fpm以有的name-value对如下
name | value |
---|---|
SCRIPT_FILENAME | value值为具体.php文件所处的位置php-fpm将根据value值找到所要处理的.php文件 |
REQUEST_METHOD | value值一般为GET,表示http请求的方式 |
好了回到主体,当我们要传递消息体为环境参数时,我们的消息体的格式如下
typedef struct {
unsigned char nameLengthB3; /* nameLengthB0 >> 7 == 0 */
unsigned char nameLengthB2;
unsigned char nameLengthB1;
unsigned char nameLengthB0;
unsigned char valueLengthB3; /* nameLengthB0 >> 7 == 0 */
unsigned char valueLengthB2;
unsigned char valueLengthB1;
unsigned char valueLengthB0;
unsigned char nameData[(B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
unsigned char valueData[valueLength
((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValue;
可以看出消息体前8个字节为固定的,其字段具体含义为
.nameLength:此字段占用4字节,用来说明name的长度
.valueLength:此字段为4个字节,用来说明value的长度
前8个字节之后紧跟的为nameLength长度的name值,接着是valueLength长度的value值
4.type值为5,6,7
当消息为输入,输出,错误时,它的消息头之后便直接跟具体数据
(3)完整消息record
fastcgi将一个完整的消息称为record,我们每次发送的单位就是record。通过上面的介绍,我们可以总结出常见的记录格式
type值 | record |
---|---|
1 | header(消息头) + 开始请求体(8字节) |
3 | header + 结束请求体(8字节) |
4 | header + name-value长度(8字节) + 具体的name-value |
5,6,7 | header + 具体内容 |
5.fastcgi协议使用实例
前面我们已经介绍了php-fpm的安装方法,其安装好之后,默认的通讯方式为unix本地域套接字通讯,具体查看
/etc/php5/fpm/pool.d/www.conf
截图如下
而我们的接下来的实例与php-fpm为TCP通讯的方式,所以,我们得在这里改下配置,具体修改结果如下所示
我将ip地址设成了127.0.0.1 监听端口设为9000
修改之后需要重启php-fpm进程,如下
sudo service php5-fpm restart
好了准备工作结束,此时编译运行这里的代码,我们就可以将内容为
<html>
<body>
<?php
echo "hello";
?>
</body>
</html>
运行程序结果如下