tinyhttpd 阅读与分析
Contents
tinyhttpd 是一个简易的 http 服务器,支持CGI。代码量少,非常容易阅读,十分适合网络编程初学者学习的项目。 麻雀虽小,五脏俱全。在tinyhttpd中可以学到 linux 上进程的创建,管道的使用。linux 下 socket 编程基本方法和http 协议的最基本结构。
1. 主要函数和作用
|
|
源码阅读顺序: main -> startup -> accept_request -> execute_cgi
2. 程序运行流程
2.1 安装运行tinyhttpd
这个项目不能直接在Linux上编译运行,它本来是在solaris上实现的,需要作如下修改:
- 33行改为
void *accept_request(void *);
所以下面的实现也要修改下:
|
|
- 438行和483行的变量类型改为socklen_t
- 497行改为
if (pthread_create(&newthread , NULL, accept_request, (void*)&client_sock) != 0)
- Makefile中编译的一行改为gcc -W -Wall -o httpd httpd.c -lpthread
2.2 程序流程
程序运行流程如下图所示:
服务器启动,在指定端口或随机选取端口绑定 httpd 服务。
收到一个 HTTP 请求时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数。
取出 HTTP 请求中的 method (GET 或 POST) 和 url,。对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数。
格式化 url 到 path 数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下。当 url 以 / 结尾,或 url 是个目录,则默认在 path 中加上 index.html,表示访问主页。
如果文件路径合法,对于无参数的 GET 请求,直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,跳到(10)。其他情况(带参数 GET,POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本。
读取整个 HTTP 请求并丢弃,如果是 POST 则找出 Content-Length. 把 HTTP 200 状态码写到套接字。
建立两个管道,cgi_input 和 cgi_output, 并 fork 一个进程。
在子进程中,把 STDOUT 重定向到 cgi_outputt 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,设置 request_method 的环境变量,GET 的话设置 query_string 的环境变量,POST 的话设置 content_length 的环境变量,这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 程序。
在父进程中,关闭 cgi_input 的读取端 和 cgi_output 的写入端,如果 POST 的话,把 POST 数据写入 cgi_input,已被重定向到 STDIN,读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。
关闭与浏览器的连接,完成了一次 HTTP 请求与回应,因为 HTTP 是无连接的。
2.3 函数分析
main
- 程序入口,通过 startup 函数来绑定和监听端口,accept 一个客户端链接后创建一个线程调用 accept_request 函数来处理用户发来的 HTTP 请求报文。
startup
- 建立socket绑定端口(bind)并且开始监听(listen),等待客户端的握手信息
accept_request
- 通过 get_line 按行处理 HTTP 请求。
|
|
execute_cgi
- fork 一个子进程执行可执行文件,然后通过管道将结果返回父进程,进而返回客户端。
- 如果是 get 方法,就读取并丢弃整个 http 首部。如果是 post 方法,还会从中 content_length 长度。
- 建立两个管道, cgi_input 和 cgi_output ,并 fork 一个进程(必须 fork 子进程,pipe 管道才有意义)。建立父子进程间的通信机制。
- 在子进程中,对其进程下的管道进行重定向,并设置对应的环境变量(method、 query_string 、 content_length ),这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 脚本,可以看出 cgi 脚本的执行在子进程中进行,然后结果通过管道以及重定向返回给父进程。
- 父进程中,关闭管道一端,如果是 POST 方式,则把 POST 数据写入 cgi_intput ,已被重定向到 STDIN,读取 cgi_output 。 管道输出到客户端(浏览器输出),具体流程图参见上面的管道最终状态图。接着关闭所有管道,等待子进程结束。
- 关闭连接,完成一次 HTTP 请求与回应。
- fork 一个子进程执行可执行文件,然后通过管道将结果返回父进程,进而返回客户端。
|
|
3. 待改进点
只用了 502 行代码就实现了一个 http 服务器,代码还是十分精炼的,逻辑非常清晰,具有单独的错误处理模块。使用线程模型实现并发服务器,这些优点还是非常值得肯定的。
但是下面几点也是十分有必要进行改进的
- 没有更改当前工作目录,如果不是在当前目录启动,则服务器将找不到对应的资源目录,当然,代码中使用相对路径是相同弊病
- 没有将运行、通信情况记入 log,这样对于以往的服务器工作状况、通信状况,我们将无从得知
- http 请求的处理非常低效
- 没有对拒绝服务攻击做出处理,即便不是拒绝服务攻击,仅仅是连接数目过多都会使服务器线程开辟过多而占用过多资源,这一点如果换成 IO 复用模型可以得到一定的改善
- 没有对信号进行处理,甚至没有对 EINTR 错误的处理
Author JackT
LastMod 2019-05-31