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

    接触ASP.NET MVC的时间不长,这段时间做东西的时候要用到分页,但是普通的分页用户体验不是很好,所以想实现无刷新的分页. 在网上找了好多例子,但是感觉都封装的不好,不小心发现了Webdiyer. ...

  2. Hello BlogsPark

    2017年8月4日, 今天是使用博客园的第一天, 签个到. NSLog(@"Hello BlogsPark");

  3. 用分治法解决最近点对问题:python实现

    最近点对问题:给定平面上n个点,找其中的一对点,使得在n个点的所有点对中,该点对的距离最小.需要说明的是理论上最近点对并不止一对,但是无论是寻找全部还是仅寻找其中之一,其原理没有区别,仅需略作改造即可 ...

  4. python 初学习 模拟用户登录

    #!/usr/bin/env python#coding:utf-8''' 2017年8月19日 模拟用户登录,userfile 文件保存字典 用户名,和密码 sorryname 文件保存字典 登录过 ...

  5. 【Java学习笔记之二十一】抽象类在Java继承中的用法小结

    一.抽象类的基本概念 普通类是一个完善的功能类,可以直接产生实例化对象,并且在普通类中可以包含有构造方法.普通方法.static方法.常量和变量等内容.而抽象类是指在普通类的结构里面增加抽象方法的组成 ...

  6. 使用JavaScript实现简单的双色球

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. asp.net core MVC 过滤器之ActionFilter过滤器(二)

    本系类将会讲解asp.net core MVC中的内置全局过滤器的使用,将分为以下章节 asp.net core MVC 过滤器之ExceptionFilter过滤器(一) asp.net core ...

  8. visual studio code 调试nodejs 配置简单HTTP服务器

    介绍 Visual Studio Code是一个轻量级的Web集成开发环境on Linux,Mac and Windows,特别是作为前端人员来了, 多了一个可供选择的生产力工具IDE,调试js代码简 ...

  9. iptables规则的删除-怎么删除一条已有的iptables规则

    语法是: iptables -D chain rulenum [options]     其中: chain 是链的意思,就是INPUT FORWARD 之类的定语     rulenum 是该条规则 ...

  10. C#学习——简介(第一天)

    一.控制台输出 在控制台输出: console.writeline(); console.readkey(); 加上后面一句是为了保证弹窗不会一闪而过. 二.注释 1.单行注释: //int a=90 ...