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 头和日期时间格式的说明。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。