参考文档:http://blog.csdn.net/jcjc918/article/details/42129311
0.简介:tinyhttpd 是一个不到 500 行的超轻量型 Http Server,用来学习非常不错,可以帮助我们真正理解服务器程序的本质。
看完所有源码,真的感觉有很大收获,无论是 unix 的编程,还是 GET/POST 的 Web 处理流程,都清晰了不少。废话不说,开始我们的 Server 探索之旅。
1.源码 : http://sourceforge.net/projects/tinyhttpd/
2.主要函数:
void accept_request(int);
void bad_request(int);
void cat(int, FILE *);
void cannot_execute(int);
void error_die(const char *);
void execute_cgi(int, const char , const char , const char *);
int get_line(int, char *, int);
void headers(int, const char *);
void not_found(int);
void serve_file(int, const char *);
int startup(u_short *);
void unimplemented(int);
各个函数的作用:
accept_request: 处理从套接字上监听到的一个 HTTP 请求,在这里可以很大一部分地体现服务器处理请求流程。
bad_request: 返回给客户端这是个错误请求,HTTP 状态吗 400 BAD REQUEST.
cat: 读取服务器上某个文件写到 socket 套接字。
cannot_execute: 主要处理发生在执行 cgi 程序时出现的错误。
error_die: 把错误信息写到 perror 并退出。
execute_cgi: 运行 cgi 程序的处理,也是个主要函数。
get_line: 读取套接字的一行,把回车换行等情况都统一为换行符结束。
headers: 把 HTTP 响应的头部写到套接字。
not_found: 主要处理找不到请求的文件时的情况。
sever_file: 调用 cat 把服务器文件返回给浏览器。
startup: 初始化 httpd 服务,包括建立套接字,绑定端口,进行监听等。
unimplemented: 返回给浏览器表明收到的 HTTP 请求所用的 method 不被支持。
建议源码阅读顺序: main -> startup -> accept_request -> execute_cgi, 通晓主要工作流程后再仔细把每个函数的源码看一看。
3. 工作流程
(1) 服务器启动,在指定端口或随机选取端口绑定 httpd 服务。
(2)收到一个 HTTP 请求时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数。
(3)取出 HTTP 请求中的 method (GET 或 POST) 和 url,。对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数。
(4) 格式化 url 到 path 数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下。当 url 以 / 结尾,或 url 是个目录,则默认在 path 中加上 index.html,表示访问主页。
(5)如果文件路径合法,对于无参数的 GET 请求,直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,跳到(10)。其他情况(带参数 GET,POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本。
(6)读取整个 HTTP 请求并丢弃,如果是 POST 则找出 Content-Length. 把 HTTP 200 状态码写到套接字。
(7) 建立两个管道,cgi_input 和 cgi_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_input,已被重定向到 STDIN,读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。这一部分比较乱,见下图说明:
图 1 管道初始状态
图 2 管道最终状态
(10) 关闭与浏览器的连接,完成了一次 HTTP 请求与回应,因为 HTTP 是无连接的。
看execute_cgi函数,
[cpp] view plain copy
if (strcasecmp(method, “GET”) ==0)
{
while ((numchars > 0) && strcmp(“\n”, buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
}
else /* POST */
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp(“\n”, buf))
{
buf[15] = ‘\0’;
if(strcasecmp(buf, “Content-Length:”) == 0)
content_length = atoi(&(buf[16]));
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -1) // {
bad_request(client);// 4xx:
return;
}
}
首先还是读取请求头并丢弃。 Content-Length描述HTTP消息实体的长度,对于POST方法,这个域是必须存在的,因为服务器需要这个长度来查找客户端请求的参数。
下面一段建立两个管道,
[cpp] view plain copy
if (pipe(cgi_output) < 0)
{
cannot_execute(client);
return;
}
if(pipe(cgi_input) < 0)
{
cannot_execute(client);
return;
}
pipe()会建立管道,并将文件描述词由参数filedes数组返回。filedes[0]为管道里的读取端,filedes[1]则为管道的写入端。管道一般是用来进程间通信的,好像目前为止只有一个进程?不急,马上就创建新的进程了。
[cpp] view plain copy
if ( (pid = fork()) < 0 ) //复制一个线程
{
cannot_execute(client);
return;
}
Linux的fork函数比较强大,但也不好理解。它可以创建一个子线程,那么当前的进程就是父进程。子进程可以获取父进程的地址空间的拷贝以及相关的资源,比如打开的文件描述符。这个很关键,上面获取的管道的描述符正是基于此特点才变得有意义。
那么问题来了,如何区分当前执行的是父进程还是子进程呢?fork函数如果调用出错会返回小于0的值。如果成功了,它会返回两次,一次是父进程中返回子进程的PID,一次是在子进程中返回0。这两个进程并发的执行,系统内核层是以时间片切换来模拟多进程的执行,不过作为程序员我们认为有两条线在并行的运行就可以了。
所以,通过判断返回的PID,就可以分别对子进程和父进程进行操作。在开始讲子进程之前,还需要插播一个概念:
CGI程序的特点是通过标准输入(stdin)和环境变量(可以理解成有两个传递数据的途径,二者相辅相成,其实跟请求方法是get或post也相关)来得到服务器的信息,并通过标准输出(stdout)向服务器输出信息。环境变量就是指的系统环境变量,就是Linux中可以用echo命令查询那些,比如HOME, PATH等。http协议中也定义了一些自己的环境变量,用于在CGI与服务器之间传输参数。下面给出几个相关的环境变量:
[html] view plain copy
SERVER_NAME
CGI脚本运行时的主机名和IP地址.
SERVER_SOFTWARE
你的服务器的类型如: CERN/3.0 或 NCSA/1.3.
GATEWAY_INTERFACE
运行的CGI版本.对于UNIX服务器, 这是CGI/1.1.
SERVER_PROTOCOL
服务器运行的HTTP协议. 这里当是HTTP/1.0.
SERVER_PORT
服务器运行的TCP口,通常Web服务器是80.
REQUEST_METHOD
POST 或 GET, 取决于你的表单是怎样递交的.
HTTP_ACCEPT
浏览器能直接接收的Content-types, 可以有HTTP Accept header定义.
HTTP_USER_AGENT
递交表单的浏览器的名称、版本 和其他平台性的附加信息。
HTTP_REFERER
递交表单的文本的 URL,不是所有的浏览器都发出这个信息,不要依赖它
PATH_INFO
附加的路径信息, 由浏览器通过GET方法发出.
PATH_TRANSLATED
在PATH_INFO中系统规定的路径信息.
SCRIPT_NAME
指向这个CGI脚本的路径,是在URL中显示的(如, /cgi-bin/thescript).
QUERY_STRING
脚本参数或者表单输入项(如果是用GET递交). QUERY_STRING 包含URL中问号后面的参数.
REMOTE_HOST
递交脚本的主机名,这个值不能被设置.
REMOTE_ADDR
递交脚本的主机IP地址.
REMOTE_USER
递交脚本的用户名. 如果服务器的authentication被激活,这个值可以设置。
REMOTE_IDENT
如果Web服务器是在ident(一种确认用户连接你的协议)运行,递交表单的系统也在运行ident, 这个变量就含有ident返回值.
CONTENT_TYPE
如果表单是用POST递交, 这个值将是application/x-www-form-urlencoded. 在上载文件的表单中,content-type 是个multipart/form-data.
CONTENT_LENGTH
对于用POST递交的表单, 标准输入口的字节数.
看看子进程,
[cpp] view plain copy
if (pid == 0) /* child: CGI script */
{
….
dup2(cgi_output[1], 1);///* 把 STDOUT 重定向到cgi_output 的写入端 */
dup2(cgi_input[0], 0); ///* 把 STDIN 重定向到cgi_input 的读取端 */
///* 关闭 cgi_input 的写入端和 cgi_output 的读取端 */
close(cgi_output[0]);
close(cgi_input[1]);
前面讲到cgi默认输出是标准输出,所以子进程在运行cgi程序之前,会通过dup2函数把标准输出重定向到与客户端关联的描述符上,这样后面cgi写到标准输出的东西都会直接到客户端。
当然,重定向完成后要把不用的描述符关掉,注意这里的不用是仅子进程不用,前面说到fork出来的子进程会拥有一份和父进程几乎一样的资源拷贝,所以这里只是把子进程空间中的描述符资源关闭。
继续看代码,
[cpp] view plain copy
sprintf(meth_env, “REQUEST_METHOD=%s”, method);
putenv(meth_env);
if(strcasecmp(method, “GET”) == 0)
{
sprintf(query_env, “QUERY_STRING=%s”, query_string);
putenv(query_env);
}
else
{ /* POST*/
sprintf(length_env, “CONTENT_LENGTH=%d”, content_length);
putenv(length_env);
}
前面已经提到环境变量是服务与CGI之间传递数据的一个途径,这一段就是服务器设置环境变量。
准备工作都作完了,可以调用cgi程序了,cgi所在的路径是前面保存的地址,还记得吧。
[cpp] view plain copy
execl(path, path, NULL);
exit(0);
}
再来看看父进程的代码,
[cpp] view plain copy
close(cgi_output[1]);
close(cgi_input[0]);
if(strcasecmp(method, “POST”) == 0)
for (i = 0; i < content_length; i++)
{
recv(client, &c, 1, 0);
write(cgi_input[1], &c, 1);/把 POST 数据写入cgi_input,已经重定向到 STDIN /
}
while (read(cgi_output[0], &c, 1) > 0)/读取 cgi_output的管道输出到客户端,该管道输入是 STDOUT /
send(client, &c, 1, 0);
close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid, &status, 0);
首先是关闭不用的文件描述符,避免资源泄露,然后父进程会把数据(post或get)写入cig_input[1], 因为前面已经重定向了,所以实际是写到stdin。然后从cgi_output[0]读取输出结果发送回客户端。
最后回到accept_request函数,最后一行是
close(client);
这个有必要说一下,http是基于无连接的,也就是完成一次请求后,马上断开与客户端的连接,所以一定要有这个