Tinyhttpd 源码剖析(基于linux ubuntu)

一、前言

tinyhttpd是一个代码不到500行的超轻量型的Http Server,使用C语言开发,全部代码只有五百多行,可以通过这个这段代码理解一个Http Server的本质。

下载地址:http://sourceforge.net/projects/tinyhttpd/

按照作者在httpd.c文件中开头注释中的说法,这个是跑在Sparc Solaris 2.6上面,如果要跑在linux上面,还需要做以下修改:

1、注释掉 #include <pthread.h> 

2、注释掉main函数中定义newthread的行

3、注释掉main函数中pthread_create那两行

4、在main函数中将accept_request()取消注释

5、将Makefile文件中,编译条件 -lsocket去掉

但是实际中按照上述方法测试,还是没能成功跑起来,所以我自己稍微修改了下,最终能在linux ubuntu跑起来,浏览器输入IP和端口即可查看效果。我喜欢将所需的知识点写在同一个博客中,所以部分知识点会在行文过程中直接体现。

接下来就开启剖析之旅,有问题欢迎指正!

二、目录结构

整个httpd代码文件量很少,包括README和html .cgi文件也才10个文件。

httpd.c:这个文件里面实现了WebServer服务器,所以这个文件也是整个工程的重点

simpleclient.c:这里面实现了一个普通的tcp  client,没什么实际的功能

Makefile:可以直接make命令进行编译,当然linux下你可以直接输入gcc -o httpd httpd.c进行编译

index.html:这个是html文件,可以直接双击有浏览器看效果。当你服务器开启监听后,用浏览器输入服务器IP:端口后,首先收到的就是这个index.html页面。

color.cgi:这个是cgi程序,用Perl编写的,主要是接收服务器的输入,并产生一个输出发送给客户端。当你在测试页面输入颜色并点击submit后,浏览器会向服务器请求color.cgi文件,并将你输入的颜色作为参数传入color.cgi程序,然后产生一个输出到你的浏览器,你就可以看到颜色的效果了。

check.cgi:使用流程和color.cgi一致,具体功能没去深究,不过你可以将html文件里面的color.cgi换成check.cgi,然后查看效果


基础知识点:

CGI:通用网关接口(common gateway interface),描述了服务器和请求处理程序之间传输数据的一种标准。在这个httpd程序中,简单来,就是服务器收到浏览器请求cgi的请求,接下来服务器调用cgi程序,并将浏览器带过来的参数传入cgi程序执行。Perl是一个广泛被用来写CGI程序的语言,但实际上像Unix shellccript、Python、Rubby、PHP、TCL、C/C++、VB都可以用来编写CGI程序。


三、服务器程序httpd.c解析

这个文件中的函数不多,包含main函数总共才13个函数。

void accept_request(int);
void bad_request(int);
void cat(int, FILE *);
void cannot_execute(int);
void error_die(const char *);
void execute_cgi(int, const char *, const char *, const char *);
int get_line(int, char *, int);
void headers(int, const char *);
void not_found(int);
void serve_file(int, const char *);
int startup(u_short *);
void unimplemented(int);

一些函数我们只简单略过,后面会挑几个函数重点讲解。

1、accept_request:处理重套接字上监听到的一个http请求。里面包含对http包的解析过程,在这里可以体现一个服务器处理请求的流程

2、bad_request:返回给客户端这是个错误请求,错误码为400 BAD REQUEST

3、cat:读取服务器上的某个文件写到socket套接字

4、cannot_execute:主要处理发生在执行cgi程序时出现的错误

5、get_line:从socket读取一行,把回车换行等情况都统一为换行符结束

6、header:把HTTP响应的头部写到套接字中

7、not_found:客户端请求的资源不存在时的情况,即常见的404 NOT FOUND

8、server_file:调用cat把服务器文件返回给浏览器

9、startup:初始化httpd服务,包括建立套接字、绑定端口,进行监听

10、unimplemented:返回给浏览器表明收到的HTTP请求所用的method不被支持

实现流程:

1、服务器启动,在随机端口监听tcp客户端的连接

2、当收到一个http请求时(也就是一个tcp连接),先取出HTTP请求中的method(GET or POST)和url。如果带有参数,则query_string指针指向url中?后面的GET参数

3、格式化url到path数组,表示服务器请求的服务器文件路径,文件在htdocs目录下。当url以/结尾或者url是个目录,则默认在path中加上index.html,表示访问主页。

4、如果文件路径合法,对于无参数的GET请求,直接输出服务器文件到浏览器,即用HTTP格式写到套接字上面。其他情况(带参数GET、POST方式、url为可执行文件),则调用excute_cgi函数执行cgi脚本。

5、读取整个HTTP请求并丢弃,如果是POST则找出Content_Length。把HTTP 200状态码写到套接字中

6、建立两个管道,cig_input和cgi_output,并fork一个进行

7、在子进程中,把STDOUT 重定向到cgi_output的写入端,把STDIN重定向cgi_input的读取端,关闭cgi_input的写入端和cgi_ouptut的读取端,设置request_method的环境变量,Get的话设置query_string的环境变量,POST的话设置content_length的环境变量,这些环境变量都是为了给cgi脚本用的,接着用execl运行cgi程序

8、在父进程中,关闭cgi_input的读取端和cig_ouput的写入端,如果POST的话,把POST数据写到cgi_input,已被重定向到STDIN,读取cgi_output的管道输出到客户端,该管道是STDOUT。接着关闭所有管道,等待子进程结束。

