本文将介绍WSGI协议的原理,并亲自实现一个符合协议规范的WSGI server
WSGI,是Python Web Server Gateway Interface的简称,是web底层跟application解耦的协议,我们的web服务器使用WSGI协议来调用application称为WSGI server。为了各类web server和web application之间能够相互兼容,常见的web server(如Nginx,Apache)无法与web application(如Flask,django、tornado)直接通信,需要WSGI server作为桥梁。如今WSGI已经成为Python的一种标准协议PEP333。
WSGI的工作原理
WSGI的工作原理分为服务器层和应用程序层:
- 服务器层:将来自socket的数据包解析为http,调用application,给application提供环境信息environ,这个environ包含wsgi自身的信息(host,post,进程模式等),还有client的header和body信息。同时还给application提供一个start_response的回调函数,这个回调函数主要在应用程序层进行响应信息处理。
- 应用程序层:在WSGI提供的start_response,生成header,body和status后将这些信息socket send返回给客户端。
上面的工作原理通过流程图可以表示如下:
可以看到,wsgi server已经完成了底层http的解析和数据转发等一系列网络底层的实现,开发者可以更加专注于开发web application。
使用wsgiref模块理解WSGI工作流程
Python自带模块wsgiref
可以实现上述WSGI工作流程,这里使用wsgiref实现一个wsgi server的例子,以对wsgi的流程有感性的认识。
from wsgiref.simple_server import make_server def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return [b'<h1>Hello, web!</h1>'] if __name__ == '__main__': httpd = make_server('', 9999, application) print("Serving HTTP on port 9999...") httpd.serve_forever()
该例子中application就是web application,这里是定义为一个函数,在实际的Flask和bottle中,就是一个类,这些类实现__call__
方法,且该方法中带参数environ
和start_response
即可,运行server时只需调用self
即可,例如:
class Flask: def __init__(self): # init params pass def __call__(self, environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return [b'<h1>Hello, web!</h1>'] def listen(self, port) from wsgiref.simple_server import make_server server = make_server('127.0.0.1', 9999, self) print('serve on 127.0.0.1:9999') server.serve_forever()
因此,在这里例子里,可以具体看到wsgi server调用application后,在start_response中application返回header
、content-type
和body
,最后在浏览器打开127.0.0.1:9999即可看到由wigs server返回的内容。
自己实现一个wsgi server
了解了wsgi的工作原理和具体的使用例子,我们自己来实现一个wsgi服务器,可以进行请求解析,并可以调用Flask暴露出来的wsgi_app参数来将Flask运行起来。
首先组织代码结构:
import socket import StringIO import sys class WSGIserver(object): def __init__(self): pass def serve_forever(self): pass def handle_one_request(self): request_data = '' # 暂不处理 self.parse_request(request_data) env = self.get_environ() result = self.application(env, self.start_response) self.finish_response(result) def parse_request(self, data): pass def get_environ(self): pass def start_response(self, status, response_headers, exc_info=None): pass def finish_response(self, result): pass def make_server(server_address, application): server = WSGIServer(server_address) server.set_app(application) return server if __name__ == '__main__': httpd = make_server('', 8888, application) httpd.serve_forever()
分析如下:
__init__
:wsgi server的初始化操作serve_forever
:可以让wsgi server一直监听客户端请求handle_one_request
:对每一次请求进行参数解析,包括parse_request
和get_environ
start_response
:传递给application的回调函数,根据PEP333,start_response
需要包含status
,response_headers
,exc_info
三个参数。status
是http的状态码,如“200 OK”,”404 Not Found”。response_headers
是一个(header_name, header_value)
元组的列表,在进行application开发时需要注意这点。exc_info
通常不需要,直接设置为None即可。具体的一些参数的解释可以参考协议的详细解释。finish_response
:解析一次请求后,需要关闭socket端口,同时将application返回的data返回至客户端。
有了代码结构,根据PEP333标准协议,那么具体可以完善上述代码,如下(wsgi.py):
#!/usr/bin/env python # -*- coding: utf-8 -*- import socket import StringIO import sys class WSGIServer(object): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM request_queue_size = 1 def __init__(self, server_address): # 创建socket,利用socket获取客户端的请求 self.listen_socket = listen_socket = socket.socket(self.address_family, self.socket_type) # 设置socket的工作模式 listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 绑定socket地址 listen_socket.bind(server_address) # socket active, 监听文件描述符 listen_socket.listen(self.request_queue_size) # 获得serve的host name和port host, port = self.listen_socket.getsockname()[:2] self.server_name = socket.getfqdn(host) self.server_port = port self.headers_set = [] def set_app(self, application): self.application = application #启动WSGI server服务,不停的监听并获取socket数据。 def serve_forever(self): listen_socket = self.listen_socket while True: self.client_connection, client_address = listen_socket.accept() #接受客户端请求 #处理请求 self.handle_one_request() def handle_one_request(self): self.request_data = request_data = self.client_connection.recv(1024) self.parse_request(request_data) # print(''.join( # '< {line} '.format(line=line) # for line in request_data.splitlines() # )) # Construct environment dictionary using request data env = self.get_environ() #给flask ornado传递两个参数,environ,start_response result = self.application(env, self.start_response) self.finish_response(result) #处理socket的http协议 def parse_request(self, data): format_data = data.splitlines() if len(format_data): request_line = data.splitlines()[0] request_line = request_line.rstrip(' ') (self.request_method, self.path, self.request_version) = request_line.split() ## ['GET', '/', 'HTTP/1.1'] # 获取environ数据并设置当前server的工作模式 def get_environ(self): env = {} env['wsgi.version'] = (1, 0) env['wsgi.url_scheme'] = 'http' env['wsgi.input'] = StringIO.StringIO(self.request_data) env['wsgi.errors'] = sys.stderr env['wsgi.multithread'] = False env['wsgi.multiprocess'] = False env['wsgi.run_once'] = False # Required CGI variables env['REQUEST_METHOD'] = self.request_method # GET env['PATH_INFO'] = self.path # /hello env['SERVER_NAME'] = self.server_name # localhost env['SERVER_PORT'] = str(self.server_port) # 8888 return env def start_response(self, status, response_headers, exc_info=None): server_headers = [('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'), ('Server', 'WSGIServer 0.2')] self.headers_set = [status, response_headers + server_headers] #把application返回给WSGI的数据返回给客户端。 def finish_response(self, result): try: status, response_headers = self.headers_set response = 'HTTP/1.1 {status} '.format(status=status) for header in response_headers: response += '{0}: {1} '.format(*header) response += ' ' for data in result: response += data self.client_connection.sendall(response) print(''.join( '> {line} '.format(line=line) for line in response.splitlines() )) finally: self.client_connection.close() SERVER_ADDRESS = (HOST, PORT) = '', 8888 def make_server(server_address, application): server = WSGIServer(server_address) server.set_app(application) return server if __name__ == '__main__': if len(sys.argv) < 2: sys.exit('Provide a WSGI application object as module:callable') app_path = sys.argv[1] module, application = app_path.split(':') # 第一个参数是文件名,第二个参数时长文件内app的命名 module = __import__(module) application = getattr(module, application) # getattr(object, name[, default]) -> value httpd = make_server(SERVER_ADDRESS, application) print('WSGIServer: Serving HTTP on port {port} ... '.format(port=PORT)) httpd.serve_forever()
至此,wsgi server的代码已完成,下面写一个简单的Flask实例(flaskapp.py)来验证这个server是否可用:
from flask import Flask from flask import Response flask_app = Flask(__name__) @flask_app.route('/hello') def hello_world(): return Response( 'hello', mimetype='text/plain' ) app = flask_app.wsgi_app # 调用Flask暴露的wsgi_app,提供给wsgi server作为application,而不是直接run(port)
在终端输入:
$python wsgi.py flaskapp:app
WSGIServer: Serving HTTP on port 8888 ...
打开浏览器,输入http://localhost:8888/hello
可看到hello的信息,同时,终端输出一些request和response的信息:
> HTTP/1.1 200 OK > Content-Type: text/plain; charset=utf-8 > Content-Length: 5 > Date: Tue, 31 Mar 2015 12:54:48 GMT > Server: WSGIServer 0.2 > > hello
总结
本文主要从WSGI的原理入手,同时通过原生wsgiref模块了解了WSGI的工作流程,最后利用socket模块自己实现了一个WSGI server,我们可以通过Flask这类web框架暴露出来的WSGI接口启动应用。
由于WSGI实现了web server和web application的高度解耦,在具体的使用中可以使用一些比较成熟的WSGI server如uwsgi和gunicorn对任何一个符合WSGI规范的app进行部署,这给Python Web应用的部署带来了极大的灵活性。