A Simple Web Server
介绍
在过去20几年里,网络已经在各个方面改变了我们的生活,但是它的核心却几乎没有什么改变。多数的系统依然遵循着Tim Berners-Lee在上个世纪发布的规则。大多数的web服务器都在用同样的方式处理消息
背景
多数在web上的服务器都是运行在IP协议标准上。在这协议家族里面我们关心的成员就是TCP,这个协议使得计算机之间的通信看起来像是在读写文件。
项目通过套接字来使用IP通信。每个套接字都是一个点对点的通信信道,一个套接字包含IP地址,端口来标识具体的机器。IP地址包含4个8Bit的数字,比如174.136.14.108;DNS将这些数字匹配到更加容易识别的名字比如aosabook.org,这样更加便于人们记住。
HTTP是一种可以在IP之上传输数据的方式。HTTP非常简单:客户端在套接字连接上发送一个请求指示需要什么样的信息,然后服务端就发送响应。数据可以是从硬盘上的文件拷贝过来,程序动态生成,或者是两者结合
HTTP请求中最重要的就是文本:任何项目都可以创造或者解析一个文本。为了便于理解,文本有图中所示的部分
HTTP方法一般采用”GET”(去获取信息)或者”POST”(去提交表单数据或者上传文件)。URL指明了客户端想要的;一般是硬件上文件的路径,比如/research/experiments.html
,但是这一切都取决于服务器端如何去做。
HTTP
版本一般是
"HTTP/1.0"
或者
"HTTP/1.1"
;我们并不关心这两者的差别。
HTTP
的头是像下面的成对键值:
Accept: text/html
Accept-Language: en, fr
If-Modified-Since: 16-May-2005
和哈希表中的键值不一样的是,键值在
HTTP
头中可以出现任意的次数。这就使得请求可以去指定它愿意接受的几种类型。
最后,请求的主体是与请求相关联的任何额外数据。这些将被用在通过表单提交数据,上传文件等等。
在最后一个标头和主体的开始之间必须有空白行以表示标头的结束。
一个被称为
Content-length
的头,用来告诉在请求数据中期望读取多数个字节。
HTTP
响应也和
HTTP
请求是一样的格式
版本
,头信息和主体都是同样的格式。状态码是一个数字用来指示请求处理时发生了什么:
200
意味着正常工作,
404
意味着没有找到,其他的码也有不同的意思。
对于这章节
,我们只需要知道
HTTP
的其他两件事。
第一个就是无状态
:每个请求都处理自己的,并且服务器端。服务器不会记住当前请求和下一个请求之间的内容。如果应用想跟踪比如用户身份的信息,就必须自己处理。
通常采用的方法是用
cookie
,
cookie
是服务器发送给客户端的字符流,然后客户端返回给服务器。当一个用户需要实现在不同请求之间保持状态的时候,服务器会创建
cookie
,存储在数据库里,然后发送给浏览器。每次浏览器把
cookie
值发送回来的时候,服务器都会用来去查找信息来知道用户在干什么。
第二个我们需要了解关于
HTTP
的就是
URL
可以通过提供参数来提供更多的信息
。比如,如果我们在使用搜索引擎,我们必须指定搜索术语。我们可以加入到
URL
的路径中,但是我们一般都是加入到
URL
的参数中。我们在
URL
中增加
?,
后面跟随
key=value
并且用
&
符号分割来达到这个目的。比如
URL
http://www.google.ca?q=Python
就告诉
Google
去搜索
Python
相关的网页。键值是字母
q
,值是
Python
。更长的查询
http://www.google.ca/search?q=Python&client=Firefox
告诉
Google
我们正在使用
Firefox
等等
。我们可以传输任何我们需要的参数。但是使用哪一个,如何解释这些参数取决于应用。
当然
,如果
?
和
&
特殊的字符,那么必须有一种方法去规避,
正如必须有一种方法将双引号字符放入由双引号分隔的字符串中一样
。
URL
的编码标准用
%
后面跟
2
个字节码的方式来表示特殊字符,用
+
来代替空格。所以为了在
Google
上搜索”
grade=A+”
,我们可以使用的
URL
为
http://www.google.ca/search?q=grade+%3D+A%2B
创建
sockets
,构建
HTTP
请求,解析响应是非常枯燥的事情。所以人们更多是使用库函数来完成大部分的工作。
Python
附带了一个
urllib2
的库,但是它暴露了很多人根本不关心的管道。
Request
库是可以替代
urllib2
并且更加好使用的库。下面是一个从
AOA
网站下载网页的例子。
import requests
response = requests.get('http://aosabook.org/en/500L/web-server/testpage.html')
print 'status code:', response.status_code
print 'content length:', response.headers['content-length']
print response.text
status code: 200
content length: 61
<html>
<body>
<p>Test page.</p>
</body>
</html>
requests.get
发送一个
HTTP GET
请求到服务器然后返回一个包含响应的对象。对象的
status_code
成员是响应的状态码;
content_length
成员是响应数据的长度,
text
是真是的数据
(
在这个例子中,是
HTTP
网页
)
你好
,web
现在我们准备去写第一个
简单的
web
服务器。
1
等待某人连接到服务器上并且发送一个请求
2
解析请求
3
指出要求获取的东西
4
获取数据(或者动态的产生)
5
将数据格式化为
HTML
格式
6
发送回去
1,2,6
步对于各种不同的应用来说都是一样的,
Python
标准库有一个模块称为
BaseHTTPServer
为我们做完成这些。我们需要完成的是步骤
3
到步骤
5.
这一部分只需要很少的工作
import BaseHTTPServer
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
'''Handle HTTP requests by returning a fixed 'page'.'''
# Page to send back.
Page = '''\
<html>
<body>
<p>Hello, web!</p>
</body>
</html>
'''
# Handle a GET request.
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(self.Page)))
self.end_headers()
self.wfile.write(self.Page)
if __name__ == '__main__':
serverAddress = ('', 8080)
server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)
server.serve_forever()
BaseHTTPRequestHandler
库会解析传入的
HTTP
请求然后决定里面包含的方法
。如果方法是
GET
,类就会调用
do_GET
的函数。我们自己的类
RequestHandler
重写了这个方法来动态生成网页
:文本
text
存储在类级别的参数
page,Page
将会在发送了
200
响应码后发送给客户端,
Content-Type
头告诉客户端用
HTML
的方式来解析数据以及网页的长度
(
end_headers
方法在我们的头和网页之间插入空白行
)
但是
RequestHandler
并不是整个的工程
:我们依然需要最后的三行启动服务器。第一行用一个元组的方式来定义服务器的地址:空字符意味着运行在本机上,
8080
是端口。然后我们用整个地址和
RequestHandler
作为参数来创建
BaseHTTPServer.HTTPServe
r
实例,然后让程序永远运行
(
在实际中,除非用
Control-C
停止整个程序
)
如果我们在命令行中运行整个项目
,不会显示任何东西
$ python server.py
如果我们在浏览器中输入
http://localhost:8080
,我们会在浏览器中得到如下的显示
Hello, web!
在
shell
中将会看到
127.0.0.1 - - [24/Feb/2014 10:26:28] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [24/Feb/2014 10:26:28] "GET /favicon.ico HTTP/1.1" 200 -
第一行是直截了当的
:因为我们并没有要求获取具体的文件,浏览器要求获取”
/”(
服务器运行的根目录
)
。第二行出现是因为浏览器自动发送第二个请求去获取图片文件
/favicon.ico
,它将在地址栏中显示为图标。
显示
数值
让我们修改下
web
服务器使得可以显示在
HTTP
请求中的内容
(
将来在调试的过程中我们经常会做这件事,所以我们先练习下
)
为了保持我们的代码干净,我们将发送和创建页面分开
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# ...page template...
def do_GET(self):
page = self.create_page()
self.send_page(page)
def create_page(self):
# ...fill in...
def send_page(self, page):
# ...fill in...
send_page
的代码和之前的一样
def send_page(self, page):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(page)))
self.end_headers()
self.wfile.write(page)
想要显示的网页模板是一个字符串
,其中包含了
HTML
表格以及一些格式化的占位符
Page = '''\
<html>
<body>
<table>
<tr> <td>Header</td> <td>Value</td> </tr>
<tr> <td>Date and time</td> <td>{date_time}</td> </tr>
<tr> <td>Client host</td> <td>{client_host}</td> </tr>
<tr> <td>Client port</td> <td>{client_port}s</td> </tr>
<tr> <td>Command</td> <td>{command}</td> </tr>
<tr> <td>Path</td> <td>{path}</td> </tr>
</table>
</body>
</html>
'''
填充的方法如下
:
def create_page(self):
values = {
'date_time' : self.date_time_string(),
'client_host' : self.client_address[0],
'client_port' : self.client_address[1],
'command' : self.command,
'path' : self.path
}
page = self.Page.format(**values)
return page
程序的主体并没有改变
:和之前一样,创建了一个
HTTPServer
类实例
,其中包含地址和请求,然后服务器就永远工作。如果我们开始运行并且从浏览器中发送请求
http://localhost:8080/something.html
。我们将得到:
Date and time Mon, 24 Feb 2014 17:17:12 GMT
Client host 127.0.0.1
Client port 54548
Command GET
Path /something.html
即使
something.html
网页不在网页上,我们也没有发现
404
异常。这是因为服务器只是一个程序,
当收到请求时,它可以做任何它想做的事:发送回前一个请求中命名的文件,提供随机选择的维基百科页面,或者我们对它进行编程的任何其他内容。 静态网页 下一步就是从硬盘上的网页开始启动而不是随机产生一个。我们可以重写do_GETdef do_GET(self):
try:
# Figure out what exactly is being requested.
full_path = os.getcwd() + self.path
# It doesn't exist...
if not os.path.exists(full_path):
raise ServerException("'{0}' not found".format(self.path))
# ...it's a file...
elif os.path.isfile(full_path):
self.handle_file(full_path)
# ...it's something we don't handle.
else:
raise ServerException("Unknown object '{0}'".format(self.path))
# Handle errors.
except Exception as msg:
self.handle_error(msg)
这个函数假设被允许web服务器正在运行的目录或者目录下的任何文件(通过os.getcwd来获取)。程序会将URL中包含的路径和当前的路径组装起来(URL中的路径放在self.path变量中,初始化的时候都是’/’)来得到用户需要的文件路径 如果路径不存在,或者不是个文件,函数将会通过产生并捕获一个异常来报告错误。如果路径和文件匹配,则会调用handle_file函数来读取并返回内容。这个函数读取文件并且使用send_content来发送给客户端
def handle_file(self, full_path):
try:
with open(full_path, 'rb') as reader:
content = reader.read()
self.send_content(content)
except IOError as msg:
msg = "'{0}' cannot be read: {1}".format(self.path, msg)
self.handle_error(msg)
注意到我们用二进制的方式来打开文件--’rb’中的’b’. 这样Python就不会帮我们通过过改变看起来像Windows行结尾的字节序列。并且在运行的时候,将整个的文件读进内存是个很糟糕的主意。像视频文件有可能是好几个G的大小。但是处理那样的情况不在本章节的考虑之内。 为了完成这个类,我们还需要写一个异常处理方法以及错误报告的网页模板
Error_Page = """\
<html>
<body>
<h1>Error accessing {path}</h1>
<p>{msg}</p>
</body>
</html>
"""
def handle_error(self, msg):
content = self.Error_Page.format(path=self.path, msg=msg)
self.send_content(content)
这个程序可以工作了,但是我们仔细看会发现问题。问题在与总是返回200的状态码,即使被请求的的网页不存在。是的,在这种情况下,发送回的页面包含错误信息,但是浏览器不能阅读英文,所以也不知道request是成功还是失败。为了让这种情况更清晰,我们需要修改handle_error和send_content。
# Handle unknown objects.
def handle_error(self, msg):
content = self.Error_Page.format(path=self.path, msg=msg)
self.send_content(content, 404)
# Send actual content.
def send_content(self, content, status=200):
self.send_response(status)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
在一个文件没被找到的时候我们没有抛出ServerException
异常
,而是产生了一个错误的页面。
ServerException
是为了在我们自己搞错的时候发送一个内部错误的信号。
handle_error
创建的异常网页,只会在用户发生错误的时候发生。比如发送
URL
中的文件并不存在。
显示目录
下一步,我们将教会服务器当
URL
是一个目录而不是文件的时候显示路径的内容。我们还可以走远一点在路径中去寻找
index.html
文件并显示出来,并且在文件不存在的时候显示路径的内容。
但是在
do_GET
中建立这些规则将会是个错误,因为所得到的方法将是一长串控制特殊行为的
if
语句。正确的解决方法是退后并解决一般性问题,那就是指出
URL
将要发生的动作。下面是对
do_GET
的重写。
def do_GET(self):
try:
# Figure out what exactly is being requested.
self.full_path = os.getcwd() + self.path
# Figure out how to handle it.
for case in self.Cases:
handler = case()
if handler.test(self):
handler.act(self)
break
# Handle errors.
except Exception as msg:
self.handle_error(msg)
第一步都是一样的
:指出请求的全路径。尽管如此,代码还是看起来不一样,不是一堆的内联测试,这个版本查找存储在列表中的事件集合。每个事件对象都有
2
个方法:
test
,用来告诉我们是否可以处理这个请求以及
act
,用来实际执行动作。一旦我们找到了正确的事件,我们就开始处理请求并且跳出循环。
下面三个对象事件重新塑造了服务器的行为:
class case_no_file(object):
'''File or directory does not exist.'''
def test(self, handler):
return not os.path.exists(handler.full_path)
def act(self, handler):
raise ServerException("'{0}' not found".format(handler.path))
class case_existing_file(object):
'''File exists.'''
def test(self, handler):
return os.path.isfile(handler.full_path)
def act(self, handler):
handler.handle_file(handler.full_path)
class case_always_fail(object):
'''Base case if nothing else worked.'''
def test(self, handler):
return True
def act(self, handler):
raise ServerException("Unknown object '{0}'".format(handler.path))
在
RequestHandler
类的开始的时候
,我们将将建立事件处理列表。
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
'''
If the requested path maps to a file, that file is served.
If anything goes wrong, an error page is constructed.
'''
Cases = [case_no_file(),
case_existing_file(),
case_always_fail()]
...everything else as before...
现在服务器代码变得越来越复杂
:代码行数从
74
变成了
99
,还有一个额外的间接级别且没有函数。当我们回到本章开始的任务,并试图教我们的服务器在
index.html
页面上提供一个目录(如果有的话)以及目录列表(如果没有的话)时,就会得到好处
。之前的处理如下:
class case_directory_index_file(object):
'''Serve index.html page for a directory.'''
def index_path(self, handler):
return os.path.join(handler.full_path, 'index.html')
def test(self, handler):
return os.path.isdir(handler.full_path) and \
os.path.isfile(self.index_path(handler))
def act(self, handler):
handler.handle_file(self.index_path(handler))
index_path
方法构建到
index.html
的路径;将其放入
case
处理程序可以防止主
RequestHandler
中的混乱,测试检查路径是否是包含
index.html
页面的目录,
act
请求主请求程序去为该网页提供服务。
RequestHandler
唯一的变化是在
Cases
列表中添加
case_directory_index_file
对象。
Cases = [case_no_file(),
case_existing_file(),
case_directory_index_file(),
case_always_fail()]
如果路径中不包含
index.html
网页
?测试和上面的一样,仅仅是插入了一个
not
语句,但是
act
方法如何处理?它应该做什么
class case_directory_no_index_file(object):
'''Serve listing for a directory without an index.html page.'''
def index_path(self, handler):
return os.path.join(handler.full_path, 'index.html')
def test(self, handler):
return os.path.isdir(handler.full_path) and \
not os.path.isfile(self.index_path(handler))
def act(self, handler):
???
看起来像是我们将自己逼入了墙角
。从逻辑上来说
,
act
方法应该创建,返回路径列表,但是我们的代码不允许这样:
RequestHandler.do_GET
调用
act
,但是并没有期望去处理和返回值。现在,让我们在
RequestHandler
加一个方法去生成路径列表,然后从事件的处理器
act
中去调用。
class case_directory_no_index_file(object):
'''Serve listing for a directory without an index.html page.'''
# ...index_path and test as above...
def act(self, handler):
handler.list_dir(handler.full_path)
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# ...all the other code...
# How to display a directory listing.
Listing_Page = '''\
<html>
<body>
<ul>
{0}
</ul>
</body>
</html>
'''
def list_dir(self, full_path):
try:
entries = os.listdir(full_path)
bullets = ['<li>{0}</li>'.format(e)
for e in entries if not e.startswith('.')]
page = self.Listing_Page.format('\n'.join(bullets))
self.send_content(page)
except OSError as msg:
msg = "'{0}' cannot be listed: {1}".format(self.path, msg)
self.handle_error(msg)
CGI
协议
当然
,多数的人都不想去编辑
web
服务器的源代码来增加新的功能。为了不给开发者增加更多的工作量,服务器总是支持称为
CGI
的机制,这为服务器提供了一种标准的方法去运行外部程序来满足需求。
比如
,加入我们想服务器能够在
HTML
网页上显示当地时间。我们可以在程序中增加几行代码
from datetime import datetime
print '''\
<html>
<body>
<p>Generated {0}</p>
</body>
</html>'''.format(datetime.now())
为了让服务器运行程序,我们增加了事件处理器:
class case_cgi_file(object):
'''Something runnable.'''
def test(self, handler):
return os.path.isfile(handler.full_path) and \
handler.full_path.endswith('.py')
def act(self, handler):
handler.run_cgi(handler.full_path)
测试样例:这个路径是否是以
.py
结尾?如果是,
RequestHandler
运行这个程序
def run_cgi(self, full_path):
cmd = "python " + full_path
child_stdin, child_stdout = os.popen2(cmd)
child_stdin.close()
data = child_stdout.read()
child_stdout.close()
self.send_content(data)
这样非常的不安全
:如果有人知道了服务器上的
Python
文件路径,我们就允许去运行这些程序而没有去关心传入了些什么数据,是否包含了死循环或者其他的。
先不管上面的这些,我们的核心观点很简单:
1
在子进程中运行程序
2
捕获子进程发送到标准输出的任何数据
3
将输出发送回触发请求的客户端
完成的
CGI
程序比这个更加负责
----
总的来说,它允许服务器将
URL
中的参数传递给正在运行的程序,但是这些细节并不影响系统的架构。
RequestHandler
有一个初始函数,
handle_file
用来处理内容。我们现在以
list_dir
和
run_cgi
的形式增加了
2
个特殊的事件。这三个方法并不属于当前的位置
,因为它们是被其他地方调用
解决办法很简单:为所有的事件处理创建一个父类,如果其他方法被多个处理器共享使用就将其移入到类中。当我们完成的时候,
RequestHandler
类看起来如下:
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
Cases = [case_no_file(),
case_cgi_file(),
case_existing_file(),
case_directory_index_file(),
case_directory_no_index_file(),
case_always_fail()]
# How to display an error.
Error_Page = """\
<html>
<body>
<h1>Error accessing {path}</h1>
<p>{msg}</p>
</body>
</html>
"""
# Classify and handle request.
def do_GET(self):
try:
# Figure out what exactly is being requested.
self.full_path = os.getcwd() + self.path
# Figure out how to handle it.
for case in self.Cases:
if case.test(self):
case.act(self)
break
# Handle errors.
except Exception as msg:
self.handle_error(msg)
# Handle unknown objects.
def handle_error(self, msg):
content = self.Error_Page.format(path=self.path, msg=msg)
self.send_content(content, 404)
# Send actual content.
def send_content(self, content, status=200):
self.send_response(status)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
事件处理器的父类如下
:
class base_case(object):
'''Parent for case handlers.'''
def handle_file(self, handler, full_path):
try:
with open(full_path, 'rb') as reader:
content = reader.read()
handler.send_content(content)
except IOError as msg:
msg = "'{0}' cannot be read: {1}".format(full_path, msg)
handler.handle_error(msg)
def index_path(self, handler):
return os.path.join(handler.full_path, 'index.html')
def test(self, handler):
assert False, 'Not implemented.'
def act(self, handler):
assert False, 'Not implemented.'
处理存在文件的代码如下
:
class case_existing_file(base_case):
'''File exists.'''
def test(self, handler):
return os.path.isfile(handler.full_path)
def act(self, handler):
self.handle_file(handler, handler.full_path)
版本
,头信息和主体都是同样的格式。状态码是一个数字用来指示请求处理时发生了什么:
200
意味着正常工作,
404
意味着没有找到,其他的码也有不同的意思。
对于这章节
,我们只需要知道
HTTP
的其他两件事。
第一个就是无状态
:每个请求都处理自己的,并且服务器端。服务器不会记住当前请求和下一个请求之间的内容。如果应用想跟踪比如用户身份的信息,就必须自己处理。
通常采用的方法是用
cookie
,
cookie
是服务器发送给客户端的字符流,然后客户端返回给服务器。当一个用户需要实现在不同请求之间保持状态的时候,服务器会创建
cookie
,存储在数据库里,然后发送给浏览器。每次浏览器把
cookie
值发送回来的时候,服务器都会用来去查找信息来知道用户在干什么。
第二个我们需要了解关于
HTTP
的就是
URL
可以通过提供参数来提供更多的信息
。比如,如果我们在使用搜索引擎,我们必须指定搜索术语。我们可以加入到
URL
的路径中,但是我们一般都是加入到
URL
的参数中。我们在
URL
中增加
?,
后面跟随
key=value
并且用
&
符号分割来达到这个目的。比如
URL
http://www.google.ca?q=Python
就告诉
Google
去搜索
Python
相关的网页。键值是字母
q
,值是
Python
。更长的查询
http://www.google.ca/search?q=Python&client=Firefox
告诉
Google
我们正在使用
Firefox
等等
。我们可以传输任何我们需要的参数。但是使用哪一个,如何解释这些参数取决于应用。
当然
,如果
?
和
&
特殊的字符,那么必须有一种方法去规避,
正如必须有一种方法将双引号字符放入由双引号分隔的字符串中一样
。
URL
的编码标准用
%
后面跟
2
个字节码的方式来表示特殊字符,用
+
来代替空格。所以为了在
Google
上搜索”
grade=A+”
,我们可以使用的
URL
为
http://www.google.ca/search?q=grade+%3D+A%2B
创建
sockets
,构建
HTTP
请求,解析响应是非常枯燥的事情。所以人们更多是使用库函数来完成大部分的工作。
Python
附带了一个
urllib2
的库,但是它暴露了很多人根本不关心的管道。
Request
库是可以替代
urllib2
并且更加好使用的库。下面是一个从
AOA
网站下载网页的例子。
import requests
response = requests.get('http://aosabook.org/en/500L/web-server/testpage.html')
print 'status code:', response.status_code
print 'content length:', response.headers['content-length']
print response.text
status code: 200
content length: 61
<html>
<body>
<p>Test page.</p>
</body>
</html>
requests.get
发送一个
HTTP GET
请求到服务器然后返回一个包含响应的对象。对象的
status_code
成员是响应的状态码;
content_length
成员是响应数据的长度,
text
是真是的数据
(
在这个例子中,是
HTTP
网页
)
你好
,web
现在我们准备去写第一个
简单的
web
服务器。
1
等待某人连接到服务器上并且发送一个请求
2
解析请求
3
指出要求获取的东西
4
获取数据(或者动态的产生)
5
将数据格式化为
HTML
格式
6
发送回去
1,2,6
步对于各种不同的应用来说都是一样的,
Python
标准库有一个模块称为
BaseHTTPServer
为我们做完成这些。我们需要完成的是步骤
3
到步骤
5.
这一部分只需要很少的工作
import BaseHTTPServer
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
'''Handle HTTP requests by returning a fixed 'page'.'''
# Page to send back.
Page = '''\
<html>
<body>
<p>Hello, web!</p>
</body>
</html>
'''
# Handle a GET request.
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(self.Page)))
self.end_headers()
self.wfile.write(self.Page)
if __name__ == '__main__':
serverAddress = ('', 8080)
server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)
server.serve_forever()
BaseHTTPRequestHandler
库会解析传入的
HTTP
请求然后决定里面包含的方法
。如果方法是
GET
,类就会调用
do_GET
的函数。我们自己的类
RequestHandler
重写了这个方法来动态生成网页
:文本
text
存储在类级别的参数
page,Page
将会在发送了
200
响应码后发送给客户端,
Content-Type
头告诉客户端用
HTML
的方式来解析数据以及网页的长度
(
end_headers
方法在我们的头和网页之间插入空白行
)
但是
RequestHandler
并不是整个的工程
:我们依然需要最后的三行启动服务器。第一行用一个元组的方式来定义服务器的地址:空字符意味着运行在本机上,
8080
是端口。然后我们用整个地址和
RequestHandler
作为参数来创建
BaseHTTPServer.HTTPServe
r
实例,然后让程序永远运行
(
在实际中,除非用
Control-C
停止整个程序
)
如果我们在命令行中运行整个项目
,不会显示任何东西
$ python server.py
如果我们在浏览器中输入
http://localhost:8080
,我们会在浏览器中得到如下的显示
Hello, web!
在
shell
中将会看到
127.0.0.1 - - [24/Feb/2014 10:26:28] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [24/Feb/2014 10:26:28] "GET /favicon.ico HTTP/1.1" 200 -
第一行是直截了当的
:因为我们并没有要求获取具体的文件,浏览器要求获取”
/”(
服务器运行的根目录
)
。第二行出现是因为浏览器自动发送第二个请求去获取图片文件
/favicon.ico
,它将在地址栏中显示为图标。
显示
数值
让我们修改下
web
服务器使得可以显示在
HTTP
请求中的内容
(
将来在调试的过程中我们经常会做这件事,所以我们先练习下
)
为了保持我们的代码干净,我们将发送和创建页面分开
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# ...page template...
def do_GET(self):
page = self.create_page()
self.send_page(page)
def create_page(self):
# ...fill in...
def send_page(self, page):
# ...fill in...
send_page
的代码和之前的一样
def send_page(self, page):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(page)))
self.end_headers()
self.wfile.write(page)
想要显示的网页模板是一个字符串
,其中包含了
HTML
表格以及一些格式化的占位符
Page = '''\
<html>
<body>
<table>
<tr> <td>Header</td> <td>Value</td> </tr>
<tr> <td>Date and time</td> <td>{date_time}</td> </tr>
<tr> <td>Client host</td> <td>{client_host}</td> </tr>
<tr> <td>Client port</td> <td>{client_port}s</td> </tr>
<tr> <td>Command</td> <td>{command}</td> </tr>
<tr> <td>Path</td> <td>{path}</td> </tr>
</table>
</body>
</html>
'''
填充的方法如下
:
def create_page(self):
values = {
'date_time' : self.date_time_string(),
'client_host' : self.client_address[0],
'client_port' : self.client_address[1],
'command' : self.command,
'path' : self.path
}
page = self.Page.format(**values)
return page
程序的主体并没有改变
:和之前一样,创建了一个
HTTPServer
类实例
,其中包含地址和请求,然后服务器就永远工作。如果我们开始运行并且从浏览器中发送请求
http://localhost:8080/something.html
。我们将得到:
Date and time Mon, 24 Feb 2014 17:17:12 GMT
Client host 127.0.0.1
Client port 54548
Command GET
Path /something.html
即使
something.html
网页不在网页上,我们也没有发现
404
异常。这是因为服务器只是一个程序,
当收到请求时,它可以做任何它想做的事:发送回前一个请求中命名的文件,提供随机选择的维基百科页面,或者我们对它进行编程的任何其他内容。 静态网页 下一步就是从硬盘上的网页开始启动而不是随机产生一个。我们可以重写do_GETdef do_GET(self):
try:
# Figure out what exactly is being requested.
full_path = os.getcwd() + self.path
# It doesn't exist...
if not os.path.exists(full_path):
raise ServerException("'{0}' not found".format(self.path))
# ...it's a file...
elif os.path.isfile(full_path):
self.handle_file(full_path)
# ...it's something we don't handle.
else:
raise ServerException("Unknown object '{0}'".format(self.path))
# Handle errors.
except Exception as msg:
self.handle_error(msg)
这个函数假设被允许web服务器正在运行的目录或者目录下的任何文件(通过os.getcwd来获取)。程序会将URL中包含的路径和当前的路径组装起来(URL中的路径放在self.path变量中,初始化的时候都是’/’)来得到用户需要的文件路径 如果路径不存在,或者不是个文件,函数将会通过产生并捕获一个异常来报告错误。如果路径和文件匹配,则会调用handle_file函数来读取并返回内容。这个函数读取文件并且使用send_content来发送给客户端
def handle_file(self, full_path):
try:
with open(full_path, 'rb') as reader:
content = reader.read()
self.send_content(content)
except IOError as msg:
msg = "'{0}' cannot be read: {1}".format(self.path, msg)
self.handle_error(msg)
注意到我们用二进制的方式来打开文件--’rb’中的’b’. 这样Python就不会帮我们通过过改变看起来像Windows行结尾的字节序列。并且在运行的时候,将整个的文件读进内存是个很糟糕的主意。像视频文件有可能是好几个G的大小。但是处理那样的情况不在本章节的考虑之内。 为了完成这个类,我们还需要写一个异常处理方法以及错误报告的网页模板
Error_Page = """\
<html>
<body>
<h1>Error accessing {path}</h1>
<p>{msg}</p>
</body>
</html>
"""
def handle_error(self, msg):
content = self.Error_Page.format(path=self.path, msg=msg)
self.send_content(content)
这个程序可以工作了,但是我们仔细看会发现问题。问题在与总是返回200的状态码,即使被请求的的网页不存在。是的,在这种情况下,发送回的页面包含错误信息,但是浏览器不能阅读英文,所以也不知道request是成功还是失败。为了让这种情况更清晰,我们需要修改handle_error和send_content。# Handle unknown objects.
def handle_error(self, msg):
content = self.Error_Page.format(path=self.path, msg=msg)
self.send_content(content, 404)
# Send actual content.
def send_content(self, content, status=200):
self.send_response(status)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
在一个文件没被找到的时候我们没有抛出ServerException
异常
,而是产生了一个错误的页面。
ServerException
是为了在我们自己搞错的时候发送一个内部错误的信号。
handle_error
创建的异常网页,只会在用户发生错误的时候发生。比如发送
URL
中的文件并不存在。
显示目录