5. 练习:实现简单的Web服务器

实现一个简单的Web服务器myhttpd。服务器程序启动时要读取配置文件/etc/myhttpd.conf,其中需要指定服务器监听的端口号和服务目录,例如:

Port=80
Directory=/var/www

注意,1024以下的端口号需要超级用户才能开启服务。如果你的系统中已经安装了某种Web服务器(例如Apache),应该为myhttpd选择一个不同的端口号。当浏览器向服务器请求文件时,服务器就从服务目录(例如/var/www)中找出这个文件,加上HTTP协议头一起发给浏览器。但是,如果浏览器请求的文件是可执行的则称为CGI程序,服务器并不是将这个文件发给浏览器,而是在服务器端执行这个程序,将它的标准输出发给浏览器,服务器不发送完整的HTTP协议头,CGI程序自己负责输出一部分HTTP协议头。

5.1. 基本HTTP协议

打开浏览器,输入服务器IP,例如 http://192.168.0.3 ,如果端口号不是80,例如是8000,则输入 http://192.168.0.3:8000 。这时浏览器向服务器发送的HTTP协议头如下:

GET / HTTP/1.1
Host: 192.168.0.3:8000
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.6) Gecko/20061201 Firefox/2.0.0.6 (Ubuntu-feisty)
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

注意,其中每一行的末尾都是回车加换行(C语言的"\r\n"),第一行是GET请求和协议版本,其余几行选项字段我们不讨论,HTTP协议头的最后有一个空行,也是回车加换行

我们实现的Web服务器只要能正确解析第一行就行了,这是一个GET请求,请求的是服务目录的根目录/(在本例中实际上是/var/www),Web服务器应该把该目录下的索引页(默认是index.html)发给浏览器,也就是把/var/www/index.html发给浏览器。假如该文件的内容如下(HTML文件没必要以"\r\n"换行,以"\n"换行就可以了):

<html>
<head><title>Test Page</title></head>
<body>
	<p>Test OK</p>
	<img src='mypic.jpg'>
</body>
</html>

显示一行字和一幅图片,图片的相对路径(相对当前的index.html文件的路径)是mypic.jpg,也就是/var/www/mypic.jpg,如果用绝对路径表示应该是:

<img src='/mypic.jpg'>

服务器应按如下格式应答浏览器:

HTTP/1.1 200 OK
Content-Type: text/html

<html>
<head><title>Test Page</title></head>
<body>
	<p>Test OK</p>
	<img src='mypic.jpg'>
</body>
</html>

服务器应答的HTTP头也是每行末尾以回车加换行结束,最后跟一个空行的回车加换行。

HTTP头的第一行是协议版本和应答码,200表示成功,后面的消息OK其实可以随意写,浏览器是不关心的,主要是为了调试时给开发人员看的。虽然网络协议最终是程序与程序之间的对话,但是在开发过程中却是人与程序之间的对话,一个设计透明的网络协议可以提供很多直观的信息给开发人员,因此,很多应用层网络协议,如HTTP、FTP、SMTP、POP3等都是基于文本的协议,为的是透明性(transparency)。

HTTP头的第二行表示即将发送的文件的类型(称为MIME类型),这里是text/html,纯文本文件是text/plain,图片则是image/jpg、image/png等。

然后就发送文件的内容,发送完毕之后主动关闭连接,这样浏览器就知道文件发送完了。这一点比较特殊:通常网络通信都是客户端主动发起连接,主动发起请求,主动关闭连接,服务器只是被动地处理各种情况,而HTTP协议规定服务器主动关闭连接(有些Web服务器可以配置成Keep-Alive的,我们不讨论这种情况)。

浏览器收到index.html之后,发现其中有一个图片文件,就会再发一个GET请求(HTTP协议头其余部分略):

GET /mypic.jpg HTTP/1.1

一个较大的网页中可能有很多图片,浏览器可能在下载网页的同时就开很多线程下载图片,因此,'''服务器即使对同一个客户端也需要提供并行服务的能力'''。服务器收到这个请求应该把图片发过去然后关闭连接:

HTTP/1.1 200 OK
Content-Type: image/jpg

(这里是mypic.jpg的二进制数据)

这时浏览器就应该显示出完整的网页了。

如果浏览器请求的文件在服务器上找不到,要应答一个404错误页面,例如:

HTTP/1.1 404 Not Found
Content-Type: text/html

<html><body>request file not found</body></html>

5.2. 执行CGI程序

如果浏览器请求的是一个可执行文件(不管是什么样的可执行文件,即使是shell脚本也一样),那么服务器并不把这个文件本身发给浏览器,而是把它的执行结果标准输出发给浏览器。例如一个shell脚本/var/www/myscript.sh(注意一定要加可执行权限):

#!/bin/sh
echo "Content-Type: text/html"
echo
echo "<html><body>Hello world!</body></html>"

这样浏览器收到的是:

HTTP/1.1 200 OK
Content-Type: text/html

<html><body>Hello world!</body></html>

总结一下服务器的处理步骤:

  1. 解析浏览器的请求,在服务目录中查找相应的文件,如果找不到该文件就返回404错误页面

  2. 如果找到了浏览器请求的文件,用stat(2)检查它是否可执行

  3. 如果该文件可执行:

    1. 发送HTTP/1.1 200 OK给客户端

    2. fork(2),然后用dup2(2)重定向子进程的标准输出到客户端socket

    3. 在子进程中exec(3)该CGI程序

    4. 关闭连接

  4. 如果该文件不可执行:

    1. 发送HTTP/1.1 200 OK给客户端

    2. 如果是一个图片文件,根据图片的扩展名发送相应的Content-Type给客户端

    3. 如果不是图片文件,这里我们简化处理,都当作Content-Type: text/html

    4. 简单的HTTP协议头有这两行就足够了,再发一个空行表示结束

    5. 读取文件的内容发送到客户端

    6. 关闭连接