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

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

程序大致流程图如下:

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

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的执行结果发送给客户端。

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

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的包装。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容