一个简单的web服务器

前言

这只是一个菜鸟写出来的 简单的单线程web服务器,只支持GET和POST方法,不支持rewrite,断点续传,SSL等。
用FAST-CGI协议与PHP-fpm通信。仅仅用于检验自己的学习情况。
使用了libevent中的bufferevent并用c写的单线程服务器。下面先写出思路。
参考资料:
https://www.jianshu.com/p/31f61470a325
http://www.w3school.com.cn/tags/html_ref_httpmessages.asp
https://blog.csdn.net/zhouyongku/article/details/53431597/
https://blog.csdn.net/zhang197093/article/details/78914509
https://blog.csdn.net/agzhchren/article/details/79173491

主函数

主函数比较简单不需要多说什么,直接上流程图
配置 是一个void *类型的数组。另有一个const char *类型的 代表配置文件中键名字的 数组,末尾以NULL结束。

const char *SETTINGNAMES[] =
{
	"LISTEN", "BACKLOG", "BASEDIR", "STATIC_CONTENT_TYPE",
		"SERVER_TIMEOUT", "FCGI_LISTEN", "FCGI_PIDFILE",
		"FCGI_CONTENT_TYPE", "FCGI_TIMEOUT", "FCGI_PARAM_NAME",
		"FCGI_OPTION_NAME", NULL
};
Created with Raphaël 2.2.0 开始 读取默认配置 处理命令行选项 检查配置 debug标志? 调用daemonize 初始化 结束 yes no

初始化过程很简单,用signal屏蔽SIGPIPE,创建event_base,创建evconnlistener,开始事件循环。

配置文件

只实现了基本的读取配置文件的函数。不支持重复键,不支持section。
函数有3个参数: 文件路径(const char *), 配置(void **), 键名字(const char **)。
配置 以固定顺序存放。用宏标记位置。键的名字必须与宏对应

#define SETTINGSIZE	11
#define S_HLI		0 //监听的端口
#define S_BLO		1 //backlog
#define S_BDI		2 //basedir
#define S_SCO		3 //静态文件的配置文件路径
#define S_RTI		4 //客户端超时
#define S_CLI		5 //FAST-CGI服务器监听的ip与端口
#define S_CPP		6 //FAST-CGI的pid文件(对这个简陋的服务器来说没有多大用处)
#define S_CCO		7 //动态文件的配置文件路径
#define S_FTI		8 //与FAST-CGI服务器通信超时
#define S_SFS		9 //HTTP包头选项部分名字的配置文件
#define S_OPN		10 //

关于S_OPN则是需要传递给PHP-FPM 请求方法 请求URI 请求文件名等HTTP包头中处理后得到的东西的 名字。
但是都是以FCGI_PARAM传递给PHP-FPM。
因为读取配置文件的函数十分简陋。所以S_SCO,S_CCO,S_SFS,S_OPN都是独立的配置文件
当主函数进行到检查配置时,再读取这几个配置文件。最后把读取到的内容放到相应的地方(只处理空行与注释)
这几个配置文件没有使用‘=’作为分隔符,如果需要分隔符,我使用了’\'作为分隔符

;static content-type profile
;format:
;.extname1\content-type1
;.extname2\content-type2
;please set .html after others

.txt\text/plain
.css\text/css
.js\text/javascript
.svg\image/svg+xml
.jpg\image/jpeg
.jpeg\image/jpeg
.png\image/png
.ico\image/x-icon
.gif\image/gif

;modify _HTML_ to increase html type

.html\text/html
.htm\text/html

把静态网页扩展名放在最后是为了简单的实现隐藏网页的扩展名。读取的结果的是char **类型。
比如:SETTINGS[S_SCO]是const char *类型的,读取之后SETTINGS[S_SCO]应该是char **类型的

数据结构

struct sbuf
{
	short flag;
	char *buf;
	size_t alen; //alloc_len
	size_t nwrite; 
	size_t nread;
};

sbuf是之后的函数使用的buffer,nwrite是已经写入的字节数,alen是buf的大小

#define ARGS_DATA_LEN	40

