Bottle 源碼分析

Bottle 是一個快速,簡單和輕量級的 WSGI 微型 Web 框架的 Python。它做爲單個文件模塊分發,除了 Python 標準庫以外沒有依賴關係。html

選擇源碼分析的版本是 Release 於 2009 年 7 月 11 日的 0.4.10 (這是我能找到的最先的發佈版本了)。python

爲何要分析 Bottle 這個比較冷門的框架?git

  • Bottle 從發佈至今一直貫徹的微型 Web 框架的理念。
  • Bottle 一直堅持單文件發佈,也就是隻有一個 bottle.py 文件。
  • 除了 Python 標準庫以外沒有依賴關係。
  • 與 Flask、Django 都遵循 PEP-3333 的 WSGI 協議。
  • 0.4.10 版本代碼量小,加上大量註釋也只有不到 1000 行的代碼。

因此,拋開框架的高級功能,單單從一個 Web 框架怎麼處理請求的角度來看,Bottle 是最佳的選擇。github

Flask 從初版開始就是依賴於 werkzeug 實現,更多的實現細節須要從 werkzeug 中查找。安全

Django 是個重型框架,不適合總體代碼閱讀,各個組件看看就能夠。cookie

Tornado 是個異類,和 WSGI 沒有什麼關係。app

在閱讀以前最好從 Github 上下載一份 0.4.10 版本的 Bottle 的源碼,邊看邊閱讀本文。框架

閱讀本文你須要有以下技能:函數

  • 熟悉 Python 的語法
  • 熟悉 HTTP 協議
  • 至少使用過一種 WSGI 的框架
  • 瞭解 CGI
  • 看得懂中文

流程結構分析

代碼雖然很少,可是毫無目的的看不免思緒混亂,會看的心煩意亂,甚至會有產生「寫的這是什麼鬼?」的想法。源碼分析

一個 Web 框架最核心也是最基本的功能就是處理 請求響應

可是在這以前,須要先建立一個 Server,才能開始處理啊!

因此大致的流程以下:

  1. 怎麼建立一個 WSGI 的 Server 。
  2. 怎麼處理到來的請求。
  3. 怎麼處理響應。

建立 WSGI Server

在 Bottle 中關於建立一個標準的 WSGI Server 涉及的類或者方法只有 3 個。

注意,這裏只關心一個標準的 WSGI,和核心功能。包括註釋、錯誤處理、參數處理,會通通刪除。

從文檔中能夠看到 Bottle 是經過一個 run 方法啓動的。

def run(server=WSGIRefServer, host='127.0.0.1', port=8080, optinmize = False, **kargs):
    server = server(host=host, port=port, **kargs)
    server.run(WSGIHandler)複製代碼

WSGIRefServer 繼承自 ServerAdapter,而且覆蓋了 run 方法。

class ServerAdapter(object):
    def __init__(self, host='127.0.0.1', port=8080, **kargs):
        self.host = host
        self.port = int(port)
        self.options = kargs

    def __repr__(self):
        return "%s (%s:%d)" % (self.__class__.__name__, self.host, self.port)

    def run(self, handler):
        pass

class WSGIRefServer(ServerAdapter):
    def run(self, handler):
        from wsgiref.simple_server import make_server
        srv = make_server(self.host, self.port, handler)
        srv.serve_forever()複製代碼

這個 run 方法自己也是很簡單,經過 Python 標準庫中的 make_server 建立了一個 WSGI Server 而後跑了起來。

注意在 run 方法中的 WSGIHandler 和 WSGIRefServer.run 中的 handler 參數,這個就是如何處理一次請求和響應的關鍵所在。

在這以前,還須要先看看 Bottle 對 Request 和 Respouse 的定義。

Request 定義

Bottle 爲每次請求都會把一些參數保存在當前的線程中,經過繼承 threading.local 實現線程安全。

class Request(threading.local):
    pass # 省略其餘方法複製代碼

Request 是由一個方法和 8 個屬性構成。

def bind(self, environ):
    """ 綁定當前請求到 handler 中 """
    self._environ = environ
    self._GET = None
    self._POST = None
    self._GETPOST = None
    self._COOKIES = None
    self.path = self._environ.get('PATH_INFO', '/').strip()
    if not self.path.startswith('/'):
        self.path = '/' + self.path複製代碼

bind 方法除了初始化一些變量之外,還添加 environ 到本次請求當中,environ 是一個字典包含了 CGI 的環境變量,更多 environ 內容參考PEP-3333 中 environ Variables 部分

@property
def method(self):
    ''' 返回請求方法 (GET,POST,PUT,DELETE 等) '''
    return self._environ.get('REQUEST_METHOD', 'GET').upper()

@property
def query_string(self):
    ''' QUERY_STRING 的內容 '''
    return self._environ.get('QUERY_STRING', '')

