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非常简单:客户端在套接字连接上发送一个请求指示需要什么样的信息,然后服务端就发送响应。数据可以是从硬盘上的文件拷贝过来,程序动态生成,或者是两者结合

p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }

HTTP请求中最重要的就是文本:任何项目都可以创造或者解析一个文本。为了便于理解,文本有图中所示的部分

p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }

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请求是一样的格式

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

版本,头信息和主体都是同样的格式。状态码是一个数字用来指示请求处理时发生了什么:200意味着正常工作,404意味着没有找到,其他的码也有不同的意思。

对于这章节,我们只需要知道HTTP的其他两件事。

第一个就是无状态:每个请求都处理自己的,并且服务器端。服务器不会记住当前请求和下一个请求之间的内容。如果应用想跟踪比如用户身份的信息,就必须自己处理。

通常采用的方法是用cookiecookie是服务器发送给客户端的字符流,然后客户端返回给服务器。当一个用户需要实现在不同请求之间保持状态的时候,服务器会创建cookie,存储在数据库里,然后发送给浏览器。每次浏览器把cookie值发送回来的时候,服务器都会用来去查找信息来知道用户在干什么。

第二个我们需要了解关于HTTP的就是URL可以通过提供参数来提供更多的信息。比如,如果我们在使用搜索引擎,我们必须指定搜索术语。我们可以加入到URL的路径中,但是我们一般都是加入到URL的参数中。我们在URL中增加?,后面跟随key=value并且用&符号分割来达到这个目的。比如URLhttp://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+”,我们可以使用的URLhttp://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>

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

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.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,它将在地址栏中显示为图标。
显示数值
让我们修改下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_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)

这个函数假设被允许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_dirrun_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)

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }


pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

版本,头信息和主体都是同样的格式。状态码是一个数字用来指示请求处理时发生了什么:200意味着正常工作,404意味着没有找到,其他的码也有不同的意思。

对于这章节,我们只需要知道HTTP的其他两件事。

第一个就是无状态:每个请求都处理自己的,并且服务器端。服务器不会记住当前请求和下一个请求之间的内容。如果应用想跟踪比如用户身份的信息,就必须自己处理。

通常采用的方法是用cookiecookie是服务器发送给客户端的字符流,然后客户端返回给服务器。当一个用户需要实现在不同请求之间保持状态的时候,服务器会创建cookie,存储在数据库里,然后发送给浏览器。每次浏览器把cookie值发送回来的时候,服务器都会用来去查找信息来知道用户在干什么。

第二个我们需要了解关于HTTP的就是URL可以通过提供参数来提供更多的信息。比如,如果我们在使用搜索引擎,我们必须指定搜索术语。我们可以加入到URL的路径中,但是我们一般都是加入到URL的参数中。我们在URL中增加?,后面跟随key=value并且用&符号分割来达到这个目的。比如URLhttp://www.google.ca?q=Python就告诉Google去搜索Python相关的网页。键值是字母q,值是Python。更长的查询http://www.google.ca/search?q=Python&amp;client=Firefox告诉Google我们正在使用Firefox等等。我们可以传输任何我们需要的参数。但是使用哪一个,如何解释这些参数取决于应用。

当然,如果?&特殊的字符,那么必须有一种方法去规避,正如必须有一种方法将双引号字符放入由双引号分隔的字符串中一样URL的编码标准用%后面跟2个字节码的方式来表示特殊字符,用+来代替空格。所以为了在Google上搜索”grade=A+”,我们可以使用的URLhttp://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.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,它将在地址栏中显示为图标。
显示数值
让我们修改下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_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)
这个函数假设被允许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中的文件并不存在。
显示目录

pre.ctl { font-family: "Liberation Mono", monospace }
p { margin-bottom: 0.25cm; line-height: 120% }
a:link { }
code.ctl { font-family: "Liberation Mono", monospace }

