前阵子,参加了实习生面试,被面试官各种虐,问我说有没有读过一些开源的代码。对于只会用框架的我来说真的是硬伤啊,在知乎大神的推荐下在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)

  1. 服务器启动,main函数调用startup函数绑定服务端口(指定端口/随机端口)
  2. main函数进入无限循环,并且由于recv调用而被阻塞,等待HTTP请求。收到请求时,将会派生一个线程运行accept_request函数,然后循环到recv调用,main函数线程继续被阻塞
  3. 在accept_request函数中,通过定制的get_line方法,取出HTTP方法和URL,对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中后面的 GET 参数。
  4. tinyhttpd的服务器文件是放置在以工作目录为相对路径的htdocs文件夹先,对于取出的url,先格式化到path字符数组中,如果是以/结尾的,或者url是目录的情况下,那么默认地在url后加上index.html表示访问主页。
  5. 如果文件路径合法(也就是文件存在),对于无参数的 GET 请求,读取整个HTTP请求并丢弃,然后直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,然后关闭连接。其他情况(带参数 GET,POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本。
  6. 读取整个 HTTP 请求并丢弃,如果是 POST 则找出 Content-Length. 把 HTTP 200 状态码写到套接字。
  7. 建立两个管道,cgi_inputcgi_output, 并 fork 一个进程。
  8. 在子进程中,把 STDOUT 重定向到 cgi_outputt 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,设置 request_method 的环境变量,GET 的话设置 query_string 的环境变量,POST 的话设置 content_length 的环境变量,这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 程序。
  9. 在父进程中,关闭 cgi_input 的读取端cgi_output 的写入端,如果 POST 的话,把 POST 数据写入 cgi_inputcgi_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 FamilyAF_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_insin_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 代码学习的更多相关文章

  1. u-boot代码学习内容

    前言  u-boot代码庞大,不可能全部细读,只能有选择的读部分代码.在读代码之前,根据韦东山教材,关于代码学习内容和深度做以下预先划定. 一.Makefile.mkconfig.config.mk等 ...

  2. Objective-C代码学习大纲(3)

    Objective-C代码学习大纲(3) 2011-05-11 14:06 佚名 otierney 字号:T | T 本文为台湾出版的<Objective-C学习大纲>的翻译文档,系统介绍 ...

  3. ORB-SLAM2 论文&代码学习 ——Tracking 线程

    本文要点: ORB-SLAM2 Tracking 线程 论文内容介绍 ORB-SLAM2 Tracking 线程 代码结构介绍 写在前面 上一篇文章中我们已经对 ORB-SLAM2 系统有了一个概览性 ...

  4. ORB-SLAM2 论文&代码学习 —— 单目初始化

    转载请注明出处,谢谢 原创作者:Mingrui 原创链接:https://www.cnblogs.com/MingruiYu/p/12358458.html 本文要点: ORB-SLAM2 单目初始化 ...

  5. ORB-SLAM2 论文&代码学习 —— LocalMapping 线程

    转载请注明出处,谢谢 原创作者:Mingrui 原创链接:https://www.cnblogs.com/MingruiYu/p/12360913.html 本文要点: ORB-SLAM2 Local ...

  6. Learning Memory-guided Normality代码学习笔记

    Learning Memory-guided Normality代码学习笔记 记忆模块核心 Memory部分的核心在于以下定义Memory类的部分. class Memory(nn.Module): ...

  7. 3.1.5 LTP(Linux Test Project)学习(五)-LTP代码学习

    3.1.5 LTP(Linux Test Project)学习(五)-LTP代码学习 Hello小崔 ​ 华为技术有限公司 Linux内核开发 2 人赞同了该文章 LTP代码学习方法主要介绍两个步骤, ...

  8. Apollo代码学习(七)—MPC与LQR比较

    前言 Apollo中用到了PID.MPC和LQR三种控制器,其中,MPC和LQR控制器在状态方程的形式.状态变量的形式.目标函数的形式等有诸多相似之处,因此结合自己目前了解到的信息,将两者进行一定的比 ...

  9. 开源代码学习之Tinyhttpd

    想开始陆续研究一些感兴趣的开源代码于是先挑一个代码量短的来过渡一下,写这篇博客的目的是记录下自己学习的过程.Tinyhttpd算是一个微型的web服务器,浏览器与Web服务器之间的通信采用的是Http ...

随机推荐

  1. oracle 通过同义词建立视图

    需要给予以下权限. GRANT CREATE VIEW TO tms;GRANT SELECT ANY table TO tms;GRANT SELECT ANY DICTIONARY TO tms;

  2. 【 js 基础 】【 源码学习 】backbone 源码阅读(二)

    最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(source-code-study)进行参考交流,有详细的源码注释,以及知识总结,同时 ...

  3. Git版本控制系统之基本使用

    最早是通过接触著名的开源社区Github了解到Git的,但一直没有系统学习过.这次下定决心从头到尾系统的学一学,也将学习过程记录于此,供大家批驳.本篇文章先从以下几个方面简单了解一下Git: Git的 ...

  4. 访问被拒绝。 说明: 访问服务此请求所需的资源时出错。服务器可能未配置为访问所请求的 URL。

    打开web.config 然后将 <authorization> <deny users="?" /> </authorization> 改为 ...

  5. js存款计算器原生小demo

    大家好,本人是初入前端的一枚程序猿,深知js底层开发的重要性,这也是我的软肋所在(曾经以为),渐渐的明白了一个道理,饭要一口口吃,路要一步步走,这也是我想告诉给所有刚刚进入IT行业的技术员们,沉下心, ...

  6. 白话ASP.NET MVC之三:Controller是如何解析出来的

    我们在上一篇文章中介绍Controller激活系统中所涉及到的一些类型,比如有关Controller类型的相关定义类型就包括了IController类型,IAsyncController类型,Cont ...

  7. 理清JS数组、json、js对象的区别与联系

    最近在敲代码时,遇上了一个关于JS数组的问题,由此引发了关于对象和json的联想,曾经觉得很畅顺的知识点突然模糊了.于是,为了理清这些东西,有了如下这篇文章.觉得没问题的猿们可以当复习,而那些带着疑问 ...

  8. iOS之Cocoapods安装

    网上关于cocoapods的教程很多,关于它的优点我不赘述:但是我根据多次安装的经验,把我遇到的问题写一下,希望对新手有所帮助. 1. 设置输入源(由于默认的gem资源是国外的,由于历史原因,访问比较 ...

  9. 《Java从入门到放弃》入门篇:hibernate查询——HQL

    不知不觉又到了hibernate的最后一篇了,只感觉时光飞逝~,岁月如梭~! 转眼之间,我们就···························,好吧,想装个X,结果装不下去了,还是直接开始吧· ...

  10. 【转载】由浅入深分析mybatis通过动态代理实现拦截器(插件)的原理

    转自:http://zhangbo-peipei-163-com.iteye.com/blog/2033832?utm_source=tuicool&utm_medium=referral 我 ...