关于管道这一步的数据传递,我研究了大半天才最终弄懂这个重定向的关系:

                                图一、管道的初始状态

匿名管道通信实际上是半双工管道,只能一方写一方读,要么父进程写子进程读,要么子进程写父进程读,如果要实现双工,那可以通过建立两个半双工管道来实现。

                         图二:管道的最终状态

图二为管道的最终状态,实际上应该说这是子进程中的管道的关系。因为调用fork后,每个管道在父子进程中都有文件描述符指向同一个管道。在父进程中,管道没有被重定向,还是还是原来的样子。从代码中可以看到,父进程关闭了cgi_ouput[1](写)和cgi_input[0(读){,也就是说对于cgi_output[2]这个管道来说,是用于子进程向父进程传递数据的,对于cgi_input[2]这个管道来说,是用于父进程向子进程传递数据的。因为子进程的STDOUT被重定向到cgi_output[1],也就是说子进程的输出会作为cgi_output[2]的输入,父进程在cgi_output[0]就可以读到子进程的数据,进而转发给浏览器。而由于cgi_input[0]被重定向到STDIN,所以父进程往cgi_input[1]写数据时,会通过管道传送到STDIN中,子进程可以读取STDIN从而获取父进程的数据。

在httpd代码中,就是父进程将‘color = yellow’这样的参数通过管道传递给子进程,子进程通过print函数的标准输出,将结果输出到父进程中。

这里再介绍cgi在这个httpd server中是怎么工作的:首先,fork出子进程后,初始化管道并通过putenv设置环境变量,这个环境变量在cgi程序里面可以通过getenv函数进行获取(这里只是举C语言的例子,Perl可能不叫getenv)。然后用execl函数并指定cgi路径来执行这个cgi函数,在cgi中通过管道获取父进程传递过来的请求参数。最终cgi向父进程输出完整的请求结果,也就是一段html代码,由父进返回给浏览器进行显示。

由于网上对于单个函数的注释解析已经有很多,且httpd代码实际上只有cgi处理的这一部分比较难懂,所以具体的我就不赘述了,可以参考:https://blog.csdn.net/jcjc918/article/details/42129311   实际上我这篇博客里面的图和部分解释都是从这篇博客摘过来的,感谢这位博主。

四、编译运行

首先不知道为什么,我的程序直接下载下来,修改了编译条件后,可以编译通过,但是服务端跑起来后,浏览器得到的空白页面。后面调试后发现,流程好像有点问题。

1、首先修改Makefile,将 -lsocket去掉。我用的ubuntu来编译的,我的Perl程序在/usr/bin/路径下,和htdocs中的两个cgi程序第一行的路径不一致,所以还要修改两个cgi文件的第一行为:

#!/usr/bin/perl -Tw

接下来直接编译运行:

make
./httpd

会出现:

可以看到服务端此时已经运行起来了,并且在58502端口进行监听。我的虚拟机IP为10.85.5.87,所以在浏览器输入10.85.5.87:58502 。但是此时浏览器页面出现一片空白。按照网上的说法,不需要动到程序的其他地方是可以直接在linux下运行的,但是我这里不能成功,如果有知道为什么的,麻烦告知,谢谢!

由于直接编译不成功,所以我就自己进行了调试。

1、首先在浏览器输入完之后,通过wireshark抓包,可以看到http的请求行信息,如下:

其实也可以通过代码打印看到请求行信息:

可以看到请求行方法为GET,url 为\ 版本为1.1 ,得到这些信息后我们继续分析,

在accept_request中,有额cgi标志位,用来判断是否需要执行cgi程序,可以看到当请求方式为GET 且不带参数的,是不需要执行cgi程序的,而是直接向浏览器返回默认页面。从请求行信息可以看出,确实GET方式不带参数的,所以应该直接返回默认的index.html页面才对,但实际上不是。分析代码主要出现在accept_request函数中的最后这段代码:

 else
 {
  if ((st.st_mode & S_IFMT) == S_IFDIR)
   strcat(path, "/index.html");
  if ((st.st_mode & S_IXUSR) ||
      (st.st_mode & S_IXGRP) ||
      (st.st_mode & S_IXOTH)    )
   cgi = 1;
  if (!cgi)
   serve_file(client, path);
  else
   execute_cgi(client, path, method, query_string);
 }

从这段代码可以看出,如果文件具有可执行权限,那么cgi被置1。大家可以通过ls -l中看出,index.html也是具有可执行权限的。这里作者的本意是否是想判断这个是否是一个可执行文件类型呢?后面我修改成如下即可在浏览器中获取正确的index.html页面:

 else
 {
  if ((st.st_mode & S_IFMT) == S_IFDIR)
   strcat(path, "/index.html");
  if ((st.st_mode & S_IXUSR) ||
      (st.st_mode & S_IXGRP) ||
      (st.st_mode & S_IXOTH))
  {
      if (!cgi)
          serve_file(client, path);
      else
          execute_cgi(client, path, method, query_string);
  }
 }

关于这个我也很疑惑,讲道理这代码不应该存在问题啊,求解释?

最终可以在浏览器实现输入yellow、red等颜色,背景自动改变。原程序设计只能体验一次服务端就会推出,想多次体验,要么重启服务,要么自己修改代码。

 

参考:

https://blog.csdn.net/jcjc918/article/details/42129311

https://blog.csdn.net/xxgxgx/article/details/47981237

https://blog.csdn.net/tyfbhlxd/article/details/71908334

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值