原文地址:https://www.cnblogs.com/zhanghongfeng/p/10142932.html

时间: 2024-08-29 23:49:16

A Simple Web Server的相关文章

Simple Web API Server in Golang (2)

In this challenge, I tried to implement a simple OAuth2 server basing on Simple Web API Server in [1]. For OAuth2, go to http://oauth.net/2/. Endpoint /api/2/domains/{domain name}/oauth/access_token Use port 80. We would like to use other ports such

Simple Web API Server in Golang (1)

To be an better Gopher, get your hands dirty. Topcoder offered a serials of challenges for learning Golang. In this blog, I tried to implement "Go Learning Challenge - Simple Web-API Server"[1]. What's used in this challenge ? Following aspects

【转】推荐介绍几款小巧的Web Server程序

原博地址:http://blog.csdn.net/heiyeshuwu/article/details/1753900 偶然看到几个小巧有趣的Web Server程序,觉得有必要拿来分享一下,让大家除了知道Apache.IIS之外,原来还有更多有趣的选择.主要介绍了:micro_httpd.mini_httpd.thttpd.lighttpd.shttpd 等无款小巧好用的httpd. [ micro_httpd - really small HTTP server] 特点:* 支持安全的 .

Lua xavante WEB server实现xmlrpc服务器端

xavante xavante是一个使用lua实现的遵守http1.1的web server,支持wsapi. 依赖库: xavante核心 -- lua, copas(纯lua编写,网络连接coroutine处理), luasocket处理网络连接. xavante file handler -- luaFileSystem 此项目属于kepler项目的一个子项目,见官网地址: http://keplerproject.github.io/xavante/manual.html github上

深入理解Web Server原理----在CC3200 WiFi模块上构建轻量级Web Server

作为博客园的处女作,本文将引导大家理解Web Server的原理. Table of contents 常见Web Server及其功能 低功耗WiFi 嵌入式Web Server的应用 Energia Project无缝支持Arduino框架在TI LaunchPad上的扩展 基于CC3200如何构建一个嵌入式Web Server 做过Web开发的同学都知道,Web Server是用来处理http(POST.GET.PUT.DELETE等)请求的系统,有大名鼎鼎的Apache http Ser

Configure mutiple IBM HTTP Server / Other Apache based WEB server on 1 physical server (Section 1)

It's very simple to configure a IBM HTTP Server / Apache service on a server. But sometimes, considering the cost or any other reason, we need to share a physical server and configure 2 or more Web services. How to do it? Here I list out the major st

Configure mutiple IBM HTTP Server / Other Apache based WEB server on 1 physical server (Section 2)

Continue from the last article...... 2) Confirmed the 80 port of the new added IP is not listened by any other services.   Why need to test this? This is to ensure the 80/443 port of the new created IP is not listened by any other application. Test m

Using OAuth 2.0 for Web Server Applications, verifying a user&#39;s Android in-app subscription

在写本文之前先说些题外话. 前段时间游戏急于在GoolePlay上线,明知道如果不加Auth2.0的校验是不安全的还是暂时略过了这一步,果然没几天就发现后台记录与玩家实际付费不太一致,怀疑有玩家盗刷游戏元宝等,并且真实的走过了GooglePlay的所有支付流程完成道具兑换,时间一长严重性可想而知.经过查阅大量google官方文档后把代码补上,并在这里记录下OAuth 2.0 的使用,Google提供了OAuth2.0的好几种使用用途,每种使用方法都有些不同,具体可以看下这篇博客.在这里只写OAu

Apache Web Server 本地推送命令

/*****openssl 系统命令    x509输入文件类型       -in 是参数输入文件   -inform本地文件     -out是参数  是生成文件  ***/ openssl    x509  -in  *.cer  -inform  der  -out *.pem openssl  pkcs12  -nocets  -out  *.pem  -in  *.p12 /*****   >是 表示连接   ****/ cat  *.pem  *.pem  >  *.pem 在终