@property
def input_length(self):
    ''' Content 的長度 '''
    try:
        return int(self._environ.get('CONTENT_LENGTH', '0'))
    except ValueError:
        return 0複製代碼

這三個屬性比較簡單,只是從 _environ 中取出了CGI 的某個環境變量。

@property
def GET(self):
    """ 返回字典類型的 GET 參數 """
    if self._GET is None:
        raw_dict = parse_qs(self.query_string, keep_blank_values=1)
        self._GET = {}
        for key, value in raw_dict.items():
            if len(value) == 1:
                self._GET[key] = value[0]
            else:
                self._GET[key] = value
    return self._GET複製代碼

GET 屬性把 query_string 解析成字典放入當前請求的變量中,因此在請求中獲取 GET 方法的參數可使用 requst.GET['xxxx'] 這樣子的用法。

@property
def POST(self):
    """返回字典類型的 POST 參數"""
    if self._POST is None:
        raw_data = cgi.FieldStorage(
            fp=self._environ['wsgi.input'], environ=self._environ)
        self._POST = {}
        if raw_data:
            for key in raw_data:
                if isinstance(raw_data[key], list):
                    self._POST[key] = [v.value for v in raw_data[key]]
                elif raw_data[key].filename:
                    self._POST[key] = raw_data[key]
                else:
                    self._POST[key] = raw_data[key].value
    return self._POST複製代碼

POST 屬性從 wsgi.input 中獲取內容(也就是表單提交的內容)放入當前請求的變量中,能夠經過 request.POST['xxxx'] 來獲取數據。

從 GET 和 POST 這兩屬性的使用來看,包括 Flask 和 Django 都實現了相似的方法,這方法屬性擁有同樣的步驟就是獲取數據,而後轉換成標準的字典格式,實現上來看沒什麼複雜的,就是普通的字符串處理而已。

@property
def params(self):
    ''' 返回 GET 和 POST 的混合數據,POST 會覆蓋 GET '''
    if self._GETPOST is None:
        self._GETPOST = dict(self.GET)
        self._GETPOST.update(dict(self.POST))
    return self._GETPOST複製代碼

params 屬性提供了一個便利訪問數據的方法。

@property
def COOKIES(self):
    """Returns a dict with COOKIES."""
    if self._COOKIES is None:
        raw_dict = Cookie.SimpleCookie(self._environ.get('HTTP_COOKIE',''))
        self._COOKIES = {}
        for cookie in raw_dict.values():
            self._COOKIES[cookie.key] = cookie.value
     return self._COOKIES複製代碼

Bottle 的 COOKIES 管理比較簡單,只是單純的從 CGI 中獲取請求的 Cookie,若是存在的話直接返回。

以上就是 Bottle 的請求定義的內容。

簡單總結來看,Request 從 CGI 中獲取數據而且作一些數據處理,而後綁定到變量上。

Response 定義

總體結構和 Resquest 大體同樣。

def bind(self):
    """ 清除舊數據並建立一個全新的響應對象 """
    self._COOKIES = None

    self.status = 200
    self.header = HeaderDict()
    self.content_type = 'text/html'
    self.error = None複製代碼

bind 方法只是初始化了一些變量。其中比較有意思的是 HeaderDict。

class HeaderDict(dict):
    def __setitem__(self, key, value):
        return dict.__setitem__(self,key.title(), value)
    def __getitem__(self, key):
        return dict.__getitem__(self,key.title())
    def __delitem__(self, key):
        return dict.__delitem__(self,key.title())
    def __contains__(self, key):
        return dict.__contains__(self,key.title())

    def items(self):
        """ 返回 (key, value) 形式的元組列表 """
        for key, values in dict.items(self):
            if not isinstance(values, list):
                values = [values]
            for value in values:
                yield (key, str(value))

    def add(self, key, value):
        """ 添加一個新 header,而不刪除舊 header """
        if isinstance(value, list):
            for v in value:
                self.add(key, v)
        elif key in self:
            if isinstance(self[key], list):
                self[key].append(value)
            else:
                self[key] = [self[key], value]
        else:
          self[key] = [value]複製代碼

這是一個擴展於 dict 的字典,轉化成大小寫無關的 Title key ,還能夠以列表方式添加多個成員。這個 HeaderDict 有意思的地方有兩個:

  • 與大小無關的 Ttile key,也就是會吧 key 轉成以大寫頭其餘小寫的 key
  • 存儲重複 kv 值時候 values 會以 list 形式存儲。若是 values 是多層 list,會自動解析成一層數據。
  • 重寫 items 方法,以二元元組方式返回數據,包括多值數據。
