• 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)

    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

  • 相关阅读:
    WAF绕过方法
    ”非常危险“的Linux命令
    CSRF--花式绕过Referer技巧
    安卓手机的后门控制工具SPADE
    基于RedHat发行的Apache Tomcat本地提权漏洞
    SQL注入--宽字节注入
    MySQL提权
    CF1067D. Computer Game(斜率优化+倍增+矩阵乘法)
    CF1063F. String Journey(后缀数组+线段树)
    BZOJ4732. [清华集训2016]数据交互(树链剖分+线段树+multiset)
  • 原文地址:https://www.cnblogs.com/pluviophile/p/7440529.html
Copyright © 2020-2023  润新知