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

前幾天寫了 淺談cgi、wsgi、uwsgi 與 uWSGI 等一些 python web 開發中遇到的一些名詞的理解,今天博主就根據 wsgi 標準實現一個 web server,並嘗試用它來跑 Django、tornado 框架的 app。python

編寫一個簡單的 http server

在實現 wsgi server 以前咱們先要作一些準備工做。首先,http server 使用 http 協議,而 http 協議封裝在 tcp 協議中,因此要創建一個 http server 咱們先要創建一個 tcp server。要使用 tcp 協議咱們不可能本身實現一個,如今比較流行的解決方案就是使用 socket 套接字編程, socket 已經幫咱們實現了 tcp 協議的細節,咱們能夠直接拿來使用不用關心細節。 socket 編程是語言無關的,不論是之前博主用 MFC 寫聊天室仍是用 C# 寫網絡延遲計算仍是如今寫 http server,它的使用流程都是同樣的:git

server

  1. 初始化 socket;github

  2. 綁定套接字到端口(bind);web

  3. 監聽端口(listen);django

  4. 接受鏈接請求(accept);編程

  5. 通訊(send/recv);後端

  6. 關閉鏈接(close);瀏覽器

client

  1. 初始化 socket;服務器

  2. 發出鏈接請求(connect);cookie

  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
server
後端
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

相關文章
相關標籤/搜索