全文基於Python 2.7 macOS 10.12.2html
werkzeug是Python實現的WSGI規範的使用函數庫。什麼是WSGI?如何理解CGI,WSGI 網上的說明不少,在文章的開始,我想要強調兩點python
正如werkzeug官網Werkzeug上所說,werkzeug使用起來很是簡單,可是卻很是強大。關於使用簡單的這個特性,官網給了一段示例代碼。web
from werkzeug.wrappers import Request, Response
@Request.application
def application(request):
return Response('Hello World!')
if __name__ == '__main__':
from werkzeug.serving import run_simple
run_simple('localhost', 4000, application)
複製代碼
運行起來之後,打開咱們的瀏覽器輸入127.0.0.1:4000就能夠看到數組
這篇文章咱們就將從這段示例代碼入手,從源碼的角度分析一下背後到底是如何實現的。瀏覽器
一個web應用的本質,實際就是: 瀏覽器(client)發送一個請求(request) ——> 服務器(server)接收到請求 ——> 服務器處理請求 ——> 返回處理的結果(response) ——> 瀏覽器處理返回的結果,顯示出來。bash
再看這段代碼,開始的func application(),很是的容易理解。函數在server端,接收了來自client的一個request,通過內部的處理之後返回了一個response。可是若是看過其餘WSGI教程(好比wsgi接口)的朋友應該會感受到奇怪,這個函數和別的地方舉例的不太同樣,由於WSGI要求web開發者必須實現的函數是這個樣子的服務器
defapplication(environ, start_response): start_response('200 OK', [('Content-Type','text/html')]) return 'Hello, web!'
app
咱們的函數必須接受兩個參數environ,start_response。environ是一個保存了請求的各項信息的字典,而start_response是一個func,咱們能夠用它來給client端返回狀態碼和response headers。最後return咱們真正想要返回的數據。可是werkzeug的這段示例代碼卻簡化了不少,緣由就在這個函數的裝飾器上 **@Request.application **框架
**@classmethod** def application(cls, f): def application(*args): request = cls(args[-2]) with request: return f(*args[:-2] + (request,))(*args[-2:]) return update_wrapper(application, f)
dom
源碼能夠看出,**@Request.application **攔截了func的倒數第二個參數(也就是environ),構建了request對象;而後把原參數移除了倒數兩個參數(environ和start_response)之後和request對象一塊兒傳入了咱們本身實現的func application();調用func application()返回的對象,傳入原參數的倒數兩個。把最後的執行結果返回。 爲了便於理解,我寫了一個裝飾器,打印每一個函數的參數。對比一下就很明瞭了。
若是你仍是有一點疑惑,以爲話說的有點繞口。咱們先保持疑問,在後面做者再給你們細細解釋。
看到這裏,這個func先放一邊。咱們來看看func run_simple()。這是這個簡單web應用的入口函數。在~/werkzeug/serving.py文件的開始,有這樣一段註釋
... There are many ways to serve a WSGI application. While you're developing it you usually don't want a full blown webserver like Apache but a simple standalone one. ... For bigger applications you should consider using
werkzeug.script
instead of a simple start file.
大多數狀況下,咱們並不須要一個大而全的webserver。好比,Apache。一個簡單而獨立的webserver就夠了。很顯然,咱們今天所討論的run_simple()就是爲此場景而服務的.
def run_simple(hostname, port, application, use_reloader=False,
use_debugger=False, use_evalex=True,
extra_files=None, reloader_interval=1,
reloader_type='auto', threaded=False,
processes=1, request_handler=None, static_files=None,
passthrough_errors=False, ssl_context=None):
if use_debugger:
from werkzeug.debug import DebuggedApplication
application = DebuggedApplication(application, use_evalex)
if static_files:
from werkzeug.wsgi import SharedDataMiddleware
application = SharedDataMiddleware(application, static_files)
def log_startup(sock):
display_hostname = hostname not in ('', '*') and hostname or 'localhost'
if ':' in display_hostname:
display_hostname = '[%s]' % display_hostname
quit_msg = '(Press CTRL+C to quit)'
port = sock.getsockname()[1]
_log('info', ' * Running on %s://%s:%d/ %s',
ssl_context is None and 'http' or 'https',
display_hostname, port, quit_msg)
def inner():
try:
fd = int(os.environ['WERKZEUG_SERVER_FD'])
except (LookupError, ValueError):
fd = None
srv = make_server(hostname, port, application, threaded,
processes, request_handler,
passthrough_errors, ssl_context,
fd=fd)
if fd is None:
log_startup(srv.socket)
srv.serve_forever()
if use_reloader:
# If we're not running already in the subprocess that is the
# reloader we want to open up a socket early to make sure the
# port is actually available.
if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
if port == 0 and not can_open_by_fd:
raise ValueError('Cannot bind to a random port with enabled '
'reloader if the Python interpreter does '
'not support socket opening by fd.')
# Create and destroy a socket so that any exceptions are
# raised before we spawn a separate Python interpreter and
# lose this ability.
address_family = select_ip_version(hostname, port)
s = socket.socket(address_family, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((hostname, port))
if hasattr(s, 'set_inheritable'):
s.set_inheritable(True)
# If we can open the socket by file descriptor, then we can just
# reuse this one and our socket will survive the restarts.
if can_open_by_fd:
os.environ['WERKZEUG_SERVER_FD'] = str(s.fileno())
s.listen(LISTEN_QUEUE)
log_startup(s)
else:
s.close()
# Do not use relative imports, otherwise "python -m werkzeug.serving"
# breaks.
from werkzeug._reloader import run_with_reloader
run_with_reloader(inner, extra_files, reloader_interval,
reloader_type)
else:
inner()
複製代碼
開始研究源碼,咱們應該學會作減法,去掉一些分支看主幹,避免對總體理解的干擾。在示例代碼中,咱們用到的參數是hostname(本地運行默認127.0.0.1),port(默認是4000),application(就是咱們傳入的func application())。其他的參數所有按照默認設置來執行。源碼中有不少的判斷,咱們根據示例代碼的參數,對源碼進行處理,去掉不執行的部分。以下:
def run_simple(hostname, port, application, use_reloader=False,
use_debugger=False, use_evalex=True,
extra_files=None, reloader_interval=1,
reloader_type='auto', threaded=False,
processes=1, request_handler=None, static_files=None,
passthrough_errors=False, ssl_context=None):
def log_startup(sock):
display_hostname = hostname not in ('', '*') and hostname or 'localhost'
if ':' in display_hostname:
display_hostname = '[%s]' % display_hostname
quit_msg = '(Press CTRL+C to quit)'
port = sock.getsockname()[1]
_log('info', ' * Running on %s://%s:%d/ %s',
ssl_context is None and 'http' or 'https',
display_hostname, port, quit_msg)
def inner():
try:
fd = int(os.environ['WERKZEUG_SERVER_FD'])
except (LookupError, ValueError):
fd = None
srv = make_server(hostname, port, application, threaded,
processes, request_handler,
passthrough_errors, ssl_context,
fd=fd)
if fd is None:
log_startup(srv.socket)
srv.serve_forever()
inner()
複製代碼
如今看來就簡單不少了。執行了一個inner()函數:首先從環境變量中獲取'WERKZEUG_SERVER_FD'的值,若是爲空就執行log_startup()函數。執行make_server()函數並讓返回的對象執行server_forever()函數。
那咱們再去make_server()裏看看到底作了什麼。
def make_server(host=None, port=None, app=None, threaded=False, processes=1,
request_handler=None, passthrough_errors=False,
ssl_context=None, fd=None):
"""Create a new server instance that is either threaded, or forks or just processes one request after another. """
if threaded and processes > 1:
raise ValueError("cannot have a multithreaded and "
"multi process server.")
elif threaded:
return ThreadedWSGIServer(host, port, app, request_handler,
passthrough_errors, ssl_context, fd=fd)
elif processes > 1:
return ForkingWSGIServer(host, port, app, processes, request_handler,
passthrough_errors, ssl_context, fd=fd)
else:
return BaseWSGIServer(host, port, app, request_handler,
passthrough_errors, ssl_context, fd=fd)
複製代碼
make_server()函數建立了一個新的server對象。參看一下源碼,** ThreadedWSGIServer** 和 ** ForkingWSGIServer 都是 ** BaseWSGIServer的子類。這篇文章咱們就不仔細分析區別,就以BaseWSGIServer入手。
BaseWSGIServer繼承自HTTPServer。在~/werkzeug/serving.py的開始
try:
import SocketServer as socketserver
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
except ImportError:
import socketserver
from http.server import HTTPServer, BaseHTTPRequestHandler
複製代碼
werkzeug的HTTP服務是基於Pyhton的BaseHTTPServer來實現的。關於BaseHTTPServer的文檔在這裏BaseHTTPServer。通常咱們使用class HTTPServer來創建Server端,監聽指定端口,而後建立一個class BaseHTTPRequestHandler來處理咱們捕獲到的request。在werkzeug中** WSGIRequestHandler** 繼承自** BaseHTTPRequestHandler**。
關於BaseHTTPServer並無什麼太多值得討論的東西,基本暴露出的接口都是調用父類的同名方法。咱們要作的也就是傳入hostname,port等參數,而後執行serve_forever()來創建服務。(serve_forever()看着眼熟嗎?就是run_simple()中make_server()返回的srv對象執行的方法) 可是** WSGIRequestHandler **,做者仍是想和各位嘮叨幾句。
根據Pyhton 2.7的官方文檔所說,** BaseHTTPRequestHandler**這個類在Server端接收到請求之後會根據請求的方法來執行do_xx()函數。好比說咱們發起的是一個GET請求,那麼就會執行do_GET()這個函數。但對應各類請求方法的do_xx()函數父類並無實現,須要使用者自行去定義。
文檔中有一段示例代碼,咱們複製下來(以下)
import BaseHTTPServer
def run(server_class=BaseHTTPServer.HTTPServer,
handler_class=BaseHTTPServer.BaseHTTPRequestHandler):
server_address = ('', 5000)
httpd = server_class(server_address, handler_class)
httpd.serve_forever()
if __name__ == '__main__':
run()
複製代碼
執行會在本地的5000端口創建一個服務。可是當你使用瀏覽器前往127.0.0.1:5000時會收到一個501的錯誤。
緣由就在於咱們並無實現do_GET()方法。如今咱們稍稍修改代碼,繼承BaseHTTPRequestHandler實現一個do_GET()方法,調用父類中send_response()返回client端一個200的狀態碼和一個'success message!'。import BaseHTTPServer
class HTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200,'success message!')
def run(server_class=BaseHTTPServer.HTTPServer,
handler_class=HTTPServerHandler):
server_address = ('', 5000)
httpd = server_class(server_address, handler_class)
httpd.serve_forever()
if __name__ == '__main__':
run()
複製代碼
再次請求。打開瀏覽器的開發者工具,在network中能夠看到返回的結果。
這樣。一個最簡單的Server端就實現了。固然,這個返回很是的簡陋,並無什麼實際的信息返回。咱們從這樣一個簡單的例子,應該要發現一點問題。
接下來,咱們看看werkzeug中是如何處理的。在官方文檔中
handle() Calls handle_one_request() once (or, if persistent connections are enabled, multiple times) to handle incoming HTTP requests. You should never need to override it; instead, implement appropriate do_*() methods.
handle_one_request() This method will parse and dispatch the request to the appropriate do_*() method. You should never need to override it.
提到了,當咱們捕獲到request的時候調用的是這兩個func()。而且,它給咱們的提示是**You should never need to override it. **(滑稽)。但實際在werkzeug中這兩個方法是被override了的。要想不顧官方的阻攔獨斷獨行,首先咱們仍是應該瞭解這兩個方法到底是怎麼實現的。
def handle_one_request(self):
"""Handle a single HTTP request. You normally don't need to override this method; see the class __doc__ string for information on how to handle specific HTTP commands such as GET and POST. """
try:
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(414)
return
if not self.raw_requestline:
self.close_connection = 1
return
if not self.parse_request():
# An error code has been sent, just exit
return
mname = 'do_' + self.command
if not hasattr(self, mname):
self.send_error(501, "Unsupported method (%r)" % self.command)
return
method = getattr(self, mname)
method()
self.wfile.flush() #actually send the response if not already done.
except socket.timeout, e:
#a read or a write timed out. Discard this connection
self.log_error("Request timed out: %r", e)
self.close_connection = 1
return
def handle(self):
"""Handle multiple requests if necessary."""
self.close_connection = 1
self.handle_one_request()
while not self.close_connection:
self.handle_one_request()
複製代碼
捕獲了request以後執行的是func handle()。其中self.close_connection 是一個關閉鏈接的標誌位。只有在request header中包含keep-alive且協議版本號大於HTTP/1.1的時候設0,其他狀況下置1。捕獲request的具體實現仍是在handle_one_request()中。
1.首先判斷接收的字節數。是否大於65536。大於返回414錯誤。(IP首部中標識長度的有16bit,因此長度限制不能夠大於2^16) 2.判斷讀取的字節。爲空關閉鏈接 3.調用parse_request()解析request。解析錯誤返回狀態嗎,成功繼續。 4.根據請求的方法尋找對應的函數。若是子類沒有實現對應方法,返回501錯誤。找到對應方法,執行並將返回內容寫入數據。 5.若是timeout,打個log記錄一下。
這就是BaseHTTPServer中的處理。在瞭解了它的原理以後,讓咱們回到werkzeug,看看它是如何處理的。
def handle(self):
"""Handles a request ignoring dropped connections."""
rv = None
try:
rv = BaseHTTPRequestHandler.handle(self)
except (socket.error, socket.timeout) as e:
self.connection_dropped(e)
except Exception:
if self.server.ssl_context is None or not is_ssl_error():
raise
if self.server.shutdown_signal:
self.initiate_shutdown()
return rv
def handle_one_request(self):
"""Handle a single HTTP request."""
self.raw_requestline = self.rfile.readline()
if not self.raw_requestline:
self.close_connection = 1
elif self.parse_request():
return self.run_wsgi()
複製代碼
BaseHTTPServer並不支持SSL。在werkzeug中添加了對SSL的支持。因此在werkzeug的handle()中,它在執行了父類的func handle()後,除了對socket.timeout的處理,還考慮了SSL的可能。在func handle_one_request()中,減小了對字節流長度的判斷。和BaseHTTPServer不一樣的是,werkzeug中把全部請求方法的處理,都放到了func run_wsgi()中,而不是根據請求方法區分紅不一樣的方法而且要求子類來實現。
def run_wsgi(self):
if self.headers.get('Expect', '').lower().strip() == '100-continue':
self.wfile.write(b'HTTP/1.1 100 Continue\r\n\r\n')
self.environ = environ = self.make_environ()
headers_set = []
headers_sent = []
def write(data):
assert headers_set, 'write() before start_response'
if not headers_sent:
status, response_headers = headers_sent[:] = headers_set
try:
code, msg = status.split(None, 1)
except ValueError:
code, msg = status, ""
self.send_response(int(code), msg)
header_keys = set()
for key, value in response_headers:
self.send_header(key, value)
key = key.lower()
header_keys.add(key)
if 'content-length' not in header_keys:
self.close_connection = True
self.send_header('Connection', 'close')
if 'server' not in header_keys:
self.send_header('Server', self.version_string())
if 'date' not in header_keys:
self.send_header('Date', self.date_time_string())
self.end_headers()
assert isinstance(data, bytes), 'applications must write bytes'
self.wfile.write(data)
self.wfile.flush()
def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
if headers_sent:
reraise(*exc_info)
finally:
exc_info = None
elif headers_set:
raise AssertionError('Headers already set')
headers_set[:] = [status, response_headers]
return write
def execute(app):
application_iter = app(environ, start_response)
try:
for data in application_iter:
write(data)
if not headers_sent:
write(b'')
finally:
if hasattr(application_iter, 'close'):
application_iter.close()
application_iter = None
try:
execute(self.server.app)
except (socket.error, socket.timeout) as e:
self.connection_dropped(e, environ)
except Exception:
if self.server.passthrough_errors:
raise
from werkzeug.debug.tbtools import get_current_traceback
traceback = get_current_traceback(ignore_system_exceptions=True)
try:
# if we haven't yet sent the headers but they are set
# we roll back to be able to set them again.
if not headers_sent:
del headers_set[:]
execute(InternalServerError())
except Exception:
pass
self.server.log('error', 'Error on request:\n%s',
traceback.plaintext)
複製代碼
func run_wsgi()首先對request的headers進行了一個判斷。有關HTTP協議的東西不在咱們這篇文章的討論範圍內,RFC的文檔在8.2.3 Use of the 100 (Continue) Status這裏。有興趣的朋友參考文檔一下,這裏咱們能夠先忽略了這個判斷。
以後是獲取環境變量environ。func make_environ()實質做用就是打包各種信息成一個字典。當你去查看這個函數源碼時,你會發現這個字典的不少key值你很是的眼熟。沒錯,就是文章開始在解釋func application()時,打印傳入參數時打印的那個environ參數。func make_environ()獲取了各項信息之後會在以後傳給咱們本身實現的application。這裏咱們先簡單略過,後面遇到再討論。
以後聲明瞭兩個數組和三個func,咱們直接略過一路看到底。看到結尾處的try-catch,這纔是run_wsgi()的入口位置。首先函數調用了func execute(),傳入綁定在HTTPServer上的application。這個application就是func run_simple()咱們傳入的自定義的那個func application(),make_server()建立HTTPServer時將application綁定在上面。
application_iter = app(environ, start_response)
這樣簡單的一句乍看會讓人比較的懵。由於咱們知道,func application()返回的是一個Response對象,而這裏application_iter很明顯是一個能夠迭代的對象。其中的祕密就在文章開始咱們所說的那個deractor**@Request.application**。
咱們已經解釋過了,這個deractor攔截倒數第二個參數,對比這裏就是environ,建立一個request對象,而後和倒數第二以前的參數(也就是隻傳入了request對象)一塊兒傳入咱們的func application(),return了一個Response對象。return f(*args[:-2] + (request,))(*args[-2:])
,這個deractor在return的時候又把這個response當作函數來處理了一把。在這個例子中最後的結果相似這樣response(environ,start_response)
。這樣列出來就很明確了,咱們去~/werkzeug/wrapper.py中看看class BaseResponse的__call__方法。
def __call__(self, environ, start_response):
"""Process this response as WSGI application. :param environ: the WSGI environment. :param start_response: the response callable provided by the WSGI server. :return: an application iterator """
app_iter, status, headers = self.get_wsgi_response(environ)
start_response(status, headers)
return app_iter
複製代碼
關於class Request和class Response其實有不少值得討論的地方。這裏我不展開討論func get_wsgi_response()方法的實現。咱們只要明確,返回了三個對象,app_iter(包含了須要返回的各項數據))status(狀態碼)headers(response headers)。其中app_iter就是咱們上面剛剛討論的application_iter。函數裏調用傳入的func start_response來寫入status和headers。
def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
if headers_sent:
reraise(*exc_info)
finally:
exc_info = None
elif headers_set:
raise AssertionError('Headers already set')
headers_set[:] = [status, response_headers]
return write
複製代碼
func start_response()將status和response_headers合併在func run_wsgi()開始建立的數組裏,而後返回一個func write()。這些信息只是被保存下來,此時尚未被寫入wfile。此時咱們獲取了application_iter,開始迭代調用func write()將每一項信息寫入wfile
def write(data):
assert headers_set, 'write() before start_response'
if not headers_sent:
status, response_headers = headers_sent[:] = headers_set
try:
code, msg = status.split(None, 1)
except ValueError:
code, msg = status, ""
self.send_response(int(code), msg)
header_keys = set()
for key, value in response_headers:
self.send_header(key, value)
key = key.lower()
header_keys.add(key)
if 'content-length' not in header_keys:
self.close_connection = True
self.send_header('Connection', 'close')
if 'server' not in header_keys:
self.send_header('Server', self.version_string())
if 'date' not in header_keys:
self.send_header('Date', self.date_time_string())
self.end_headers()
assert isinstance(data, bytes), 'applications must write bytes'
self.wfile.write(data)
self.wfile.flush()
複製代碼
func write()首先判斷了header_set是否爲空,保證func start_response在func write以前執行。這是由於咱們在寫入數據以前首先要寫入response_headers,再調用func self.end_headers()來將response_headers和應用數據區分開來。若是順序錯開就會發生錯誤。
再拿以前的例子用一下,此次咱們加一點內容
import BaseHTTPServer
class HTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200,'success message!')
self.end_headers()
self.wfile.write('hello world!')
def run(server_class=BaseHTTPServer.HTTPServer,
handler_class=HTTPServerHandler):
server_address = ('', 5000)
httpd = server_class(server_address, handler_class)
httpd.serve_forever()
if __name__ == '__main__':
run()
複製代碼
運行這樣一段代碼以後打開瀏覽器輸入127.0.0.1:5000,你能夠看到瀏覽器顯示
可是若是你替換了一下寫入的順序
def do_GET(self):
self.wfile.write('hello world!')
self.send_response(200,'success message!')
self.end_headers()
複製代碼
再次訪問127.0.0.1:5000,這個時候就會報錯了。
官網的文檔解釋了end_headers()的做用。雖然簡單,但也仍是要咱們注意
end_headers() Sends a blank line, indicating the end of the HTTP headers in the response.
回到werkzeug的源碼,以後咱們判斷headers_sent是否爲空。其實看這個命令咱們也能猜出大概,這是保存已經寫入過的response_headers的數組。若是爲空證實咱們尚未寫入,進入if判斷裏,從咱們以前func start_response中保存的headers_set中取出狀態碼和response_headers寫入返回信息中。而且覈查了幾個必要的response_headers是否存在,若是不存在就進行設置寫入。而且每個寫入的response_headers都被保存進headers_sent。這樣第二次調用func write就再也不重複設置。
func write的最後是真正的數據寫入操做。
回到func run_wsgi的主幹上來。咱們在拿到application_iter後開始逐個迭代調用func write寫入response。另外,假設func start_response沒有設置任何response_headers,application_iter也爲空。爲了保證func write必定被執行一次,response_headers默認值被寫入。咱們會寫入一個*' '*的數據。
以後是一些對於異常的處理。包括超時或者不可預知的錯誤。會拋出異常或者打log記錄。
至此。一個完整的web流程就走完了。
這篇文章做者嘗試把werkzeug這顆大樹的枝丫所有砍去,留下一根主幹來講明這個框架核心所在。固然,現實的web應用不可能如此簡單。好比
...
後面做者會從主幹拓展開始,慢慢補回枝丫來解釋其餘部分。著名的Flask框架就是基於werkzeug和Jinja 2實現的。在以後的文章,我不但願拆開werkzeug和Flask來分析。當作一個總體來看會更加和諧,也便於理解。