全文基於Python 2.7 macOS 10.12.2
werkzeug是Python實現的WSGI規範的使用函式庫。什麼是WSGI?如何理解CGI,WSGI 網上的說明很多,在文章的開始,我想要強調兩點
- WSGI是一種伺服器和客戶端互動的介面規範
- 理解web元件:client, server, and middleware.
正如werkzeug官網Werkzeug上所說,werkzeug使用起來非常簡單,但是卻非常強大。關於使用簡單的這個特性,官網給了一段示例程式碼。
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) ——> 瀏覽器處理返回的結果,顯示出來。
再看這段程式碼,開始的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!`
我們的函式必須接受兩個引數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)
原始碼可以看出,**@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 usingwerkzeug.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端就實現了。當然,這個返回非常的簡陋,並沒有什麼實際的資訊返回。我們從這樣一個簡單的例子,應該要發現一點問題。
- 每一種HTTP請求的方法都需要對應去實現一個do_xx()方法,非常的麻煩。
- 所有的請求資訊都暴露給開發者,很多時候開發者作為上層的呼叫者,並不想關心很多底層的操作。
- 當我們的application複雜起來,需要返回更多的內容。每次都要去操作這些基礎的HTTP請求相關的東西。非常的不友好。
接下來,我們看看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
`)
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:
%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應用不可能如此簡單。比如
- 所有的url的處理都導向一個func去處理。
- 返回複雜的資料如何處理
- 各種異常的處理
- 對各種操作的容錯處理
…
後面作者會從主幹擴充開始,慢慢補回枝丫來解釋其他部分。著名的Flask框架就是基於werkzeug和Jinja 2實現的。在之後的文章,我不希望拆開werkzeug和Flask來分析。當做一個整體來看會更加和諧,也便於理解。