struct args
{
	short type;
	short keepconn; //是否保持连接
	struct bufferevent *bev; //与客户端连接的bufferevent
	struct timeval *rtime; //客户端超时
	struct timeval *ftime; //fcgi_server超时
	struct sockaddr *addr; //客户端地址
	char data[ARGS_DATA_LEN]; //请求数据
};

struct bev_rargs
{
	short type;
	short keepconn;
	struct bufferevent *bev;
	struct timeval *rtime;
	struct timeval *ftime;
	struct sockaddr *addr;
	struct sbuf *header; //http头不包括附加数据
	struct sbuf *content; //附加数据
	struct sbuf *tbuf; //临时buf
};

struct bev_pargs
{
	short type;
	short keepconn;
	struct bufferevent *bev;
	struct timeval *rtime;
	struct timeval *ftime;
	struct sockaddr *addr;
	void **package;  //处理后的请求
};

struct bev_wargs
{
	short type;
	short keepconn;
	struct bufferevent *bev;
	struct timeval *rtime;
	struct timeval *ftime;
	struct sockaddr *addr;
	int flag; //区分文件和数据
	struct sbuf *header; //应答头
	struct sbuf *content; //记录 文件大小与写出大小 或者 数据大小与写出大小 
};

struct fbev_wargs
{
	short type;
	short keepconn;
	struct bufferevent *bev;
	struct timeval *rtime;
	struct timeval *ftime;
	struct sockaddr *addr;
	int sno; //序列号
	struct bufferevent *fbev; //与fcgi_server通信的bufferevent
	struct sbuf *buf; //需要写出的buf
};

struct rsize
{
	size_t csize;  //contentLength
	size_t psize; //paddingLength
	size_t n; //读取到的字节数
};

struct fbev_rargs
{
	short type;
	short keepconn;
	struct bufferevent *bev;
	struct timeval *rtime;
	struct timeval *ftime;
	struct sockaddr *addr;
	int sno;
	struct bufferevent *fbev;
	struct sbuf *buf; //从fcgi_server接收到的数据
	struct rsize *rs; //记录fcgi数据包大小和读取的字节数
};

#define ARGS			0
#define BEV_RARGS		1
#define BEV_PARGS		2
#define BEV_WARGS		4
#define FBEV_RARGS		8
#define FBEV_WARGS		16

libevent的回调函数有一个void *参数可以为自己使用。所以这里用了一个通用结构来记录一个请求的状态和信息
服务器在执行一个请求有几种状态:
最开始是读取请求(bev_rargs),当读取完之后需要处理请求数据(bev_pargs),如果出错,会把一个错误页面返回给客户端(bev_warg)。没有出错,如果请求的文件是一个静态文件,则会把文件用mmap的方式返回给客户端(bev_wargs).请求的文件是一个动态文件则需要把请求信息发送给fcgi_server (fbev_wargs), 发送完成后从fcgi_server读取内容(fbev_rargs).读取完成后处理下内容,返回给客户端(bev_warg).
大概是这样子的:

Created with Raphaël 2.2.0 ARGS BEV_RARGS BEV_PARGS 出错? BEV_WARGS free_args 请求动态文件? FBEV_WARGS FBEV_RARGS yes no yes no

有一个缺陷 bev_rargs到bev_pargs之间的过程没有放入事件循环,bev_pargs到 bev_wargs或fbev_wargs 之间的过程也没有放入事件循环

请求处理

关于char *strtok_r(char *str, const char delim, char **savptr), 如果返回值不是NULL则savptr指向为str中delim的第一字节。

当请求的数据读取结束之后会进行数据包的处理,数据包一般是"\r\n"分割行的。用strtok_r分割出第一行,并把其中的空格替换为0,就能分离出METHOD URI VERSION。*savptr+2指向选项的第一字节。选项与附加数据以"\r\n\r\n"分割。所以strtok_r(选项,"\r\n\r\n",&sav)的返回值指向附加数据的第一字节。(bev_readcb回调函数会分割http头和附加数据)
下面是package的定义:

