花了好一会儿的时间去阅读了TinyHttpd
的源码,读完了确实对HTTP
和HTTP服务器
有了一些不一样的见解。
HTTP
之前一直在想,浏览器发起一个HTTP
请求的时候,到底在发送些什么。定义了那么多有的没有的东西,对于服务器来说,他是怎么根据HTTP头
去知道的。现在总算是知道了。
对于HTTP协议
而言,其本质上就是一系列的文本,浏览器在进行HTTP请求
的时候,实际上就是在发生一系列的文本信息。服务器在拿到了HTTP请求
之后,会对该请求进行分析,提取出有用的信息,进行处理之后再与客户端进行通信互动。
HTTP服务器
读完了该TinyHttpd
的代码后,确实对于HTTP服务器
的运行有了一个实质性的了解。记录一些阅读后的感悟。
该服务器采用纯C编写,全部代码不到500行。确实让人感到佩服。之前在学习TCP\IP
编程的时候,学到了socket
的各种用法,多进程的实现与坑等等。当时还在想就利用这些简单的函数来来回回是怎么实现网络编程的。现在看来,回归到本质,一切都很清晰了。在互联网上传输的数据,(除却媒体信息)。实质上都是一些文本信息,甚至媒体信息我们也可以当成文本信息来处理。
main
在该代码中,main
函数先建立一个服务器socket
,然后利用循环不断的接收信息。当有连接建立的时候,调用pthread_create
建立一个线程来处理连接。此外,该服务器socket
启动的时候,如果不指定端口,则会随机监听一个。
accept_request
此时进入该线程所执行的函数中。第一件事就是读取HTTP报文的第一行请求数据
,从中提取出该HTTP报文
的请求是啥。
如果是POST
请求或是带有参数的GET
请求,会将cgi
标志位记为1。之后可以调用cgi
对请求数据进行处理,否则直接发送一个静态文件给客户端。
这里需要注意两个数组url
和path
。
url
数组保存着网址的具体位置
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
{
url[i] = buf[j];
i++;
j++;
}
sprintf(path, "htdocs%s", url);
通过上述的代码获取url
信息和将url
信息填入网页具体位置。刚开始并不是很理解这两部分的代码。后来看了下几个网站的请求之后算明白了。
在http
的请求报文中。第一行的含义如下
Method Request-URI HTTP-Version CRLF
其中Request-URI
是除了网站网址之后的一系列东西。即对于127.0.0.1/index.html
,Request-URI
保存的是/index.html
。简单理解为Request-URI
保存着网址的客户端请求访问的具体网页文件。
对于cgi
标记位为1的请求,将会执行execute_cgi
,返回计算结果。
execute_cgi
该函数用于处理客户端的各类请求。
问题
其中有个地方不是特别理解。对于GET
请求,为何要把头信息都读取进来,不可以直接无视掉吗?
if (strcasecmp(method, "GET") == 0)
//如果是GET请求
//读取并且丢弃头信息
while ((numchars > 0) && strcmp("\n", buf))
numchars = get_line(client, buf, sizeof(buf));
对于这段循环读取完所有的请求头的代码,我个人认为是清空缓存区,防止缓存区不够。或是清理掉该部分的垃圾数据。
感悟
在该函数内,会产生两个管道:cgi_output
和cgi_input
。这两个管道的操控也是我花了最长时间的地方。
在GitHub
上找到两张图,非常完美的解释这两个管道的工作方式。
初始状态
最终状态
然后在代码中调用dup2(cgi_output[1], 1);
将标准输出全部重定向到cgi_output[1]
中。
所以,现在一共有三个指针指向cgi_output
这个管道入口!!!
这点非常重要。分别是以下三个指针
- 子进程的
cgi_output[1]
- 父进程的
cgi_output[1]
- 标准输出
同样的,对于cgi_input[1]
,最终也是会有三个指针指向它!
重点
上面提到了一共有三个变量指向cgi_output[1]
和 cgi_input[1]
。这里需要重点理解的是。对于一条管道,父进程和子进程会同时指向它的入口和出口!父进程和子进程的文件描述符是互不影响的!!!这个部分也是我一开始纠结了很久没意识到的地方
通过上面的解析,应该能够理解最终状态的管道是如何建立的了。
所以现在数据的流向就比较清晰了。
- 父进程读取了请求头的数据,将数据直接写入
cgi_input[1]
中。 - 子进程调用
cgi
脚本 cgi
脚本中的标准输入来源于cgi_input[1]
,处理完直接print
到标准输出- 到标准输出中的数据会被父进程从
cgi_output[0]
中获取 - 直接送到客户端
总结
该服务器的实现非常非常的简单明了,其中的难点在于对可执行脚本的数据流向处理,这种处理方式确实是第一次见到,所以纠结了好一会儿。不过看懂了之后发现异常清晰。
该源码采用七夜的注释版