13.6. http.server — 实现 Web 服务器的基础类

目标:利用 http.server 中的一些类可以实现一个基本的网络服务器

http.server 使用 socketserver 中的一些类来创建用于实现 HTTP 服务器的基类。HTTPServer 可以直接拿来用,而 BaseHTTPRequestHandler 的目的则是提供一个可供扩展的基础,以便处理各项协议 ( GET,POST 等)。

HTTP GET

在处理请求的类中,要添加对 HTTP 方法的支持,就需要实现 do_METHOD() 方法,且将 METHOD 替换成 HTTP 方法的名字。(比如  do_GET()do_POST() 等)。为了保持一致,处理请求的方法一律没有参数。请求的所有参数由 BaseHTTPRequestHandler来解析,并且作为一个对象保存在一个请求对象的属性中。

下面这个处理请求的例子展示了如何向客户返回一个答复,其中一些本地属性可以被用来构建回复。

http_server_GET.py

from http.server import BaseHTTPRequestHandler
from urllib import parse

class GetHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        parsed_path = parse.urlparse(self.path)
        message_parts = [
            'CLIENT VALUES:',
            'client_address={} ({})'.format(
                self.client_address,
                self.address_string()),
            'command={}'.format(self.command),
            'path={}'.format(self.path),
            'real path={}'.format(parsed_path.path),
            'query={}'.format(parsed_path.query),
            'request_version={}'.format(self.request_version),
            '',
            'SERVER VALUES:',
            'server_version={}'.format(self.server_version),
            'sys_version={}'.format(self.sys_version),
            'protocol_version={}'.format(self.protocol_version),
            '',
            'HEADERS RECEIVED:',
        ]
        for name, value in sorted(self.headers.items()):
            message_parts.append(
                '{}={}'.format(name, value.rstrip())
            )
        message_parts.append('')
        message = '\r\n'.join(message_parts)
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()
        self.wfile.write(message.encode('utf-8'))

if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), GetHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

所有文本信息先被组装起来再被写到 wfile 中,文件处理器则将回复包装到 socket 里。每个回复都需要一个回复代码,由 send_response() 设定。如果使用了一个错误代码( 404,501 等),一个合适的默认错误信息应该包含在头部信息中,或者包含在某个可以传递错误代码的信息中。

要运行一个服务器的请求处理器,需要将它传给 HTTPServer 构建函数,就如 __main__ 部分脚本所示处理。

然后开启服务器:

$ python3 http_server_GET.py

Starting server, use <Ctrl-C> to stop

再另开一个终端,用 curl 来访问它:

$ curl -v -i http://127.0.0.1:8080/?foo=bar

*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /?foo=bar HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf-8
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 20:44:11 GMT

