500 Lines or Less | A Simple Web Server

原文:http://aosabook.org/en/500L/a-simple-web-server.html

Greg Wilson是Software Carpentry的创始人,Software Carpentry是面向科学家和工程师的计算机技能速成课程。他已经在工业界和学术界工作了30年,并且是多本计算机相关书籍的作者或主编,包括获得2008年计算机图书震撼大奖“震撼奖”(译者注:Jolt Award,软件届的奥斯卡奖,1998-2014年每年会评出一个震撼奖和三个生产力奖)的Beautiful Code以及The Architecture of Open Source Applications的前两卷。Greg于1993年获得了爱丁堡大学(Edinburgh University)的计算机科学与技术博士学位。

前言

在过去二十年里网络已经在很多方面改变了社会,但是其核心却变化很少。大多数系统仍然遵守着25年前Tim Berners-Lee提出的规则。尤其是,大部分网络服务器仍然在以同样的方式处理着与多年前相同的消息。

本章将会探讨网络服务器是如何工作的。同时,我们也将探讨开发者如何构建一个无需重写即可增加新功能的软件系统。

背景

Web上的几乎每个程序都运行在一个被称为因特网协议(IP)的通信标准家族上。该家族成员中与我们相关的是传输控制协议(TCP/IP),它使得计算机间的通信看起来好像是读写文件。

使用IP的程序通过套接字(socket)进行通信。每个套接字是端到端通信信道的一个端点,就像听筒是电话通信的一个端点。一个套接字由一个标识一个特定机器的IP地址和该机器上的一个端口号组成。IP地址由4个8比特位的数字构成,如174.136.14.108,域名系统(DNS)将这些数字与更容易被人类记忆的符号名称(如,aosabook.org)相匹配。

端口号是一个在0-65535范围内的数字,它在宿主机上唯一标识一个套接字。(如果说IP地址是一个公司的电话号码,那么端口号就是分机号。)端口0-1023是为操作系统使用保留的端口号,其他用户可以使用剩余端口号。

超文本传输协议描述了一种使程序在IP上交换数据的方式。HTTP设计得很简单:客户端发送请求说明它希望通过套接字连接获取什么,服务端在响应消息中发送一些数据(如图22.1)。这些数据可能是从硬盘上的一个文件中拷贝的,可能是由程序动态生成的,又或者二者兼有。

fab90c46a2f9e61ef730dedcac05a0e0.png
图22.1 HTTP周期

关于HTTP请求最重要的一点是http请求仅是文本:任何程序都可以创建一个http请求或解析一个http请求。但是,为了能够被理解,该文本必须包括图22.2中显示的字段。
e9d412daa6e24451d5da2eb9b8f97e81.png
图22.2 一个http请求

HTTP方法几乎都是要么“GET”(获取信息),要么“POST”(提交表单数据或上传文件)。URL描述了客户端想要什么,一般是指向硬盘上某个文件的路径,比如/research/experiments.html,但是(这也是最关键的环节)如何去处理这些URL是完全由服务端决定的。HTTP版本一般是“HTTP/1.0”或“HTTP/1.1”,我们并不关心这两者间的差别。

HTTP头部是一些键-值对,像下面显示的三个:

Accept: text/html
Accept-Language: en, fr
If-Modified-Since: 16-May-2005

不像哈希表中的键,HTTP头部中的键可以出现任何次数。这使得http请求可以表明它愿意接收多种类型的内容。

最后,请求体是与请求相关的其余所有数据。请求体用于通过表单提交数据、上传文件等操作。在请求头部的最后和请求体的开始间必须有一行空格,以表示请求头部结束。

HTTP请求的其中一个头部是Content-Length,用于告知服务器将会从请求体中读取多少字节的数据。

HTTP响应消息同HTTP请求消息的格式类似(如图22.3):
7f81855389ead55049720b394717523d.png
图22.3 HTTP响应消息

版本号、消息头部、消息体与HTTP请求的格式及含义相同。状态码是一个数字,用于表明请求被处理时发生了什么:200表示“一切工作正常”,404表示“未找到”,其余状态码也分别有其含义。状态码描述短语会以人类可理解的短语复述状态码信息,如“OK”“未找到”。

就本节内容而言,关于HTTP请求我们只需要再了解两件事。

第一件事是,HTTP是无状态的:每个请求仅可依靠其自身的信息被处理,服务端并不记得一个请求和下一个请求间的任何关系。如果一个应用程序希望记录一些信息如用户身份,那么它必须自己记录。

