Tinyhttpd学习

学习一个简易的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和端口号进行测试,结果为:

Tinyhttpd学习_第1张图片

点提交

  Tinyhttpd学习_第2张图片

【TIPS】关闭该程序的时候可用ctrl+c,不能用ctrl+z,会在bind时报已在使用的错误,还得kill程序解决此问题。

经典图摘自:Tinyhttpd精读解析

Tinyhttpd学习_第3张图片

具体的代码也有很多人写过了,这里总结一下自己学到的东西。

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的写入端。

如果把管道和进程之间的连接关系连起来看可画图:

Tinyhttpd学习_第4张图片

然后子进程调用excel函数(putenv函数可用于将信息放入CGI环境变量来进行交互)执行CGI脚本,父进程则读取POST内容,并将数据发送给CGI脚本(往cgi_input[1]里写),再从 cgi_output[0] 中读取内容返回给浏览器。这里可以理解为子进程用于处理CGI文件,父进程负责socket的读写操作。

父进程调用waitpid等待子进程结束即可。

整体的流程图参考:tinyhttpd 剖析

Tinyhttpd学习_第5张图片

 

你可能感兴趣的:(拓展)