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
Tinyhttpd 代码学习的更多相关文章
- u-boot代码学习内容
前言 u-boot代码庞大,不可能全部细读,只能有选择的读部分代码.在读代码之前,根据韦东山教材,关于代码学习内容和深度做以下预先划定. 一.Makefile.mkconfig.config.mk等 ...
- Objective-C代码学习大纲(3)
Objective-C代码学习大纲(3) 2011-05-11 14:06 佚名 otierney 字号:T | T 本文为台湾出版的<Objective-C学习大纲>的翻译文档,系统介绍 ...
- ORB-SLAM2 论文&代码学习 ——Tracking 线程
本文要点: ORB-SLAM2 Tracking 线程 论文内容介绍 ORB-SLAM2 Tracking 线程 代码结构介绍 写在前面 上一篇文章中我们已经对 ORB-SLAM2 系统有了一个概览性 ...
- ORB-SLAM2 论文&代码学习 —— 单目初始化
转载请注明出处,谢谢 原创作者:Mingrui 原创链接:https://www.cnblogs.com/MingruiYu/p/12358458.html 本文要点: ORB-SLAM2 单目初始化 ...
- ORB-SLAM2 论文&代码学习 —— LocalMapping 线程
转载请注明出处,谢谢 原创作者:Mingrui 原创链接:https://www.cnblogs.com/MingruiYu/p/12360913.html 本文要点: ORB-SLAM2 Local ...
- Learning Memory-guided Normality代码学习笔记
Learning Memory-guided Normality代码学习笔记 记忆模块核心 Memory部分的核心在于以下定义Memory类的部分. class Memory(nn.Module): ...
- 3.1.5 LTP(Linux Test Project)学习(五)-LTP代码学习
3.1.5 LTP(Linux Test Project)学习(五)-LTP代码学习 Hello小崔 华为技术有限公司 Linux内核开发 2 人赞同了该文章 LTP代码学习方法主要介绍两个步骤, ...
- Apollo代码学习(七)—MPC与LQR比较
前言 Apollo中用到了PID.MPC和LQR三种控制器,其中,MPC和LQR控制器在状态方程的形式.状态变量的形式.目标函数的形式等有诸多相似之处,因此结合自己目前了解到的信息,将两者进行一定的比 ...
- 开源代码学习之Tinyhttpd
想开始陆续研究一些感兴趣的开源代码于是先挑一个代码量短的来过渡一下,写这篇博客的目的是记录下自己学习的过程.Tinyhttpd算是一个微型的web服务器,浏览器与Web服务器之间的通信采用的是Http ...
随机推荐
- R读取excel文件乱码 read.xlsx() 解决方法
1. 参考[R语言]R读取含中文excel文件,read.xlsx乱码问题 该文章总结得很好,可以直接跳到最后看博主的总结. 2. 如果依旧是乱码那么用read.xlsx2()去读取excel文件, ...
- 【Lab】Python改bat文件
[Lab]Python改bat文件 给出一个特定的树形结构,每一层的数字依次递增后,按照从上到下,同时从左到右这样的顺序生成.这么说还是不太明白,比如下面这个简单的树形结构. 按照顺序应该写成这样[3 ...
- HDU 6033 Add More Zero (数学)
Description There is a youngster known for amateur propositions concerning several mathematical hard ...
- 木棍分割[HAOI2008]
题目描述 有n根木棍, 第i根木棍的长度为Li,n根木棍依次连结了一起, 总共有n-1个连接处. 现在允许你最多砍断m个连接处, 砍完后n根木棍被分成了很多段,要求满足总长度最大的一段长度最小, 并且 ...
- (4)UIView和父子控件
IButton控件中除了有自身的属性之外还有继承的view的属性 内存地址一样,是同一个view来的,也就是最外层的view.
- testbench中$display查看例化model里面信号方法以及$realtime用法
前言 此为测试语法,不可综合: 流程: 1.在tb中可以这么写,检测clk_t_en的高电平,输出仿真时间位置,想查看的cnt_t是底层模块中的.这么会使得时间延迟一个周期: always @(pos ...
- AWK求和、平均值、最值
--AWK求和.平均值.最值------------------------2014/02/14 打包当前目录下的所有文件 ls | awk '{ print "tar zcvf &quo ...
- selenium--关键字驱动
package com.dn.twohomework;import java.util.ArrayList;import java.util.Set;import java.util.List;// ...
- python——模块和包 需要注意的地方
一 模块 1.import import module: 将执行文件(module)的目录路径插入到sys.path的第一个位置 执行时: 1.创建新的名称空间 2.执行被调用的模块 第二次调用,不会 ...
- Qt图片按原比例缩放
1.选择图片 QString strFilePath = QFileDialog::getOpenFileName(this, tr("Select file"), QStanda ...