记录HTTP请求相关信息通常使用的方式是使用cookie,cookie是一个由服务端发送给客户端,而后又由客户端返回给服务端的短字符串。当用户执行某些需要在几个请求间保存状态的功能时,服务端将会创建一个新的cookie,将其存储在数据库中,并将该信息发送给浏览器。每次浏览器发送回cookie信息时,服务端便使用该cookie查找用户操作的相关信息。

第二件我们需要了解的关于HTTP的事情是,一个URL可以通过使用参数来补充提供更多的信息。例如,当我们使用搜索引擎时,我们需要指明我们的搜索项是什么。我们可以将搜索项信息添加到URL的路径中,但更好的方式是在URL中添加参数。方法是,在URL中添加‘?’,并在其后添加以‘&’分割的‘键=值’对。比如,URL http://www.google.ca?q=Python 要求谷歌查询python相关的页面:键为字母‘q’,值为‘python’。下边这个较长的查询 http://www.google.ca/search?q=Python&;client=Firefox 告知谷歌我们在使用Firefox客户端,等等。我们可以传递我们想传递的任意参数,但同样地,哪些参数应该重视以及如何去解释是由运行在网站服务器上的应用来决定的。

当然,如果‘?’和‘&’是特殊字符,则必须有方法对其进行转义,就像在使用双引号界定一个字符串时,必须存在方法使字符串中可以包括双引号字符。URL编码标准使用‘%’后紧跟一个两位编码来表示特殊符号,并使用‘+’来替换空格。因此,为了在Google中搜索“grade = A+”(包含空格),我们将会使用URL http://www.google.ca/search?q=grade+%3D+A%2B。

打开套接字,构建HTTP请求,以及解析响应消息的过程是烦冗的,因此大部分人使用库来完成大部分工作。Python自带一个名为urllib2(因为该库为一个早些的库urllib的替代品)的库,但该库暴露了很多大部分人从未关心的管道。Requests库是比urllib2库更容易使用的一个替代选择。如下示例是使用Requests库从AOSA图书网站下载一个页面:

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

request.get向服务器发送一个HTTP GET请求,并返回一个包含响应消息的对象。该对象的status_code成员是响应消息的返回状态码,content_length成员是响应数据中的字节数,text成员是实际数据(在本例中,是一个HTML页面)。

你好,网站

现在我们即将开始编写我们的第一个简单的网站服务器。基本思路很简单:

  1. 等待客户连接我们的服务器并向服务器发送一个HTTP请求;
  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重写了do_GET方法,以动态生成一个简单的页面:页面文本储存于类级变量Page,我们在发送了一个200响应状态码后会将该页面送回给客户端,一个Content-Type头部用于告知客户端来将我们的数据解释为HTML,以及页面的长度。(end_headers方法调用会插入一个可以将我们的消息头部和页面本身分割开的空白行。)

但是类RequestHandler并不是全部:我们还需要最后三行来实际启动服务器的运行。第一行将服务器的地址定义为一个元组:空字符串表示“在当前机器运行”,8080是指服务端口。接着我们使用该服务器地址和我们的请求句柄类名字作为参数,创建类BaseHTTPServer.HTTPServer的一个实例,然后,我们永久运行该实例(实际上是指直到我们使用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的图片文件的请求,该图片如果存在将会作为图标展示在地址栏中。

展示值

让我们修改我们的网站服务器,以便能显示在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表格的字符串,该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

程序的主体并未变化:与原来相同,它使用地址和请求句柄RequestHandler作为参数创建一个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_GET开始:

def 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)

该方法假设它可以服务网站运行目录(通过os.getcwd获得)下的任何文件。它将网站运行目录与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不会试图通过改变字节顺序来“帮助”我们,使其看起来像是window的行结尾。还应该注意的是,在实际应用中,当为用户提供文件请求服务时,将整个文件读取到内存中是一个非常糟糕的做法,因为该文件可能是包含几个G字节的视频数据。对此种情况的处理不在本章的讨论范围之内。

完成类RequestHandler,我们需要编写错误处理方法,以及错误报告页面的模板:

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状态码,即使用户请求的页面并不存在。是的,在页面不存在时返回的页面中会包含错误信息,然而,由于我们的浏览器无法读懂英文,因此,它们并不知道请求实际失败了。为了使返回结果更清晰,我们需要修改 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。【1】

目录列表

下一步,我们将教网络服务器在收到路径为目录而非文件的URL时去展示目录内容的列表。我们还可以更进一步,使其显示该目录下的index.html,只有在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)

