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>总结一下服务器的处理步骤:
解析浏览器的请求,在服务目录中查找相应的文件,如果找不到该文件就返回 404 错误页面
如果找到了浏览器请求的文件,用 stat(2) 检查它是否可执行
如果该文件可执行:
- 发送 HTTP/1.1 200 OK 给客户端
- fork(2),然后用 dup2(2) 重定向子进程的标准输出到客户端 socket
- 在子进程中 exec(3) 该 CGI 程序
- 关闭连接
如果该文件不可执行:
- 发送 HTTP/1.1 200 OK 给客户端
- 如果是一个图片文件,根据图片的扩展名发送相应的 Content-Type 给客户端
- 如果不是图片文件,这里我们简化处理,都当作
Content-Type: text/html - 简单的 HTTP 协议头有这两行就足够了,再发一个空行表示结束
- 读取文件的内容发送到客户端
- 关闭连接