>>> h = HeaderDict()
>>> h.add('mytest', [['Test', ['test1', ['test2']]], {'name':'two'}])
>>> h
{'Mytest': ['Test', 'test1', 'test2', {'name': 'two'}]}
>>> print list(h.items())
[('Mytest', 'Test'), ('Mytest', 'test1'), ('Mytest', 'test2'), ('Mytest', "{'name': 'two'}")]
>>>複製代碼
@property
def COOKIES(self):
    if not self._COOKIES:
        self._COOKIES = Cookie.SimpleCookie()
    return self._COOKIES

def set_cookie(self, key, value, **kargs):
    """ 設置 Cookie """
    self.COOKIES[key] = value
    for k in kargs:
        self.COOKIES[key][k] = kargs[k]複製代碼

Response 對 Cookie 的初始化,而且提供了設置的方法。

def get_content_type(self):
    return self.header['Content-Type']

def set_content_type(self, value):
    self.header['Content-Type'] = value

content_type = property(
    get_content_type,
    set_content_type,
    None,
    get_content_type.__doc__)複製代碼

爲 content_type 屬性提供了 set 和 get 方法,針對的是 Header 中的 Content-Type。

添加路由和 handler

這部分由一個裝飾器和三個方法組成。

  • compile_route:路由正則
  • add_route:添加路由
  • route:路由裝飾器
def route(url, **kargs):
    def wrapper(handler):
        add_route(url, handler, **kargs)
        return handler
    return wrapper複製代碼

路由裝飾器,簡化 add_route 的調用。

def add_route(route, handler, method='GET', simple=False):
    method = method.strip().upper()
    if re.match(r'^/(\w+/)*\w*$', route) or simple:
        ROUTES_SIMPLE.setdefault(method, {})[route] = handler
    else:
        route = compile_route(route)
        ROUTES_REGEXP.setdefault(method, []).append([route, handler])複製代碼

ROUTES_SIMPLE 和 ROUTES_REGEXP 是兩個全局字典,用於存儲路由相關數據(方法,參數,地址)。

簡單路由放入 ROUTES_SIMPLE,以 method 爲 key ,在 method 中再以路由地址爲 key,處理函數 handler 爲 value 存儲。

複雜路由放入 ROUTES_REGEXP,以 method 爲 key,以 route 和 handler 組成的元組列表存儲。

處理請求和響應

根據 PEP-3333 文檔須要爲編寫一個可調用對象(能夠是函數,或者是具備 __call__ 方法的類)。

Bottle 中的 WSGIHandler 正是這麼一個可調用對象。

def WSGIHandler(environ, start_response):
    # 全局 request、response,每一個線程獨立
    global request
    global response

    # bind 當前 environ 數據
    request.bind(environ) 
    response.bind()
    try:
        # 根據 path 和 method 找處處理方法和參數
        handler, args = match_url(request.path, request.method)
        if not handler:
            raise HTTPError(404, "Not found")
        # 執行返回 output 數據
        output = handler(**args)
    except BreakTheBottle, shard:
        # Bottle 錯誤產生的輸出
        output = shard.output
    except Exception, exception:
        # 處理內部錯誤,500 錯誤
        response.status = getattr(exception, 'http_status', 500)
        errorhandler = ERROR_HANDLER.get(response.status, error_default)
        try:
            output = errorhandler(exception)
        except:
            output = "Exception within error handler! Application stopped."

        if response.status == 500:
            request._environ['wsgi.errors'].write("Error (500) on '%s': %s\n" % (request.path, exception))

    db.close() # DB cleanup

    # 若是是文件,則發送文件
    if hasattr(output, 'read'):
        fileoutput = output
        if 'wsgi.file_wrapper' in environ:
            output = environ['wsgi.file_wrapper'](fileoutput)
        else:
            output = iter(lambda: fileoutput.read(8192), '')
    elif isinstance(output, str):
        output = [output]

    # 根據 response 的 cookie 添加 Set-Cookie 的 header
    for c in response.COOKIES.values():
        response.header.add('Set-Cookie', c.OutputString())

    # 完成本次處理
    status = '%d %s' % (response.status, HTTP_CODES[response.status])
    start_response(status, list(response.header.items()))
    return output複製代碼

爲了和代碼契合度高,分析已經註釋在當中。

處理流程以下:

  1. 拿到線程獨立的 request 和 response
  2. bind environ 數據
  3. 根據 match_url 找處處理的 handler 和參數,執行
    1. 處理 Bottle 錯誤
    2. 處理內部錯誤
  4. 若是是文件則發送文件,不是的話正常返回字符串
  5. 設置 Set-Cookie header
  6. 結束

結束

Bottle 0.4.10 版本的核心內容就差麼多,其餘都是一些錯誤處理之類的。

該版本的 Bottle 以簡單的過程,描述出了一個基於 WSGI 的 Web 框架是怎麼樣處理請求和響應的過程,徹底基於 Python 標準庫實現。

好噠,麼麼噠~~~,Python 大法好啊,Python 大法好啊,Python 大法好啊。

相關文章
相關標籤/搜索