自己寫一個 wsgi 服務器運行 Django 、Tornado 等框架應用

來源:https://segmentfault.com/a/1190000005640475


前幾天寫了 淺談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:8000

後端

瀏覽器

到此,我們的 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 訪問服務器:

可以看到,中間件起作用了!

接下來,我們再談談 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,完美運行,中間件也運行正常:

文中代碼源碼:simple_wsgi_server
參考資料:Let’s Build A Web Server

原文地址

作者:rapospectre


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章