Tinyhttpd 代码学习
前阵子,参加了实习生面试,被面试官各种虐,问我说有没有读过一些开源的代码。对于只会用框架的我来说真的是硬伤啊,在知乎大神的推荐下在EZLippi-浮生志
找了一些源代码来阅读,于是从小型入手,找了Tinyhttpd来读一读。
什么是Tinyhttpd
tinyhttpd
是一个超级轻量级的Http Server,是C语言写的,简单的实现了GET和POST方法,虽然有点简陋连注释加起来只有502行,但是却是了解Http Server如何运作的一个很好的例子。源代码是在 Solaris机器上编译通过的,在Linux上有一些不一样,有可能会导致编译错误。感谢EZ大大 在Github上维护了一份Linux的版本。
原始代码地址 http://tinyhttpd.sourceforge.net
EZ大大维护的代码 https://github.com/EZLippi/Tinyhttpd
工作流程
tinyhttpd的源代码只有502行,并不复杂。花一天就能读懂源代码,仔细思考可以学习一些网络编程和系统调用的知识. 下面说一说tinyhttpd的流程和关键的函数。
工作流程(参考于EZ大大的README)
- 服务器启动,main函数调用startup函数绑定服务端口(指定端口/随机端口)
- main函数进入无限循环,并且由于recv调用而被阻塞,等待HTTP请求。收到请求时,将会派生一个线程运行accept_request函数,然后循环到recv调用,main函数线程继续被阻塞
- 在accept_request函数中,通过定制的
get_line
方法,取出HTTP方法和URL,对于 GET 方法,如果有携带参数,则query_string
指针指向 url 中?
后面的 GET 参数。 - tinyhttpd的服务器文件是放置在以工作目录为相对路径的htdocs文件夹先,对于取出的url,先格式化到path字符数组中,如果是以
/
结尾的,或者url是目录的情况下,那么默认地在url后加上index.html
表示访问主页。 - 如果文件路径合法(也就是文件存在),对于无参数的 GET 请求,读取整个HTTP请求并丢弃,然后直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,然后关闭连接。其他情况(带参数 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
,cgi_input
已被重定向到子进程的STDIN,读取cgi_output
的管道输出,然后把cgi_output
的输入写入到套接字中。接着关闭所有管道,等待子进程结束。
代码笔记
代码中有一些技巧和系统调用,由于知识面不广,感觉很新鲜。另外由于这份代码是根据Solaris版本代码修改的,有一些妥协和考量,都在这里记录下来。
main
函数
定义了几个常用变量 port
默认为4000, 调用startup
函数,进行httpd服务的初始化,并返回创建完成的server_socket
。接着利用一个循环等待接收客户端的连接,如果获取到客户端的套接字,将创建一个线程accept_request
并把客户端套接字传递给这个线程。在创建线程这里就出现了第一个关键的不同。
Solaris版本的pthread_create
是按值传递,而Linux版本则是传递void*
指针。
EZ大大的版本中这样写
// main()
int client_sock;
pthread_create(&newthread , NULL, (void *)accept_request, (void *)&client_sock);
// accept_request(void *arg)
int client = *(int*)arg;
这样会出现,线程竞争而导致创建的线程中的client还没获取到时就被另外的线程篡改了。在Issue#5有这方面的讨论,一种解决办法就是加锁,另一种是动态分配内存,在子线程中释放内存解决办法。
而让我觉得很巧妙的办法是huntinux的解法,利用了函数参数值传递,不用加锁而解决了竞争问题。
// main()
int client_sock;
pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock);
// accept_request(void *arg)
int client = (intptr_t)arg;
这样很巧妙地解决了问题,但是在没有注释的情况下,我觉得有一点费解。但我个人还是比较赞成使用动态分配内存,在子线程中释放内存的做法。
startup
函数
httpd = socket(PF_INET, SOCK_STREAM, 0)
这里的PF_INET
中PF是Protocol Family和AF_INET中AFAddress Family是一样的PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。创建一个套接字
在《Unix网络变成:卷一》中有提到PF_前缀
和AF_前缀
:
历史上曾有这样的想法:单个协议族(PF)可以支持多个地址族(AF),
PF_值
用来创建套接字,而AF_值
用于套接字地址结构。但实际上多个地址族的协议族从来就未实现过,而且<sys/socket.h>
中为一给定的协议定义的PF_值
总是于此协议的AF_值
相等。尽管这种相等关系不一定永远成立,但若有人试图给已有的协议改变这种约定,则许多现存代码都将崩溃。所以通常来说会在sockaddr_in
结构体中看到AF_INET
、在socket()
调用中看到PF_INET
. 但是从实践方面的角度来说,可以在任何地方使用AF_INET
-
setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))
这段代码的目的是设置httpd的状态。其中SO_REUSEADDR
,允许在bind()
过程中本地地址可重复使用。具体的应用在于如果server端因为不可控原因而崩溃,由于TCP的本身体质,需要经历2MSL(《TCP/IP详解卷一》)的等待时间,来防止新socket()
重用了这个端口造成误解释。如果不进行设置的话,需要等待一段比较长的时间才可以重新使用这个端口,不然会显示端口已占用。这里进行这番设置用意也在这里(有待验证)。 这里的on为1。就是表明SO_REUSEADDR
使能。(由于Solaris版本中没有指定端口,所以不用考虑到2MSL问题,而Linux版本中存在指定端口,如果在程序崩溃后仍要使用这个端口,就需要加上这段代码) -
如果传递进startup()的port为0的话。在Unix网络编程中,如果
sockaddr_in
的sin_port
为0 的话表示系统分配端口,也就是系统随机分配端口。
execute_cgi
函数
execute_cgi
函数中通过fork()
系统调用创建了一个子进程通过execl
调用,来执行cgi脚本,由于这是多线程环境,那么创建多进程就有一些乱七八糟的东西要考虑。多线程环境创建多进程注意的事项之前有了解过,在这里由于是通过execl
调用cgi脚本,并且没有使用任何锁,所以并没有太多要考虑的东西,这里先开个坑,后面再补上多线程环境fork()
execute_cgi
函数中在重定向子进程的输入输出流用到的dup2()
,调用值得看一看。我翻看了Linux编程手册,看了dup2()
和它的孪生兄弟dup()
。
dup(int oldfd)
该系统调用创建了描述符的oldfd
的一个副本,返回的是系统可使用最小的文件描述符。返回的描述符newfd
其实是oldfd
的“引用”(C中并没有引用),对其中任何一个描述符操作都会影响到另外的描述符,例如任何一个描述符上调用lseek()
都会影响到另外一个描述符偏移量。dup2(int oldfd, int newfd)
这个系统调用的工作原理和dup
很类似,但是它返回的不是系统最小可用文件描述符,而是newfd
,如果newfd
没有关闭,那么系统会先将newfd
关闭,再关联到oldfd
// execute_cgi
dup2(cgi_output[1], STDOUT);
dup2(cgi_input[0], STDIN);
那么子进程——cgi进程——的标准输入输出就被重定向到了管道中了。在cgi脚本中就只需要从标准输入流读入从标准输出流输出就好了。en,挺好的做法。
那么tinyhttpd已经阅读完了,阅读他人的代码很有收获的,怪不得面试官会问没有没看过开源代码呢。
我的注释版本https://github.com/Lisupy/tinyhttpd_mirror