Bottle 是一種 WSGI 架構,也就有 WSGI 的同步本性,所幸有超讚的 「gevent 專案」,bottle 仍有可能寫出非同步應用。
#非同步應用入門
非同步的設計模式沒有辦法與 WSGI 的同步本性好好的融合。
這也就是為何多數非同步的架構(framework, ex:tornado, twisted)實作特殊的 API 才能使用他們的非同步特性。
Bottle 是一種 WSGI 架構,也就有 WSGI 的同步本性,所幸有超讚的 「gevent 專案」,bottle 仍有可能寫出非同步應用。
本文就在說明擁有非同步 WSGI 的 bottle 的使用。
#同步 WSGI 的限制
簡單來說,WSGI 規範(pep 3333) 定義一個 request/response 的循環,如後:
對於每個 request,應用程式可呼叫點被呼叫一次,就要「必須」回傳一個內容列舉。
有了那個列舉,server 就會用它來逐步取得內容,然後一次一次寫到 chunk 進 socket。
當該列舉到盡頭,client 連結就被關掉。
足夠簡單,但有個小問題,這就是所謂的同步呼叫。如果你的應用需要等待一些資料(IO, sockets, databases,...),這就只有兩個選擇,要嘛先 yeild 一個空字串(empty string),再不就卡住當前的執行緒(thread)。以上兩選擇,都將佔住處理的執行緒,造成無法回應新的 request。這就造成一個執行緒同一時間只能處理一個 request。
多數 server 會限制執行緒數目避免太多的負荷,一個執行緒池(pools)大多是設定執行緒數目在 20 以下。只要所有的執行緒都有事做,任何新的連線都會卡住。此時對其他人而言跟死掉沒兩樣。如果,你打算實作一個聊天室,採用 long-polling ajax request 以取得即時的更新,你很容易就碰到 20 個同時連線的限制。這只是個非常小的聊天室而已。
#救援投手上場 greenlet
多數 server 限制 worker pools 擁有的執行緒數目在一個相對低檔的數字,是因為執行緒切換與產生新執行緒的代價頗高。與fork 行程(process)相比,執行緒比較便宜沒錯,但為每個連線建一個新的執行緒還是滿貴的。
gevent 模組加入 greenlet 這個東西。Greenlet 與傳統的執行緒行為一樣,但建立 greenlet 很便宜。一個採用 gevent 的 server,可以產生出數千個 greenlet,一對一處理每個連線,而且幾乎沒有額外代價。任何一個 greenlet 卡住,不會影響 server 接受新 request 的能力。可同時間處理的連結數幾乎是無限制。
這讓建立一個非同步應用不可思議的簡單,因為他看起來就跟同步的應用一樣。一個 gevent 的 server,實際上不是非同步,它行為像是超多執行緒。以下是範例:
from gevent import monkey; monkey.patch_all()
from time import sleep
from bottle import route, run
@route('/stream')
def stream():
yield 'START'
sleep(3)
yield 'MIDDLE'
sleep(5)
yield 'END'
run(host='0.0.0.0', port=8080, server='gevent')
第一行,超重要。它讓 gevent 把 python 多數的阻塞式 API 變成不會阻塞現用執行緒,而是讓 CPU 去處理下一個 greenlet。它實際上是用 gevent 的 pseudo-thread 取代 python 的 threading 模組。因此,你仍可以用 time.sleep() 來卡住整個執行緒。如果你對於取代 python 內建模組不舒服,那可以使用對應的 gevent 函式(在此範例是 gevent.sleep())
如果,你執行這個腳本,然後用瀏覽器指到 http://localhost:8080/stream,你應該可以看到 START, MIDDLE, END 一個接一個的出現(而不是等 8 秒後一次看見它們)。這行為與使用一般執行緒是一樣的,但是你的 server 可以同時處理數千個 request 也不會有問題。
#事件 callback
非同步架構中(包含 tornado, twisted, node.js),一種常見的設計模式是使用非阻塞 API,綁一個 callback 到非同步事件。像是 sccket 物件是持續開啟,直到明確被關閉,也允許稍後有資料時呼叫 callback 寫入到 socket。這裡有個 tornado library 的範例:
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
worker = SomeAsyncWorker()
worker.on_data(lambda chunk: self.write(chunk))
worker.on_finish(lambda: self.finish())
主要的好處是,request 的 handler 可早點被結束掉。該執行緒就有空可處理新 request,當 callback 被呼叫時依然能把前一個 request 的資料寫到 socket 去。這些架構就能以小量作業系統的執行緒同時處理許大量 request。
使用 gevent + WSGI 的時候,事情就不是這樣了。首先,提早結束這件事沒有益處,因為我們擁有無限制的(pseudo)執行緒池可以接收新的 request。第二,我們也不能提早結束,因為這會讓 socket 被關掉(這是被 WSGI 要求的)。第三,我們必須回傳一個列舉以符合 WSGI。
為了要符合 WSGI 標準,我們能做的,就是回傳內容的列舉,而該列舉是可以非同步的寫入。有了 gevent.queue 的幫助,我們能「模擬」一個斷開的 socket,然後把前面的範例改寫如下:
@route('/fetch')
def fetch():
body = gevent.queue.Queue()
worker = SomeAsyncWorker()
worker.on_data(body.put)
worker.on_finish(lambda: body.put(StopIteration))
worker.start()
return body
從 server 的角度來看,這個 queue 物件是可列舉的。它會阻塞住,如果它沒有內容,而當他遇到 StopIteration 的時候會馬上停止。這符合 WSGI。在應用程式這邊,queue 物件表現就跟個非阻塞的 socket 一樣。你可以在任何時候寫入它,傳遞它,甚至開一個 pseudo 執行緒以非同步方式寫入它。這就是多數 long-polling 的實作方法。
#最後一招 websocket
讓我們暫時忘記底層的細節,來談談 WebSocket。既然你在讀這文章,或許你知道 websocket 是什麼。它是一個雙向溝通的通道,存在於瀏覽器(client)與網路應用程式(server)之間。感謝 gevent-websocket 套件,已經把難的工作都做完了。這裡有個簡單的 websocket 端點,它可以接受訊息然後傳回去給 client:
from bottle import request, Bottle, abort
app = Bottle()
@app.route('/websocket')
def handle_websocket():
wsock = request.environ.get('wsgi.websocket')
if not wsock:
abort(400, 'Expected WebSocket request.')
while True:
try:
message = wsock.receive()
wsock.send("Your message was: %r" % message)
except WebSocketError:
break
from gevent.pywsgi import WSGIServer
from geventwebsocket import WebSocketError
from geventwebsocket.handler import WebSocketHandler
server = WSGIServer(("0.0.0.0", 8080), app,
handler_class=WebSocketHandler)
server.serve_forever()
while-loop 一直執行到 client 關掉連線。你知道的 :)
client 端的 javascript API 也很直接:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
var ws = new WebSocket("ws://example.com:8080/websocket");
ws.onopen = function() {
ws.send("Hello, world");
};
ws.onmessage = function (evt) {
alert(evt.data);
};
</script>
</head>
</html>
原文:
http://bottlepy.org/docs/dev/async.html