这只是一个菜鸟写出来的 简单的单线程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
};
初始化过程很简单,用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).
大概是这样子的:
有一个缺陷 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则进行下一步,否则返回。
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协议参考文章中用详细的描述。
序列号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.