自己动手实现一个web服务器(支持PHP)

本文介绍的是一个单进程,支持PHP,能够处理GET、POST请求的web服务器,项目地址为:https://github.com/jaykizhou/php-server/

程序大致流程图如下:

自己动手实现一个web服务器(支持PHP)_第1张图片

web服务器在指定端口等待用户请求连接,当有连接请求时,执行的是server.c中的doit函数。

自己动手实现一个web服务器(支持PHP)_第2张图片

36~42行,读取请求行,分别提取请求方法、请求uri和HTTP协议版本。
45~49行,判断请求方法是否是GET或POST,如果不是,则向客户端发送提示信息。
52行,读取请求头,将头部数据保存到struct http_header结构体中。
55行,分析请求uri,计算请求文件的绝对路径和查询参数,并保存到struct http_header结构体中。
struct http_header结构体声明如下:

struct http_header {
    char uri[256];          // 请求地址
    char method[16];        // 请求方法
    char version[16];       // 协议版本
    char filename[256];     // 请求文件名(包含完整路径)
    char name[256];         // 请求文件名(不包含路径,只有文件名)
    char cgiargs[256];      // 查询参数
    char contype[256];      // 请求体类型
    char conlength[16];     // 请求体长度
};

58~62行,判断请求文件是否存在,如果不存在,则向客户端发送提示信息。
64~71行,如果请求文件是静态资源,并且有读取权限,则直接读取文件内容发送给客户端,具体实现见server.c中的serve_static函数,不再详述。否则,向客户端发送没有权限提示信息。
72~79行,如果请求文件是动态php文件,并且有执行权限,则调用serve_dynamic函数,否则,向客户端发送没有权限提示信息。

serve_dynamic函数通过与php-fpm通信来处理php文件。web服务器本身并没有执行php文件的能力,需要由专门的php解释器执行。而php-fpm就是一个php解释器,只要将php-fpm需要的参数(具体的参数见下面说明)发送过去,然后读取php-fpm的执行结果发送给客户端。

自己动手实现一个web服务器(支持PHP)_第3张图片

243行,创建一个套接字,连接php fastcgi服务器,即php-fpm。
246行,向php-fpm发送请求数据,包含请求文件名、请求方法、查询参数等。
249行,读取php-fpm处理结果,并发送给客户端。
对于fastcgi和php-fpm之间的关系,可参见:https://segmentfault.com/q/1010000000256516
send_fastcgi函数主要通过fastcgi协议规范规定的消息格式,发送数据给php-fpm。

自己动手实现一个web服务器(支持PHP)_第4张图片

337~345行,向php-fpm发送的各种param参数。
348~356行,对应上面参数在struct http_header结构体中的偏移位置。
359~362行,向php-fpm发送请求开始记录,表示开始请求。
365376行,向php-fpm发送337345行处定义的各个param参数。
379~382行,向php-fpm发送空的param参数记录,这是fastcgi协议规定,必须在具体param参数发送完毕后,发送一个内容为空的param参数。
385~403行,向php-fpm发送stdin数据,只有是post请求,并且有请求体数据时才会执行。首先读取请求体,然后发送给php-fpm。
406~409行,向php-fpm发送内容为空的stdin数据,同param参数,这是fastcgi协议规定。
Fastcgi协议定义了web服务器与php-fpm通信消息格式。每条消息包含消息头和消息体,消息头是一些元数据信息,用C语言表示如下:

/*
 * 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;         // 保留字节
} FCGI_Header;

version字段:标识Fastcgi协议版本,默认为1。
type字段:标识Fastcgi协议记录类型,比如开始请求记录类型、param参数记录类型、stdin数据记录类型等。
requestId字段:标识该条连接线路。requestId=requestIdB1<<8 + requestIdB0。由于结构体中每个字段类型为unsigned char类型,一个字节,当requestId的值大于一个字节的值,需要将值分开放在相邻的B1和B0中,下面contentLength同理。
contentLength字段:消息体中contentData的字节数。
paddingLength字段:消息体中paddingData的字节数。
reserved字段:暂时未用到。
消息体根据不同的记录类型,格式也不一样。
1.开始请求记录类型消息体格式C语言表示如下:

/*
 * 请求开始记录的协议体
 */
typedef struct {
    unsigned char roleB1;   // web服务器期望php-fpm扮演的角色
    unsigned char roleB0;
    unsigned char flags;    // 控制连接响应后是否立即关闭
    unsigned char reserved[5];
} FCGI_BeginRequestBody;

role字段:期望php-fpm扮演的角色,一般是响应器FCGI_RESPONDER。
flags字段:如果为0,表示请求结束后关闭该连接线路,否则不关闭。
2.结束请求记录类型消息体格式C语言表示如下:

/*
 * 结束请求记录的协议体
 */
typedef struct {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;   // 协议级别的状态码
    unsigned char reserved[3];
} FCGI_EndRequestBody;

appStatus字段:应用级别的状态码。
protocolStatus字段:协议级别的状态码,可能的值有:
FCGI_REQUEST_COMPLETE:请求的正常结束。
FCGI_CANT_MPX_CONN:拒绝新请求。
FCGI_OVERLOADED:拒绝新请求。
FCGI_UNKNOWN_ROLE:拒绝新请求。
3.字节流(FCGI_STDIN、FCGI_STDOUT、FCGI_STDERR)记录类型消息格式C语言表示如下:

typedef struct {
    unsigned char contentData[contentLength];
    unsigned char paddingData[paddingLength];
} FCGI_Body;

contentData字段:具体消息数据,contentLength值已消息头结构体中设置。
paddingData字段:填充数据,直接丢弃。由于协议规定,消息以8字节对齐,这样可以提高网络通信效率,所以当contentData部分数据不满足8字节对齐,需要填充数据。
4.名-值对流(FCGI_PARAMS)记录类型消息格式C语言表示如下:

typedef struct {
    unsigned char nameLength;
    unsigned char valueLength;
    unsigned char data[0];
} FCGI_ParamsBody;

nameLength字段:param参数名的字节数。
valueLength字段:param参数值的字节数。
data字段:依次是名和值的具体数据。
详细的fastcgi规范可参见:http://andylin02.iteye.com/blog/648412/,实现代码参见fastcgi.c文件。
另外,rio.c参照《CSAPP》一书第10章,主要是对系统read、write的包装。

你可能感兴趣的:(自己动手实现一个web服务器(支持PHP))