TinyHttpd 是 J. David Blackstone 于 1999 年写的一个 500 行左右的超轻量级 http server,用来学习服务器的工作原理十分不错。
intro
本篇 blog 为自己学习 tinyhttpd 这个服务器小项目所写的学习笔记,内容不多,但很细致。另外,本文所使用的源码来自于Tiny HTTPd’s tiny homepage,按照源码中的注释,修改掉对应的部分就可以在 Linux 上运行了。实际上,按照注释修改的版本是个无线程版的 tinyhttpd,而现在 Linux 也有了pthread.h
这个头文件,也就可以使用pthread_create
函数(这个函数可以创建线程)了,但在源码基础上,仍然需要做一些小修改才可以运行。
如果要在 Linux 上使用线程函数pthread_create
,在进行下面的改动之前,忽略掉源码本来的提示修改注释。
首先是accept_request
函数,需要修改函数的声明和定义的局部内容(具体可以参考后面贴出来的源码):
1 | - void accept_request(int); |
接着在accept_request
函数内第一行再添加int client = *(int*)pclient;
即可,后面会解释原因。
然后在main
函数中,将pthread_create
函数的第四个参数改为传入client_sock
的地址即可:
1 | - if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0) |
现在通过编译所得到的 httpd 是一个线程版本的服务器。
但无论是线程版还是无线程版,都需要修改以下内容才可以在 Linux 下运行:
首先是Makefile
,与源码中注释 5 一样,需要删除-lsocket
指令,并将-lpthread
指令放在最后面,具体如下:
1 | - gcc -W -Wall -lsocket -lpthread -o httpd httpd.c |
然后,还需要修改
index.html
的权限,去掉这个文件的可执行权限,终端下执行命令chmod 600 index.html
即可。
最后,还需要提醒的是此源码的 CGI 脚本需要用 perl 来执行,所以要先安装 perl,如何安装,此处不表,自行百度,但 perl 安装好之后又会存在一个 perl 路径的问题。
具体而言,源码中 CGI 脚本的路径是#!/usr/local/bin/perl -Tw
,但不同 Linux 发行版的软件安装路径可能不一致,比如 Ubuntu 16.04 下需要将路径改为#!/usr/bin/perl -Tw
。所以,如若可以访问index.html
页面,但无法执行 CGI 脚本,可以尝试从这方面调试。
最后的最后(再废话一下😂),还需要说明的是本文所有的代码部分,不会修改或删除 J. David Blackstone 曾经留下的注释,这部分注释也可以读读,对直接理解函数的功能有用,但是英文的,读起来可能不太方便🙂。
下面就开始逐个分析一下关键的函数。
main
先从main
函数开始,先搞清楚大致的工作流程是什么,相关的注释已经写在对应位置:
1 | int main(void) { |
以下是相关知识点简要说明:
sockaddr_in
是网络编程中的一种结构,在不同的环境中其定义也不一致。in_addr
是一个结构体,可以用来表示一个 32 位的 IPv4 地址。accept
函数是库函数,作用是通过套接字接受一个连接。
pthread_create
这里,再着重说一下pthread_create
函数,它是类 Unix 操作系统中创建线程的库函数(Linux 现在也有了)。功能是先将线程创建好,然后开始运行相关的线程函数。
前面已经提到了需要对pthread_create
函数的调用做一些修改:
1 | - if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0) |
实际上,在笔者的 Ubuntu 16.04 系统下,pthread_create
函数的第三个参数需要一个返回空指针且参数为void *
的函数指针(类型为void *(*)(void *)
),所以需要修改accept_request
函数的声明和定义;第四个参数则需要一个空指针(类型为void *
),而这第四个参数实际上就是给函数指针指向的函数(也就是accept_request
)使用的,之所以能直接传入client_sock
的地址是因为笔者使用的 GCC 编译器会做隐式转换(implicit conversion),所以直接传指针即可,其他 C/C++ 系编译器会不会转换就不知道了。
startup
startup
主要作用是用来启动服务,相关的注释已经写在对应位置:
1 | /**********************************************************************/ |
从源码中可以发现,startup
函数不仅创建好了套接字、端口,还开始进行监听了。虽然startup
这个函数使用的系统 API 非常多,但结合注释也能大概知道功能是什么了。
accept_request
这个函数是整个项目的关键函数,功能是接收客户端请求,并解析客户端发来的报文,再根据不同的方法和文件是否可执行来决定是否执行 CGI 程序。
详细的注释请看下面的修改后源码:
1 | /**********************************************************************/ |
前面提到过需要修改index.html
的文件权限,修改后这个文件的权限是-rw-------
,可以发现只保留了用户对其的读写权限,没有可执行权限。
为什么要这样改?
可以从上面代码的 77 - 81 行发现答案,如果index.html
文件可执行,那么就会被当作 CGI 脚本来执行,那这个文件就不会发送给客户端了。
而且,从源码中还可以看出服务器解析客户端请求报文时,其实就是通过解析字符串来完成的。
另外,前面还提到了需要在首行添加int client = *(int*)pclient;
,之所以要添加这一句,其实就是尽可能的避免修改更多的代码,因为源码是直接使用client
变量的。
get_line
get_line
函数的功能与其名称一致,就是读取一行字符,以 CRLF 为结尾。可能会有人疑问为什么是 CRLF 为结尾的标志,因为这是 HTTP 协议规定的。
下面是对应的注释和源码:
1 | /**********************************************************************/ |
这个函数并不复杂,本质上就可以当作一个字符串相关的处理函数来理解。
not_found/bad_request/cannot_execute/headers/unimplemented
not_found
、bad_request
、cannot_execute
、headers
和unimplemented
这五个函数都很简单,只有一个功能就是向客户端发送信息,这里不细说了,直接看源码和对应注释即可:
1 | /**********************************************************************/ |
1 | /**********************************************************************/ |
1 | /**********************************************************************/ |
1 | /**********************************************************************/ |
1 | /**********************************************************************/ |
serve_file
serve_file
函数用来向客户端发送文件,需要有客户端套接字和对应的文件路径作为参数。
1 | /**********************************************************************/ |
cat
cat
函数的功能类似 Linux 中的cat
命令,都是读取逐行读取文件,不过这里的cat
函数需要逐行读取文件并发送给客户端。
1 | /**********************************************************************/ |
execute_cgi
execute_cgi
函数也是一个关键函数,具体功能是执行 CGI 程序,具体请看下面的源码和注释:
1 | /**********************************************************************/ |
这个函数的难点在于对父子进程之间管道通信的理解,单纯读注释可能会被绕晕,这里引用别人 blog 的一张图,结合下面这张图应该会清楚一些。
summary
现在回过头来,可以发现服务器的工作流程其实很简单:
- 启动本地服务
- 接收并分析客户端请求
- 执行对应 CGI 程序
- 发送 CGI 程序执行结果给客户端
- 关闭套接字
也就是下面这张图:
实际上,如果单纯是针对 tinyhttpd 这个小项目而言,下面这张图会更细节一点:
同样的,tinyhttpd 项目所设计的函数也可以按功能分类:
最后,不得不感叹,麻雀虽小,但是依然五脏俱全~