CLIENT VALUES:
client_address=('127.0.0.1', 52934) (127.0.0.1)
command=GET
path=/?foo=bar
real path=/
query=foo=bar
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept=*/*
Host=127.0.0.1:8080
User-Agent=curl/7.43.0
* Connection #0 to host 127.0.0.1 left intact

注意

由不同版本的 curl 输出可能不好。如果运行例子产生不同的输出,就检查一下 curl 的版本号。

HTTP POST

要支持 POST 请求需要更多一点的工作,因为提供的基类不能自动分析表单数据。不过,如果给定的输入是正确的,那么 cgi 模块提供的 FieldStorage 类却可以用来分析表单。

http_server_POST.py

import cgi
from http.server import BaseHTTPRequestHandler
import io

class PostHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        # 分析提交的表单数据
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={
                'REQUEST_METHOD': 'POST',
                'CONTENT_TYPE': self.headers['Content-Type'],
            }
        )

        # 开始回复
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()

        out = io.TextIOWrapper(
            self.wfile,
            encoding='utf-8',
            line_buffering=False,
            write_through=True,
        )

        out.write('Client: {}\n'.format(self.client_address))
        out.write('User-agent: {}\n'.format(
            self.headers['user-agent']))
        out.write('Path: {}\n'.format(self.path))
        out.write('Form data:\n')

        # 表单信息内容回放
        for field in form.keys():
            field_item = form[field]
            if field_item.filename:
                # 字段中包含的是一个上传文件
                file_data = field_item.file.read()
                file_len = len(file_data)
                del file_data
                out.write(
                    '\tUploaded {} as {!r} ({} bytes)\n'.format(
                        field, field_item.filename, file_len)
                )
            else:
                # 通常形式的值
                out.write('\t{}={}\n'.format(
                    field, form[field].value))

        # 将编码 wrapper 到底层缓冲的连接断开, 
        # 使得将 wrapper 删除时, 
        # 并不关闭仍被服务器使用 socket 。
        out.detach()

if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), PostHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

在一个窗口运行服务器

$ python3 http_server_POST.py

Starting server, use <Ctrl-C> to stop

使用 -F 选项, curl 的参数可以包含要提交给服务器的表单数据。最后一个参数 -Fdatafile=@http_server_GET.py ,将文件 http_server_GET.py 的内容用表单提交,展示了如何利用表单来读取一个文件数据。

$ curl -v http://127.0.0.1:8080/ -F name=dhellmann -F foo=bar\
-F datafile=@http_server_GET.py

*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Length: 1974
> Expect: 100-continue
> Content-Type: multipart/form-data;
boundary=------------------------a2b3c7485cf8def2
>
* Done waiting for 100-continue
HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf-8
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 20:53:48 GMT

Client: ('127.0.0.1', 53121)
User-agent: curl/7.43.0
Path: /
Form data:
    name=dhellmann
    Uploaded datafile as 'http_server_GET.py' (1612 bytes)
    foo=bar
* Connection #0 to host 127.0.0.1 left intact

Threading 和 Forking

HTTPServer 是 socketserver.TCPServer 的一个简单自子类,它并不使用多线程或多进程来处理请求。要添加 threading 或 forking ,需要从  socketserver 中使用一个合适的 mix-in 来创建一个新的类。

http_server_threads.py

from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading

class Handler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()
        message = threading.currentThread().getName()
        self.wfile.write(message.encode('utf-8'))
        self.wfile.write(b'\n')

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """在一个新的线程中处理请求。"""

if __name__ == '__main__':
    server = ThreadedHTTPServer(('localhost', 8080), Handler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

和其他例子一样,以同样的方式运行服务器

$ python3 http_server_threads.py

Starting server, use <Ctrl-C> to stop

每当服务器接收一个请求,它就创建一个新的线程或进程来处理它:

$ curl http://127.0.0.1:8080/

Thread-1

$ curl http://127.0.0.1:8080/

Thread-2

$ curl http://127.0.0.1:8080/

Thread-3

用 ForkingMixIn 替换 ThreadingMixIn 可以达到类似的效果,只不过这时创建的是一个新的进程,而不是线程。

处理错误

传递一个合适的错误代码以及可选的错误信息,调用 send_error()来处理错误,将自动生成整个回复(包括头部,状态代码和信息体)。

http_server_errors.py

from http.server import BaseHTTPRequestHandler

class ErrorHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_error(404)

if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), ErrorHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

在这个例子中,总是返回一个 404 错误。

$ python3 http_server_errors.py

Starting server, use <Ctrl-C> to stop

错误发生时,返回信息在头部指明错误代码,并回传一个 HTML 文件将该错误报告给客户。

$ curl -i http://127.0.0.1:8080/

HTTP/1.0 404 Not Found
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 20:58:08 GMT
Connection: close
Content-Type: text/html;charset=utf-8
Content-Length: 447

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type"
        content="text/html;charset=utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 404</p>
        <p>Message: Not Found.</p>
        <p>Error code explanation: 404 - Nothing matches the
        given URI.</p>
    </body>
</html>

设定头部

使用 send_header 方法可添加头数据到 HTTP 回复中。该方法需要两个参数:头的名称和相应的值。

http_server_send_header.py

from http.server import BaseHTTPRequestHandler
import time

class GetHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header(
            'Content-Type',
            'text/plain; charset=utf-8',
        )
        self.send_header(
            'Last-Modified',
            self.date_time_string(time.time())
        )
        self.end_headers()
        self.wfile.write('Response body\n'.encode('utf-8'))

if __name__ == '__main__':
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), GetHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

在这个例子中,我们用当前的时间戳来给头 Last-Modified 赋值,并将其格式化为符合 RFC 7231 的形式。

$ curl -i http://127.0.0.1:8080/

HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 21:00:54 GMT
Content-Type: text/plain; charset=utf-8
Last-Modified: Thu, 06 Oct 2016 21:00:54 GMT

Response body

如同其他例子一样,服务器在终端记录请求。

$ python3 http_server_send_header.py

Starting server, use <Ctrl-C> to stop
127.0.0.1 - - [06/Oct/2016 17:00:54] "GET / HTTP/1.1" 200 -

使用命令行

http.server 內建有一个用于服务本地文件系统文件的服务器。 使用 Python 解释器的  -m  选项可以从命令行运行它。

$ python3 -m http.server 8080

Serving HTTP on 0.0.0.0 port 8080 ...
127.0.0.1 - - [06/Oct/2016 17:12:48] "HEAD /index.rst HTTP/1.1" 200 -

服务器的根目录即当前运行服务器的工作目录。

$ curl -I http://127.0.0.1:8080/index.rst

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 21:12:48 GMT
Content-type: application/octet-stream
Content-Length: 8285
Last-Modified: Thu, 06 Oct 2016 21:12:10 GMT

参考

  • 标准库 http.server 文档
  • socketserver -- socketserver 模块提供了处理原始未加工的 socket 连接的基类。
  • RFC 7231 -- "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content" 含有一份关于 HTTP 头和日期时间格式的说明。

本文章首发在 LearnKu.com 网站上。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~