web-server:一个简单的 Web 框架
OneFile
源码
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# Author : XueWeiHan
# E-mail : 595666367@qq.com
# Date : 2022-03-29 16:56
# Desc : 迷你 Web 服务器
import sys
import socket
import selectors
import datetime
import time
import html
# 默认错误信息 HTML 模版
DEFAULT_ERROR_MESSAGE = """\
<!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: %(code)d</p>
<p>Message: %(message)s.</p>
<p>Error code explanation: %(code)s - %(explain)s.</p>
</body>
</html>
"""
# 处理连接进行数据通信
class HTTPServer(object):
def __init__(self, server_address, RequestHandlerClass):
self.server_address = server_address
self.RequestHandlerClass = RequestHandlerClass
self.request_queue_size = 5
self.__shutdown_request = False
# 创建 TCP Socket
self.socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
try:
# 绑定 socket 和端口
self.server_bind()
# 开始监听端口
self.server_activate()
except:
# 关闭 socket
self.server_close()
raise
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定端口
self.socket.bind(self.server_address)
def server_activate(self):
# 监听端口
self.socket.listen(self.request_queue_size)
def server_close(self):
# 关闭socket
self.socket.close()
def fileno(self):
"""
返回 socket 文件号
用于 select 监控文件句柄状态
"""
return self.socket.fileno()
def serve_forever(self, poll_interval=0.5):
"""
服务器启动入口
"""
with selectors.SelectSelector() as selector:
# 基于 select 轮询获取已准备好的可读句柄
selector.register(self, selectors.EVENT_READ)
while True:
ready = selector.select(poll_interval)
if ready:
# 有准备好的可读文件句柄,则与客户端的链接建立完毕
# 可以进行下一步
self._handle_request_noblock()
def _handle_request_noblock(self):
"""
处理请求
"""
try:
# 接收来自客户端的请求:request 对象
request, client_address = self.get_request()
except socket.error:
return
try:
# 开始处理请求
self.process_request(request, client_address)
except:
self.handle_error(client_address)
self.shutdown_request(request)
def get_request(self):
# 接收请求
return self.socket.accept()
def process_request(self, request, client_address):
# 处理请求
self.finish_request(request, client_address)
self.shutdown_request(request)
def finish_request(self, request, client_address):
# 调用处理请求的类
self.RequestHandlerClass(request, client_address, self)
def shutdown_request(self, request):
try:
# 后续不允许发送和接收
request.shutdown(socket.SHUT_WR)
except socket.error:
pass # some platforms may raise ENOTCONN here
# 结束请求
request.close()
def handle_error(self, client_address):
print('-'*40, file=sys.stderr)
print('Exception occurred during processing of request from',
client_address, file=sys.stderr)
import traceback
traceback.print_exc()
print('-'*40, file=sys.stderr)
# 处理请求
class HTTPRequestHandler(object):
responses = {
100: ('Continue', 'Request received, please continue'),
101: ('Switching Protocols',
'Switching to new protocol; obey Upgrade header'),
200: ('OK', 'Request fulfilled, document follows'),
201: ('Created', 'Document created, URL follows'),
202: ('Accepted',
'Request accepted, processing continues off-line'),
203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
204: ('No Content', 'Request fulfilled, nothing follows'),
205: ('Reset Content', 'Clear input form for further input.'),
206: ('Partial Content', 'Partial content follows.'),
300: ('Multiple Choices',
'Object has several resources -- see URI list'),
301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
302: ('Found', 'Object moved temporarily -- see URI list'),
303: ('See Other', 'Object moved -- see Method and URL list'),
304: ('Not Modified',
'Document has not changed since given time'),
305: ('Use Proxy',
'You must use proxy specified in Location to access this '
'resource.'),
307: ('Temporary Redirect',
'Object moved temporarily -- see URI list'),
400: ('Bad Request',
'Bad request syntax or unsupported method'),
401: ('Unauthorized',
'No permission -- see authorization schemes'),
402: ('Payment Required',
'No payment -- see charging schemes'),
403: ('Forbidden',
'Request forbidden -- authorization will not help'),
404: ('Not Found', 'Nothing matches the given URI'),
405: ('Method Not Allowed',
'Specified method is invalid for this resource.'),
406: ('Not Acceptable', 'URI not available in preferred format.'),
407: ('Proxy Authentication Required', 'You must authenticate with '
'this proxy before proceeding.'),
408: ('Request Timeout', 'Request timed out; try again later.'),
409: ('Conflict', 'Request conflict.'),
410: ('Gone',
'URI no longer exists and has been permanently removed.'),
411: ('Length Required', 'Client must specify Content-Length.'),
412: ('Precondition Failed', 'Precondition in headers is false.'),
413: ('Request Entity Too Large', 'Entity is too large.'),
414: ('Request-URI Too Long', 'URI is too long.'),
415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
416: ('Requested Range Not Satisfiable',
'Cannot satisfy request range.'),
417: ('Expectation Failed',
'Expect condition could not be satisfied.'),
500: ('Internal Server Error', 'Server got itself in trouble'),
501: ('Not Implemented',
'Server does not support this operation'),
502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
503: ('Service Unavailable',
'The server cannot process the request due to a high load'),
504: ('Gateway Timeout',
'The gateway server did not receive a timely response'),
505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
}
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.protocol_version = "HTTP/1.0"
self.setup()
try:
self.handle()
finally:
self.finish()
def setup(self):
self.connection = self.request
# 初始化请求和响应的文件句柄
self.rfile = self.connection.makefile('rb', -1)
self.wfile = self.connection.makefile('wb', 0)
def handle(self):
try:
# 设置请求体限制:65536
self.raw_requestline = self.rfile.readline(65537)
# 如果超过限制则返回 414 HTTP code
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(414)
return
# 解析 HTTP 请求,并把值通过 self 属性传递
if not self.parse_request():
return
# 具体处理请求的方法 do_方法,比如:do_get、do_post
mname = ('do_' + self.command).lower()
if not hasattr(self, mname):
self.send_error(501, "Unsupported method (%r)" % self.command)
return
method = getattr(self, mname)
# 对应到具体的处理 HTTP method 的方法
method()
# 返回响应
self.wfile.flush()
except socket.timeout as e:
self.log_error("Request timed out: %r", e)
return
def finish(self):
if not self.wfile.closed:
try:
self.wfile.flush()
except socket.error:
pass
# 关闭请求和响应的句柄
self.wfile.close()
self.rfile.close()
def parse_request(self):
"""
解析 HTTP 请求
"""
self.command = None # set in case of error on the first line
self.request_version = version = "HTTP/1.0"
# 开始解析 HTTP 请求,数据格式如下:
"""
{HTTP method} {PATH} {HTTP version}\r\n
{header field name}:{field value}\r\n
...
\r\n
{request body}
"""
# 解析请求头
requestline = str(self.raw_requestline, 'iso-8859-1')
requestline = requestline.rstrip('\r\n')
self.requestline = requestline
words = requestline.split()
if len(words) == 3:
# HTTP method, PATH, HTTP version
command, path, version = words
if version[:5] != 'HTTP/':
self.send_error(400, "Bad request version (%r)" % version)
return False
# 检查 HTTP version 正确性
try:
base_version_number = version.split('/', 1)[1]
version_number = base_version_number.split(".")
if len(version_number) != 2:
raise ValueError
version_number = int(version_number[0]), int(version_number[1])
if version_number >= (2, 0):
self.send_error(
505, "Invalid HTTP Version (%s)" % base_version_number)
return False
except (ValueError, IndexError):
self.send_error(400, "Bad request version (%r)" % version)
return False
elif len(words) == 2:
# 如果没有 HTTP version 则使用默认版本即:HTTP/1.0
command, path = words
elif not words:
return False
else:
self.send_error(400, "Bad request syntax (%r)" % requestline)
return False
self.command, self.path, self.request_version = command, path, version
# 解析 header,仅做解析
self.headers = self.parse_headers()
return True
def parse_headers(self):
headers = {}
while True:
line = self.rfile.readline()
if line in (b'\r\n', b'\n', b''):
break
line_str = str(line, 'utf-8')
key, value = line_str.split(': ')
headers[key] = value.strip()
return headers
def log_message(self, format, *args):
log_data_time_string = datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")
sys.stderr.write("%s - - [%s] %s\n" %
(self.client_address[0],
log_data_time_string,
format%args))
def log_request(self, code='-', size='-'):
self.log_message('"%s" %s %s',
self.requestline, str(code), str(size))
def log_error(self, format, *args):
self.log_message(format, *args)
def send_error(self, code, message=None):
"""
返回异常响应 code+message
"""
try:
short, long = self.responses[code]
except KeyError:
short, long = '???', '???'
if message is None:
message = short
explain = long
self.log_error("code %d, message %s", code, message)
self.send_response(code, message)
content = None
# 状态码大于 200,并且不是 204、205、304 为异常
if code > 200 and code not in (204, 205, 304):
content = (DEFAULT_ERROR_MESSAGE % {
'code': code,
'message': html.escape(message),
'explain': explain
})
self.send_header("Content-Type", "text/html;charset=utf-8")
self.end_headers()
body = content.encode('UTF-8', 'replace')
if self.command != 'HEAD' and content:
# 返回响应
self.wfile.write(body)
def send_response(self, code, message=None):
self.log_request(code)
if message is None:
if code in self.responses:
message = self.responses[code][0]
else:
message = ''
# 响应体格式
"""
{HTTP version} {status code} {status phrase}\r\n
{header field name}:{field value}\r\n
...
\r\n
{response body}
"""
# 写响应头
self.wfile.write(("%s %d %s\r\n" %
(self.protocol_version, code, message)).encode(
'latin-1', 'strict'))
self.send_header('Server', "HG/Python " + sys.version.split()[0])
# 写响应 header
self.send_header('Date', self.date_time_string())
def send_header(self, keyword, value):
self.wfile.write(("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict'))
def end_headers(self):
# header 结束的标识符
self.wfile.write(b"\r\n")
@staticmethod
def date_time_string(timestamp=None):
weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
if timestamp is None:
timestamp = time.time()
year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
weekdayname[wd],
day, monthname[month],
year, hh, mm, ss)
return s
# 请求具体的处理方式
class RequestHandler(HTTPRequestHandler):
def handle_index(self):
page = '''
<html>
<body>
<p>你好, HG Web Server!</p>
</body>
</html>
'''
self.send_response(200) # status code
# 试试删除这行
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(page)))
self.end_headers()
self.wfile.write(page.encode('utf-8'))
def handle_favicon(self):
page = '''
<html>
<body>
<p>这里还未开发</p>
</body>
</html>
'''
self.send_response(200) # status code
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(page)))
self.end_headers()
self.wfile.write(page.encode('utf-8'))
# 处理 GET 请求
def do_get(self):
# 根据 path 对应到具体的处理方法
if self.path == '/':
self.handle_index()
elif self.path.startswith('/favicon'):
self.handle_favicon()
else:
self.send_error(404)
if __name__ == '__main__':
server = HTTPServer(('', 8080), RequestHandler)
# 启动服务
server.serve_forever()