学习一个简易的http服务器开源代码,源码:https://github.com/EZLippi/Tinyhttpd
由于该代码不能直接在linux上运行,需要进行一些修改,若直接下载make编译,会报错:
需要将
gcc -g -W -Wall $(LIBS) -o $@ $<
改为
gcc -g -W -Wall -o $@ $< $(LIBS)
对于如下warning,可以将该行的 excel(path,NULL) 改为 excel(path,path,NULL) 来解决。
其次,如果不修改文件权限及cgi文件,输入颜色后是无法显示颜色的,具体做法为:
cd htdocs
which perl //确认是否为/usr/bin/perl
vim color.cgi
将
#!/usr/local/bin/perl -Tw
改为
#!/usr/bin/perl -Tw
再用chmod 700 color.cgi修改权限把文件改成可执行的即可
【TIPS】第一次测试是在windows和虚拟机的共享文件夹下,ll后发现chmod等命令可能不起作用改不了文件权限,奇葩。
当然也可以选择下载即可编译通过的版本:https://github.com/qiyeboy/SourceAnalysis.git,其中含linux版本。
【项目部署参考】tinyhttpd在Linux编译、HTTP服务器的本质:tinyhttpd源码分析及拓展
启动后可直接通过浏览器访问IP和端口号进行测试,结果为:
点提交
【TIPS】关闭该程序的时候可用ctrl+c,不能用ctrl+z,会在bind时报已在使用的错误,还得kill程序解决此问题。
具体的代码也有很多人写过了,这里总结一下自己学到的东西。
startup函数绑定监听套接字等操作非常常规,然后在accept接收客户端连接后通过pthread_create函数创建线程执行代码,这里进入线程入口函数accept_request,第一步就是读取http请求并解析。
代码首先获取一行HTTP报文数据,代码如下:
int get_line(int sock, char *buf, int size)
{
int i = 0; //游标
char c = '\0'; //当前读取到的字符
int n; //临时变量
while ((i < size - 1) && (c != '\n')) //没超过1024或者c不等于\n就一直读
{
n = recv(sock, &c, 1, 0);
/* DEBUG printf("%02X\n", c); */
if (n > 0)
{
if (c == '\r') //如果读到了\r就证明读到回车了
{
n = recv(sock, &c, 1, MSG_PEEK);
//先读一个字符,最后一项是0就是正常读完了清TCP缓冲区
//但是MSG_PEEK不清缓冲区,这样下一次recv的时候还是读的它
/* DEBUG printf("%02X\n", c); */
if ((n > 0) && (c == '\n')) //如果读到了\n证明该结束了
recv(sock, &c, 1, 0); //如上,这样只是为了清缓冲区
else
c = '\n'; //如果没读到其实也要换行了,所以让c等于\n
}
buf[i] = c; //每次读取完的赋值
i++; //游标+1
}
else
c = '\n'; //如果一上来什么都没读到,直接退出循环
}
buf[i] = '\0'; //字符数组最后补\0
return(i);
}
然后程序通过对字符数组的操作提取出请求方法放入method数组,若为不是GET和POST请求,返回不支持;若为POST,CGI置为1。
#define ISspace(x) isspace((int)(x))
while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
{
//提取其中的请求方式是GET还是POST
method[i] = buf[j];
i++;
j++;
}
method[i] = '\0';
//函数说明:strcasecmp()用来比较参数s1和s2字符串,比较时会自动忽略大小写的差异。
//返回值:若参数s1和s2字符串相同则返回0
//s1长度大于s2长度则返回大于0的值,s1长度若小于s2长度则返回小于0的值。
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
//tinyhttp仅仅实现了GET和POST
unimplemented(client);
return;
}
//cgi为标志位,置1说明开启cgi解析
if (strcasecmp(method, "POST") == 0)
//如果请求方法为POST,需要cgi解析
cgi = 1;
跳过空格,从BUF中并将URL存入数组,读URL的时候是读到?或者读完才停,?后面就是查询参数,此时需执行CGI解析参数,标志位置1并截取参数,最后和自带的htdocs文件夹组成查询路径。
如果路径只是一个目录 / ,默认设置为首页index.html(测试的时候权限要开启)。
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
此时利用stat函数通过文件名获取文件信息并保存在所指的stat结构体中,若页面不存在,一直读完剩余的请求头信息丢弃即可,调用not_found函数返回网页不存在的信息。
void not_found(int client)
{
char buf[1024];
sprintf(buf,"HTTP/1.0 404 NOT FOUND\r\n");
send(client,buf,strlen(buf),0);
sprintf(buf,SERVER_STRING);
send(client,buf,strlen(buf),0);
sprintf(buf,"Content-Type: text/html\r\n");
send(client,buf,strlen(buf),0);
sprintf(buf,"\r\n");
send(client,buf,strlen(buf),0);
sprintf(buf,"Not Found \r\n");
send(client,buf,strlen(buf),0);
sprintf(buf,"The server could not fulfill\r\n");
send(client,buf,strlen(buf),0);
sprintf(buf,"your request because the resource specified\r\n");
send(client,buf,strlen(buf),0);
sprintf(buf,"is unavailable or nonexistent.\r\n");
send(client,buf,strlen(buf),0);
sprintf(buf,"\r\n");
send(client,buf,strlen(buf),0);
}
若页面存在,看CGI标志位,为1进行动态解析,为0返回静态文件。
若返回静态文件,调用serve_file函数,首先丢弃HTTP请求头其他信息,然后根据filename打开文件读取内容,调用headers函数添加HTTP头并调用cat函数发送文件内容。
若为动态解析,首先需要对POST请求取出Content-Length,GET无所谓。
建立两个管道,cgi_output以及cgi_input,fork一个子进程。
int cgi_output[2];
int cgi_input[2];
//#include
//int pipe(int filedes[2]);
//返回值:成功,返回0,否则返回-1。
//参数数组包含pipe使用的两个文件的描述符。fd[0]:读管道,fd[1]:写管道。
if (pipe(cgi_output) < 0) {
cannot_execute(client);
return;
}
if (pipe(cgi_input) < 0) {
cannot_execute(client);
return;
}
if ((pid = fork()) < 0) {
cannot_execute(client);
return;
}
在子进程中,把标准输出重定向到cgi_output的写入端,把标准输入重定向到cgi_input的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,同理在父进程中要关闭 cgi_input 的读取端和 cgi_output的写入端。
如果把管道和进程之间的连接关系连起来看可画图:
然后子进程调用excel函数(putenv函数可用于将信息放入CGI环境变量来进行交互)执行CGI脚本,父进程则读取POST内容,并将数据发送给CGI脚本(往cgi_input[1]里写),再从 cgi_output[0] 中读取内容返回给浏览器。这里可以理解为子进程用于处理CGI文件,父进程负责socket的读写操作。
父进程调用waitpid等待子进程结束即可。
整体的流程图参考:tinyhttpd 剖析