第一步是相同的:计算被请求事物的完整路径。然而,之后的代码看起来差别很大。不是一堆嵌在函数内部的用例判断,该版本循环遍历存储于列表中的用例集。每个用例是一个具有两个方法的对象: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文件的路径,将它放在用例句柄中避免了主函数RequestHandler中过于杂乱。test检验该路径是否为包含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页面的目录呢?test除了在适当的地方插入了一个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协议

当然,大部分人不希望为了增加新功能而去编辑他们网络服务器的源码。为避免必须要如此做,服务器一般都支持一个成为通用网关接口的机制,它为网络服务器提供了一个标准的方式,使网络服务器可以运行一个额外的程序以满足某请求。

比如,假设我们希望服务器能够在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文件路径,那我们就会允许他们运行它,无论该程序访问了哪些数据,它是否包含一个无限循环或其它。【2】

解决该问题,核心思想很简单:
在子进程中运行程序。
捕获子进程向标准输出发的所有内容。
将捕获到的内容返回给发送该请求的客户端。

完整的CGI协议栈比上述更为丰富,尤其是,它允许URL中存在参数,网络服务器会将它们传递至运行的程序中,但这些细节并不会影响系统的整体架构… …它们又开始变得非常混乱了。RequestHandler初始只有一个方法handle_file,用于处理内容。现在我们已经以 list_dir 和run_cgi 的形式新添了两个特殊用例。这三个方法并没有在实际应该在的地方,因为他们主要被其它程序使用。

解决方法很直观:为我们所有的用例句柄创建一个父类,并将被两个及以上句柄共享的方法移到该类中。当我们完成这些后,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)
讨论

我们的初始代码与重构后版本之间的区别反映了两个重要的思想。第一个是,将类看作是相关服务的一个集合。RequestHandler 和 base_case 并不会做决定或采取行动,他们提供其他类可以用来完成这些事情的工具。

第二个是扩展性:人们可以通过写一个外部的CGI程序,或者增加用例句柄类,向我们的网络服务器添加新功能。后者确实需要对RequestHandler(将用例句柄插入到用例列表) 作一行的变动,但是我们可以通过让网络服务器读取配置文件并从文件加载句柄类来消除该变动。两种情况下,他们都可以忽略大部分低层次的细节,如同BaseHTTPRequestHandler 类的作者允许我们忽略处理套接字连接和解析HTTP请求的细节一样。

这些思想是通用的,端看你是否能在自己的项目中找到使用它们的方法。


【1】我们在本章中将多次使用handle_error,包括状态码404并不合适的几个场景。在你阅读时,试着去思考你将如何去扩展该程序,以满足在每个场景下都可以轻松提供相应的状态响应码。

【2】我们的源码中使用了popen2库函数,该库已经被subprocess模块代替。然而,在本例中,popen2不会让我们分散注意力。


参考代码:

# a_simple_web_server.py
import BaseHTTPServer
import os

class ServerException(Exception):
    '''server internal error'''
    pass

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_no_file(base_case):
    '''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_cgi_file(base_case):
    '''Something runnable.'''

    def run_cgi(self, handler):
        cmd = "python2 " + handler.full_path
        child_stdin, child_stdout = os.popen2(cmd)
        child_stdin.close()
        data = child_stdout.read()
        child_stdout.close()
        handler.send_content(data)
        
    def test(self, handler):
        return os.path.isfile(handler.full_path) and \
               handler.full_path.endswith('.py')

    def act(self, handler):
        self.run_cgi(handler)
        

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)


class case_directory_index_file(base_case):
    '''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):
        self.handle_file(handler, self.index_path(handler))


class case_directory_no_index_file(base_case):
    '''Serve listing for a directory without an index.html page.'''

    # How to display a directory listing.
    Listing_Page = '''\
        <html>
        <body>
        <ul>
        {0}
        </ul>
        </body>
        </html>
        '''

    def list_dir(self, handler, 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))
            handler.send_content(page)
        except OSError as msg:
            msg = "'{0}' cannot be listed: {1}".format(self.path, msg)
            self.handle_error(msg)

    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):
        self.list_dir(handler, handler.full_path)


class case_always_fail(base_case):
    '''Base case if nothing else worked.'''

    def test(self, handler):
        return True

    def act(self, handler):
        raise ServerException("Unknown object '{0}'".format(handler.path))


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_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)

    
#----------------------------------------------------------------------

if __name__ == '__main__':
    serverAddress = ('', 8080)
    server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)
    server.serve_forever()
# showdate.py
from datetime import datetime

print '''\
<html>
<body>
<p>Generated {0}</p>
</body>
</html>'''.format(datetime.now())
<!--index.html -->
<html>
<body>
<p>Hello, web!</p>
<p>This is a index page. </p>
</body>
</html>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值