#define PACKAGE_SIZE	12 
#define METH		0 //请求方法
#define URI		1 //请求uri
#define VER		2 //客户端http版本
#define FP		3 //请求的文件路径
#define RADR		4 //客户端ip
#define RPOT		5 //客户端端口
#define BDIR		6 //basedir
//
#define FN		7 //请求文件名
#define CARG		8 //查询参数
//
#define CONT		9 //content,也就是附加数据
#define SETS		10 //选项
#define FCON		11 //请求文件的contenttype

最后请求会处理为package。
对于uri中请求的文件的识别:
首先包头uri的格式可以是http://xx.xxx.xx/xxx/xxxx/或/xxx/xxxx/,所以需要判断。分离出/xxx/xxxxx/。
检查 SETTINGS[S_BDI]中最后一位非0字节是不是’/’。如果是,就替换为0。检查 分离出的 中最后一位非0字节是不是’/’。如果是,就替换为0。连接两个字符串.
例:SETTINGS[S_BDI] = “/var/www/html“
连接之后path = ”/var/www/html/xxx/xxxx”
使用stat检查文件是否存在。
若存在,通过st_mode判断文件类型。

目录:在path后面加上"/index",把S_SCO和S_CCO中 网页 的扩展附名依次加到path末尾,检查是否存在。若存在记录contentype在package[FCON]中。不存在则返回错误404.
文件: 取文件扩展名,如果没有则使用默认contenttype。把扩展名依次与SETTINGS[S_SCO]中的 非网页扩展名 进行对比。
若成功,记录contenttype在package[FCON]中。失败则使用默认contenttype。

若不存在,把SETTINGS[S_SCO]和SETTINGS[S_CCO]中 网页 的扩展名加到path末尾,重新测试。如果都失败了就返回错误404。

回调函数

读取请求的回调: bev_readcb
响应请求的回调: bev_writecb
bev事件回调: bev_eventcb
写fcgi_server回调: fbev_writecb
读fcgi_server回调: fbev_readcb
fbev事件回调: fbev_eventcb
evconnlistener的回调: conncb,errcb
conncb创建一个bufferevent和bev_rargs,并把bufferevent放入事件循环。errcb打印accept错误直接退出。
在 bev或fbev的 事件 回调 中,只要发生的事件不是BEV_EVENT_CONNECTED,直接切断连接,并做好清理。也可以为BEV_EVENT_TIMEOUT返回一个错误码。
不管是读或者写,一般不会在一次事件循环中完成。sbuf->nread,sbuf->nwrite用来记录就很方便。

