前几天写了 浅谈cgi、wsgi、uwsgi 与 uWSGI 等一些 python web 开发中遇到的一些名词的理解,今天博主就根据 wsgi 标准实现一个 web server,并尝试用它来跑 Django、tornado 框架的 app。python
在实现 wsgi server 以前咱们先要作一些准备工做。首先,http server 使用 http 协议,而 http 协议封装在 tcp 协议中,因此要创建一个 http server 咱们先要创建一个 tcp server。要使用 tcp 协议咱们不可能本身实现一个,如今比较流行的解决方案就是使用 socket 套接字编程, socket 已经帮咱们实现了 tcp 协议的细节,咱们能够直接拿来使用不用关心细节。 socket 编程是语言无关的,不论是之前博主用 MFC 写聊天室仍是用 C# 写网络延迟计算仍是如今写 http server,它的使用流程都是同样的:git
初始化 socket;github
绑定套接字到端口(bind);web
监听端口(listen);django
接受链接请求(accept);编程
通讯(send/recv);后端
关闭链接(close);浏览器
初始化 socket;服务器
发出链接请求(connect);cookie
通讯(send/recv);
关闭链接(close);
server 的具体实现:
# coding: utf-8 # server.py import socket HOST, PORT = '', 8888 # 初始化 listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 绑定 listen_socket.bind((HOST, PORT)) # 监听 listen_socket.listen(1) print 'Serving HTTP on port %s ...' % PORT while True: # 接受请求 client_connection, client_address = listen_socket.accept() # 通讯 request = client_connection.recv(1024) print request http_response = """ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(http_response) # 关闭链接 client_connection.close()
而 client 不须要咱们本身实现,咱们的浏览器就是一个 client ,如今运行python server.py
,而后在浏览器中打开 localhost:8888
便可看到浏览器中显示 hello world!
,这么快就实现了一个 http server 有木有 hin 激动!
然而想要 Django 这类框架的 app 在咱们写的 http server 中运行起来还远远不够,如今咱们就须要引入 wsgi 规范,根据这个规范咱们就可让本身的 server 也能运行这些框架的 app啦。
首先,咱们要看官方文档里 wsgi 的解释:PEP 3333
嗯,就是一篇很长的英语阅读理解,大概意思就是若是你想让你的服务器和应用程序一块儿好好工做,你要遵循这个标准来写你的 web app 和 web server:
server--middleware--application
application 是一个接受接受两个参数environ, start_response
的标准 wsgi app:
environ: 一个包含请求信息及环境信息的字典,server 端会详细说明 start_response: 一个接受两个参数`status, response_headers`的方法: status: 返回状态码,如http 200、404等 response_headers: 返回信息头部列表
具体实现:
def application(environ, start_response): status = '200 OK' response_headers = [('Content-Type', 'text/plain')] start_response(status, response_headers) return ['Hello world']
这样一个标准的 wsgi app 就写好了,虽然这看上去和咱们写的 Django app、 tornado app 截然不同,但实际上这些 app 都会通过相应的处理来适配 wsgi 标准,这个以后会详谈。
wsgi server 的实现要复杂一些,因此我先贴本身实现的 wsgi server 代码,而后再讲解:
# server.py # coding: utf-8 from __future__ import unicode_literals import socket import StringIO import sys import datetime class WSGIServer(object): socket_family = socket.AF_INET socket_type = socket.SOCK_STREAM request_queue_size = 10 def __init__(self, address): self.socket = socket.socket(self.socket_family, self.socket_type) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(address) self.socket.listen(self.request_queue_size) host, port = self.socket.getsockname()[:2] self.host = host self.port = port def set_application(self, application): self.application = application def serve_forever(self): while 1: self.connection, client_address = self.socket.accept() self.handle_request() def handle_request(self): self.request_data = self.connection.recv(1024) self.request_lines = self.request_data.splitlines() try: self.get_url_parameter() env = self.get_environ() app_data = self.application(env, self.start_response) self.finish_response(app_data) print '[{0}] "{1}" {2}'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), self.request_lines[0], self.status) except Exception, e: pass def get_url_parameter(self): self.request_dict = {'Path': self.request_lines[0]} for itm in self.request_lines[1:]: if ':' in itm: self.request_dict[itm.split(':')[0]] = itm.split(':')[1] self.request_method, self.path, self.request_version = self.request_dict.get('Path').split() def get_environ(self): env = { 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': StringIO.StringIO(self.request_data), 'wsgi.errors': sys.stderr, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'REQUEST_METHOD': self.request_method, 'PATH_INFO': self.path, 'SERVER_NAME': self.host, 'SERVER_PORT': self.port, 'USER_AGENT': self.request_dict.get('User-Agent') } return env def start_response(self, status, response_headers): headers = [ ('Date', datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT')), ('Server', 'RAPOWSGI0.1'), ] self.headers = response_headers + headers self.status = status def finish_response(self, app_data): try: response = 'HTTP/1.1 {status}\r\n'.format(status=self.status) for header in self.headers: response += '{0}: {1}\r\n'.format(*header) response += '\r\n' for data in app_data: response += data self.connection.sendall(response) finally: self.connection.close() if __name__ == '__main__': port = 8888 if len(sys.argv) < 2: sys.exit('请提供可用的wsgi应用程序, 格式为: 模块名.应用名 端口号') elif len(sys.argv) > 2: port = sys.argv[2] def generate_server(address, application): server = WSGIServer(address) server.set_application(TestMiddle(application)) return server app_path = sys.argv[1] module, application = app_path.split('.') module = __import__(module) application = getattr(module, application) httpd = generate_server(('', int(port)), application) print 'RAPOWSGI Server Serving HTTP service on port {0}'.format(port) print '{0}'.format(datetime.datetime.now(). strftime('%a, %d %b %Y %H:%M:%S GMT')) httpd.serve_forever()
首先咱们看 WSGIServer 类__init__
方法主要是初始化 socket 与服务器地址,绑定并监听端口;
其次,serve_forever(self):
持续运行 server;handle_request(self):
处理请求;
最后,finish_response(self, app_data):
返回请求响应。
再来看__main__
里是如何运行 WSGIServer的:
得到地址和端口后先初始化 WSGIServer:server = WSGIServer(address)
,而后设置加载的wsgi app:server.set_application(TestMiddle(application))
,接着持续运行 server:httpd.serve_forever()
那么根据以上信息,能够总结出 wsgi server 应该是这样一个过程:
初始化,创建套接字,绑定监听端口;
设置加载的 web app;
开始持续运行 server;
处理访问请求(在这里能够加入你本身的处理过程,好比我加入了打印访问信息,字典化访问头部信息等功能);
获取请求信息及环境信息(get_environ(self)
);
用environ
运行加载的 web app 获得返回信息;
构造返回信息头部;
返回信息;
只要实现了以上过程,一个标准的 wsgi server 就写好了。仔细观察,其实一个 wsgi server 的重要之处就在于用environ
去跑 web app 获得返回结果这一步,这一步和前面的 application 实现相辅相成,而后框架和服务器都根据这套标准,你们就能够愉快的一块儿工做了。
如今运行python server.py app.app 8000
, 而后浏览器访问localhost:8000
:
后端
浏览器
到此,咱们的 wsgi server 已经能够正常运行了,这时咱们再来看看 middleware:
middleware 中间件的做用就是在server 拿到请求数据给 application 前若是想作一些处理或者验证等等功能,这时候 middleware 就派上用场了,固然你愿意的话也能够写在你的 server 里,只是 wsgi 规范更建议把这些写在中间件里,下面我来实现一个检查请求'User-Agent'是否为正常浏览器,不是就把请求拒绝掉的中间件:
# coding: utf-8 # middleware.py from __future__ import unicode_literals class TestMiddle(object): def __init__(self, application): self.application = application def __call__(self, environ, start_response): if 'postman' in environ.get('USER_AGENT'): start_response('403 Not Allowed', []) return ['not allowed!'] return self.application(environ, start_response)
初始化用来接收 application,而后在__call__
方法里写入处理过程,最后返回 application 这样咱们的中间件就能像函数同样被调用了。
而后引入中间件:
from middleware import TestMiddle ... server.set_application(TestMiddle(application))
如今重启 server 而后用 postman 访问服务器:
能够看到,中间件起做用了!
接下来,咱们再谈谈 Django 和 tornado 对于 wsgi 的支持:
django 自己的应用体系比较复杂,因此没有办法直接拿来用在咱们写的 wsgi server 上,不过 Django 考虑到了这一点, 因此提供了 WSGIHandler:
class WSGIHandler(base.BaseHandler): request_class = WSGIRequest def __init__(self, *args, **kwargs): super(WSGIHandler, self).__init__(*args, **kwargs) self.load_middleware() def __call__(self, environ, start_response): set_script_prefix(get_script_name(environ)) signals.request_started.send(sender=self.__class__, environ=environ) try: request = self.request_class(environ) except UnicodeDecodeError: logger.warning( 'Bad Request (UnicodeDecodeError)', exc_info=sys.exc_info(), extra={ 'status_code': 400, } ) response = http.HttpResponseBadRequest() else: response = self.get_response(request) response._handler_class = self.__class__ status = '%d %s' % (response.status_code, response.reason_phrase) response_headers = [(str(k), str(v)) for k, v in response.items()] for c in response.cookies.values(): response_headers.append((str('Set-Cookie'), str(c.output(header='')))) start_response(force_str(status), response_headers) if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'): response = environ['wsgi.file_wrapper'](response.file_to_stream) return response
能够看到,这里 WSGIHandler 同样使用start_response(force_str(status), response_headers)
把 Django app 封装成了 标准 wsgi app ,而后返回 response。
Django 一样也实现了 wsgi server:
class WSGIServer(simple_server.WSGIServer, object): """BaseHTTPServer that implements the Python WSGI protocol""" request_queue_size = 10 def __init__(self, *args, **kwargs): if kwargs.pop('ipv6', False): self.address_family = socket.AF_INET6 self.allow_reuse_address = kwargs.pop('allow_reuse_address', True) super(WSGIServer, self).__init__(*args, **kwargs) def server_bind(self): """Override server_bind to store the server name.""" super(WSGIServer, self).server_bind() self.setup_environ() def handle_error(self, request, client_address): if is_broken_pipe_error(): logger.info("- Broken pipe from %s\n", client_address) else: super(WSGIServer, self).handle_error(request, client_address)
基本所有继承于wsgiref.simple_server.WSGIServer
:
class WSGIServer(HTTPServer): """BaseHTTPServer that implements the Python WSGI protocol""" application = None def server_bind(self): """Override server_bind to store the server name.""" HTTPServer.server_bind(self) self.setup_environ() def setup_environ(self): # Set up base environment env = self.base_environ = {} env['SERVER_NAME'] = self.server_name env['GATEWAY_INTERFACE'] = 'CGI/1.1' env['SERVER_PORT'] = str(self.server_port) env['REMOTE_HOST']='' env['CONTENT_LENGTH']='' env['SCRIPT_NAME'] = '' def get_app(self): return self.application def set_app(self,application): self.application = application
能够看到,和咱们实现的 wsgi server 是差很少的。
tornado 直接从底层用 epoll 本身实现了 事件池操做、tcp server、http server,因此它是一个彻底不一样当异步框架,但 tornado 一样也提供了对 wsgi 对支持,不过这种状况下就没办法用 tornado 异步的特性了。
与其说 tornado 提供了 wsgi 支持,不如说它只是提供了 wsgi 兼容,tornado 提供两种方式:
其余应用要在 tornado server 运行, tornado 提供 WSGIContainer。
今天这里主要讨论 wsgi ,因此这里就不分析 tornado 这部分代码,以后作 tornado 源码分析会再分析这里。
tornado 应用要在 wsgi server 上运行, tornado 提供 WSGIAdapter:
class WSGIAdapter(object): def __init__(self, application): if isinstance(application, WSGIApplication): self.application = lambda request: web.Application.__call__( application, request) else: self.application = application def __call__(self, environ, start_response): method = environ["REQUEST_METHOD"] uri = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", ""))) uri += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) if environ.get("QUERY_STRING"): uri += "?" + environ["QUERY_STRING"] headers = httputil.HTTPHeaders() if environ.get("CONTENT_TYPE"): headers["Content-Type"] = environ["CONTENT_TYPE"] if environ.get("CONTENT_LENGTH"): headers["Content-Length"] = environ["CONTENT_LENGTH"] for key in environ: if key.startswith("HTTP_"): headers[key[5:].replace("_", "-")] = environ[key] if headers.get("Content-Length"): body = environ["wsgi.input"].read( int(headers["Content-Length"])) else: body = b"" protocol = environ["wsgi.url_scheme"] remote_ip = environ.get("REMOTE_ADDR", "") if environ.get("HTTP_HOST"): host = environ["HTTP_HOST"] else: host = environ["SERVER_NAME"] connection = _WSGIConnection(method, start_response, _WSGIRequestContext(remote_ip, protocol)) request = httputil.HTTPServerRequest( method, uri, "HTTP/1.1", headers=headers, body=body, host=host, connection=connection) request._parse_body() self.application(request) if connection._error: raise connection._error if not connection._finished: raise Exception("request did not finish synchronously") return connection._write_buffer
能够看到 tornado 也是将本身的应用使用前文那个流程改成标准 wsgi app,最后咱们来试试让咱们本身的服务器运行 tornado app:
# coding: utf-8 # tornado_wsgi.py from __future__ import unicode_literals import datetime import tornado.web import tornado.wsgi from middleware import TestMiddle from server import WSGIServer class MainHandler(tornado.web.RequestHandler): def get(self): self.write("this is a tornado wsgi application") if __name__ == "__main__": application = tornado.web.Application([ (r"/", MainHandler), ]) wsgi_app = tornado.wsgi.WSGIAdapter(application) server = WSGIServer(('', 9090)) server.set_application(TestMiddle(wsgi_app)) print 'RAPOWSGI Server Serving HTTP service on port {0}'.format(9090) print '{0}'.format(datetime.datetime.now(). strftime('%a, %d %b %Y %H:%M:%S GMT')) server.serve_forever()
运行:python tornado_wsgi.py
,打开浏览器:localhost:9090
,完美运行,中间件也运行正常:
文中代码源码:simple_wsgi_server
参考资料:Let’s Build A Web Server
做者:rapospectre