V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
rapospectre
V2EX  ›  Python

自己写一个 wsgi 服务器运行 Django 、Tornado 应用

  •  
  •   rapospectre ·
    bluedazzle · 2016-06-02 19:26:54 +08:00 · 4241 次点击
    这是一个创建于 3129 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前几天写了 浅谈 cgi 、 wsgi 、 uwsgi 与 uWSGI 等一些 python web 开发中遇到的一些名词的理解,今天就根据 wsgi 标准实现一个 web server ,并尝试用它来跑 Django 、 tornado 框架的 app 。

    编写一个简单的 http server

    在实现 wsgi server 之前我们先要做一些准备工作。首先, http server 使用 http 协议,而 http 协议封装在 tcp 协议中,所以要建立一个 http server 我们先要建立一个 tcp server 。要使用 tcp 协议我们不可能自己实现一个,现在比较流行的解决方案就是使用 socket 套接字编程, socket 已经帮我们实现了 tcp 协议的细节,我们可以直接拿来使用不用关心细节。 socket 编程是语言无关的,不管是以前博主用 MFC 写聊天室还是用 C# 写网络延迟计算还是现在写 http server ,它的使用流程都是一样的:

    server

    1. 初始化 socket ;
    2. 绑定套接字到端口(bind);
    3. 监听端口(listen);
    4. 接受连接请求(accept);
    5. 通信(send/recv);
    6. 关闭连接(close);

    client

    1. 初始化 socket ;
    2. 发出连接请求(connect);
    3. 通信(send/recv);
    4. 关闭连接(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 server

    首先,我们要看官方文档里 wsgi 的解释:PEP 3333 嗯,就是一篇很长的英语阅读理解,大概意思就是如果你想让你的服务器和应用程序一起好好工作,你要遵循这个标准来写你的 web app 和 web server :

    server--middleware--application

    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 标准,这个之后会详谈。

    server

    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 应该是这样一个过程:

    1. 初始化,建立套接字,绑定监听端口;
    2. 设置加载的 web app ;
    3. 开始持续运行 server ;
    4. 处理访问请求(在这里可以加入你自己的处理过程,比如我加入了打印访问信息,字典化访问头部信息等功能);
    5. 获取请求信息及环境信息(get_environ(self));
    6. environ运行加载的 web app 得到返回信息;
    7. 构造返回信息头部;
    8. 返回信息;

    只要实现了以上过程,一个标准的 wsgi server 就写好了。仔细观察,其实一个 wsgi server 的重要之处就在于用environ去跑 web app 得到返回结果这一步,这一步和前面的 application 实现相辅相成,然后框架和服务器都根据这套标准,大家就可以愉快的一起工作了。 现在运行python server.py app.app 8000, 然后浏览器访问localhost:8000server 后端 brower 浏览器

    到此,我们的 wsgi server 已经可以正常运行了,这时我们再来看看 middleware :

    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 访问服务器: 403 可以看到,中间件起作用了!

    接下来,我们再谈谈 Django 和 tornado 对于 wsgi 的支持:

    Django WSGI:

    Django WSGI application

    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

    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 WSGI

    tornado 直接从底层用 epoll 自己实现了 事件池操作、 tcp server 、 http server ,所以它是一个完全不同当异步框架,但 tornado 同样也提供了对 wsgi 对支持,不过这种情况下就没办法用 tornado 异步的特性了。

    与其说 tornado 提供了 wsgi 支持,不如说它只是提供了 wsgi 兼容, tornado 提供两种方式:

    WSGIContainer

    其他应用要在 tornado server 运行, tornado 提供 WSGIContainer。 今天这里主要讨论 wsgi ,所以这里就不分析 tornado 这部分代码,之后做 tornado 源码分析会再分析这里。

    WSGIAdapter

    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,完美运行,中间件也运行正常: tornado

    文中代码源码:simple_wsgi_server 参考资料:Let ’ s Build A Web Server

    原文地址

    作者:rapospectre

    12 条回复    2016-06-03 11:54:43 +08:00
    fcicq
        1
    fcicq  
       2016-06-02 20:49:57 +08:00
    tornado 用 wsgi 明显是降级了. 绝少有人不加个 tornado.gen 或者 async 这样肯定和 wsgi 是不兼容的. 反过来其他 wsgi 跑在 tornado 里面也必然阻塞 ioloop.
    rapospectre
        2
    rapospectre  
    OP
       2016-06-02 20:58:34 +08:00
    @fcicq 嗯嗯,是的。我在文中提到了 tornado app 用 wsgi 跑就失去了异步的特性,这里主要是分析 wsgi server 怎么写, tornado 就恰好拿来当做例子,实际一般不会这样写
    WangYanjie
        3
    WangYanjie  
       2016-06-03 00:26:25 +08:00
    The uwsgi (lowercase!) protocol is the native protocol used by the uWSGI server.
    rapospectre
        4
    rapospectre  
    OP
       2016-06-03 01:42:50 +08:00 via Android
    @WangYanjie 是的,不过 uwsgi 协议具体用在哪里我没有查到明确的说明,我觉得他是用在 uWSGI 和前端服务器之间的?
    aljun
        5
    aljun  
       2016-06-03 02:10:02 +08:00 via iPhone
    前段时间还基于 gevent 的 wsgi 写了个 web 框架⋯⋯

    http://jolla.readthedocs.io/zh/latest/


    文档在这
    WangYanjie
        6
    WangYanjie  
       2016-06-03 02:36:31 +08:00
    @rapospectre uwsgi 似乎是 uWSGI 用的底层协议, **我记得**
    xiaket
        7
    xiaket  
       2016-06-03 06:12:47 +08:00
    Django 源代码里面除了这一段读后很有收获外, Request/Response 这两个类的代码也很有益.
    rapospectre
        8
    rapospectre  
    OP
       2016-06-03 09:28:13 +08:00
    @aljun 棒棒哒,抽时间拜读一下
    rapospectre
        9
    rapospectre  
    OP
       2016-06-03 09:29:14 +08:00
    rapospectre
        10
    rapospectre  
    OP
       2016-06-03 09:31:22 +08:00
    @xiaket 谢谢推荐,之后一定看看
    ipconfiger
        11
    ipconfiger  
       2016-06-03 09:34:47 +08:00
    不错的学习实践材料, 初学者都应该干一遍
    rapospectre
        12
    rapospectre  
    OP
       2016-06-03 11:54:43 +08:00
    @ipconfiger 是的。 web 标准学习路线:搭博客-写框架-写服务器。哈哈
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4421 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 39ms · UTC 10:04 · PVG 18:04 · LAX 02:04 · JFK 05:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.