下拉刷新
OneFile
源码
web-server一个简单的 Web 框架
作者 削微寒·主语言 Python·无依赖·1.3w 次查看
访问复制
#!/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()