Bottle 是一個快速,簡單和輕量級的 WSGI 微型 Web 框架的 Python。它做爲單個文件模塊分發,除了 Python 標準庫以外沒有依賴關係。html
選擇源碼分析的版本是 Release 於 2009 年 7 月 11 日的 0.4.10 (這是我能找到的最先的發佈版本了)。python
爲何要分析 Bottle 這個比較冷門的框架?git
因此,拋開框架的高級功能,單單從一個 Web 框架怎麼處理請求的角度來看,Bottle 是最佳的選擇。github
Flask 從初版開始就是依賴於 werkzeug 實現,更多的實現細節須要從 werkzeug 中查找。安全
Django 是個重型框架,不適合總體代碼閱讀,各個組件看看就能夠。cookie
Tornado 是個異類,和 WSGI 沒有什麼關係。app
在閱讀以前最好從 Github 上下載一份 0.4.10 版本的 Bottle 的源碼,邊看邊閱讀本文。框架
閱讀本文你須要有以下技能:函數
代碼雖然很少,可是毫無目的的看不免思緒混亂,會看的心煩意亂,甚至會有產生「寫的這是什麼鬼?」的想法。源碼分析
一個 Web 框架最核心也是最基本的功能就是處理 請求 和 響應。
可是在這以前,須要先建立一個 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 的定義。
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 中獲取數據而且作一些數據處理,而後綁定到變量上。
總體結構和 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 有意思的地方有兩個:
>>> 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。
這部分由一個裝飾器和三個方法組成。
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複製代碼
爲了和代碼契合度高,分析已經註釋在當中。
處理流程以下:
Bottle 0.4.10 版本的核心內容就差麼多,其餘都是一些錯誤處理之類的。
該版本的 Bottle 以簡單的過程,描述出了一個基於 WSGI 的 Web 框架是怎麼樣處理請求和響應的過程,徹底基於 Python 標準庫實現。
好噠,麼麼噠~~~,Python 大法好啊,Python 大法好啊,Python 大法好啊。