bev_readcb简化流程:
把bufferevent中缓存的数据全部写入bev_rargs->tbuf
检查head->flag
{
false: 用strtok_r(tbuf->buf, “\r\n\r\n”, &sav)
[
返回NULL,检查tbuf->nwrite
( >=MAX_HEADER返回错误400.
< MIN_HEADER(这是自己设置的,是一个远小于MAX_HEADER的值。用来识别非标准的包头格式 如使用“\r\n"结尾又没有 content的包)head的长度为tbuf->nwrite - 2
其他情况直接return。
)
返回ret,则ret - tbuf->buf为head的长度
写入head,head->flag置1.在head中就能确定keepconn和content-length了。
]
true: 在head中得到conten-length.
}
根据content-length与content->flag得到content。
如果head->flag与content->flag都为1则进行下一步,否则返回。

Created with Raphaël 2.2.0 tbuf strtok_r 写入head 得到content head->flag&&content->flag process return tbuf->nwrite >= MAX_HEADER error 400 tbuf->nwrite < MIN_HEADER yes no yes no yes no yes no

bev_writecb简易流程:
如果head->flag和content->flag都为0.根据bev_wargs->keepconn处理连接。
如果head->flag是置1的: 写出到bev,head一次写完,flag置0,return
如果conten->flag是置1的:
如果bev_wargs->flag是小于0的,写content->buf到bev,每次MAX_PACKAGE字节。如果content全部写出,content->flag置0。return。
如果bev_wargs->flag是大或等于0的,mmap bev_wargs->flag 起始content->nread 每次MAX_PACKAGE,如果全部写出,content->flag置0。return。

fbev_writecb简易流程:
如果fbev_wargs->buf为空,把fbev_wargs转变为fbev_rargs,注意fbev的读写开关和超时。
如果fbev_wargs->buf不为空, 写出到fbev 每次MAX_PACKAGE(0x2000: PHP-FPM默认的数据分包大小为0x2000,包括8字节的包头,8184字节数据)。

fbev_readcb的流程不好描述,下面是源码:

void
fbev_readcb(struct bufferevent *bev, void *ctx)
{
#ifdef _DEBUG
	log_msg(0, "fbev_readcb calling. bev:%p, args:%p", bev, ctx);
#endif
	struct fbev_rargs *p = ctx;
	size_t len = bufferevent_get_length(bev, input); //相当于evbuffer_get_length(bufferevent_get_input(bev))
	int sno, i, n;
	struct sbuf *cbuf, *buf;
	struct rsize *rs;
	FCGI_Header *head;
	assert(p->type == FBEV_RARGS);
	if(!p->buf)
		p->buf = sbuf_create();
	if(!p->rs)
		p->rs = rsize_create();
	rs = p->rs;
	buf = p->buf;
	cbuf = sbuf_create();
	if(bufferevent_to_sbuf(bev, cbuf, len) < 0)
		log_exit(1, "bufferevent_read error");
	while(!is_empty(cbuf))
	{
		if(rs->n >= rs->csize)
		{
			if(rs->n < rs->csize + rs->psize)
				adjust_read(cbuf,
					rs->csize + rs->psize - rs->n); //cbuf->nread += (rs->csize+rs->psize-rs->n)
			head = sbuf_read(cbuf, FCGI_HEADER_LEN);
			sno = (int)(head->requestIdB1 << 8) +
				head->requestIdB0;
			if(sno != p->sno)
				log_exit(0,"fbev_readcb error: sno mismatch");
			if(head->type == FCGI_END_REQUEST)
				goto _quit;
			if(head->type != FCGI_STDOUT &&
				head->type != FCGI_STDERR)
				goto _error;
			rs->csize = (int)(head->contentLengthB1 << 8) +
				head->contentLengthB0;
			rs->psize = (int)head->paddingLength;
			rs->n = 0;
		}
		if(buf_size(cbuf) >= rs->csize + rs->psize - rs->n) //buf_size: buf->nwrite-buf->nread
		{
			i = rs->csize - rs->n;
			n = rs->csize + rs->psize - rs->n;
		}
		else if(buf_size(cbuf) >= rs->csize - rs->n &&
			buf_size(cbuf) < rs->csize + rs->psize -
				rs->n)
		{
			i = rs->csize - rs->n;
			n = buf_size(cbuf);
		}
		else
		{
			i = buf_size(cbuf);
			n = i;
		}
		sbuf_add_to(buf, cbuf, i);
		sbuf_lseek(cbuf, n - i, SBUF_NREAD); //相当于adjust_read
		rs->n += n;
	}
	sbuf_free(cbuf);
	return;
_quit:
	sbuf_free(cbuf);
	set_flag(buf, 1); //set_flag: buf->flag = 1
	parse_fcgi_buf(p); //进一步处理,会改变p->type
#ifdef _DEBUG
	log_msg(0, "fbev_readcb:		successed\tfbev:%p", bev);
#endif
	free_fbev(bev);
	args_start(p); //根据p->type调整读写开关和超时
	return;
_error:
	sbuf_free(cbuf);
#ifdef _DEBUG
	log_msg(0, "fbev_readcb:		faild\tfbev:%p", bev);
#endif
	free_fbev(bev);
	senderr(p, E_500);
	return;
}

FAST-CGI协议

FAST-CGI协议参考文章中用详细的描述。
序列号sno是用rand()随机生成的。包的最大长度为MAX_PACKAGE(0x2000),用了一个结构来辅助处理包头

struct header_helper
{
	size_t header; //当前包头在buf中的位置
	struct sbuf *buf; //存放数据包的buf
	int sno; //序列号
	size_t psize; 当前数据包的长度,包括包头
	size_t maxsize; //每个数据包的最大长度
};

在PHP-FPM中,FCGI_PARAM中字符串可以包含末尾的0,如果包含了0,计算长度时要增加1.

一个简单的web服务器_第1张图片

你可能感兴趣的:(一个简单的web服务器)