Web開發系列(四):Flask, Tornado和WSGI
如上篇 所講,作為一個web伺服器, 我們需要建立socket連線,並且監聽在指定埠,當請求來到時我們要解析請求的內容,做出判斷,給出響應, 然後關閉連線,進行下一個服務。
可是誰也不願意需要做web開發的時候都從頭開始,然後每次都處理這麼多事情,那簡直是太麻煩了。於是便有了框架, 今天我們講兩個框架,Flask,Tornado。
Flask
Flask似乎很受歡迎,emm。。。實際上我目前所在公司和上一家都是用Flask,不過我個人並不喜歡Flask的設計,最討厭兩點:
- proxy
- 藉助proxy提供偽全域性變數
那麼,怎麼寫一個簡單的Flask應用呢?在開始之前我們先來講講一個web框架大概需要哪些東西。
首先我們需要一個路由器,不是發射Wi-Fi的那個路由器,而是將URL裡,不同的URL指向不同的函式或者其他能處理並且作出響應的 東西,我們叫做路由器,或者叫路由。此外我們需要一個東西,包含一些預設的配置,一般情況下都會叫做 "app",一般都會把router 放在app裡。這樣我們就可以做出一個簡單的web框架。
但是框架之所以叫做框架,是因為它規定了一系列流程,定義好了一系列介面,應用程式員只需要按照給定的介面寫出符合介面的程式碼, 便可以做出web服務來。比如吧,我們決定我們的web框架有這樣一系列動作:
def before_request(app, request): pass def handle_request(app, request): pass def after_request(app, request): pass
我們的應用將會從上至下依次呼叫函式,那麼我們只要實現具體的函式,便可以完成指定的功能。
我們來看一個簡單的Flask示例,來自官網:
from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World!" if __name__ == "__main__": app.run()
儲存並執行,就可以了。Flask的核心之一在於@app.route
這個裝飾器,他有一個概念叫做Blueprint,是什麼呢?就是把一夥URL
集結在一起,比如,凡是/api/say/v1
開頭的URL都放在say_bp_v1
下,那麼便可以這樣使用:
@say_bp_v1.route("/hello") def foo(): pass @say_bp_v1.route("/world") def bar(): pass
其作用吧。。。其實是可以少些很多重複的程式碼,差不多就這樣。我們來看看@app.route
的原始碼:
def route(self, rule, **options): def decorator(f): endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator
所以我們應該追下去看ofollow,noindex" target="_blank">add_url_rule ,這裡我們暫不繼續 展開。
我們再看一下Flask中最最核心的東西,proxy:
https://github.com/pallets/flask/blob/master/flask/globals.py#L14
繼續追到Werkzeug的程式碼中看LocalProxy
和LocalStack
:
class LocalStack(object): def __init__(self): self._local = Local() def __release_local__(self): self._local.__release_local__() def _get__ident_func__(self): return self._local.__ident_func__ def _set__ident_func__(self, value): object.__setattr__(self._local, '__ident_func__', value) __ident_func__ = property(_get__ident_func__, _set__ident_func__) del _get__ident_func__, _set__ident_func__ def __call__(self): def _lookup(): rv = self.top if rv is None: raise RuntimeError('object unbound') return rv return LocalProxy(_lookup) def push(self, obj): """Pushes a new item to the stack""" rv = getattr(self._local, 'stack', None) if rv is None: self._local.stack = rv = [] rv.append(obj) return rv def pop(self): """Removes the topmost item from the stack, will return the old value or `None` if the stack was already empty. """ stack = getattr(self._local, 'stack', None) if stack is None: return None elif len(stack) == 1: release_local(self._local) return stack[-1] else: return stack.pop() @property def top(self): """The topmost item on the stack.If the stack is empty, `None` is returned. """ try: return self._local.stack[-1] except (AttributeError, IndexError): return None @implements_bool class LocalProxy(object): __slots__ = ('__local', '__dict__', '__name__', '__wrapped__') def __init__(self, local, name=None): object.__setattr__(self, '_LocalProxy__local', local) object.__setattr__(self, '__name__', name) if callable(local) and not hasattr(local, '__release_local__'): # "local" is a callable that is not an instance of Local or # LocalManager: mark it as a wrapped function. object.__setattr__(self, '__wrapped__', local) def _get_current_object(self): """Return the current object.This is useful if you want the real object behind the proxy at a time for performance reasons or because you want to pass the object into a different context. """ if not hasattr(self.__local, '__release_local__'): return self.__local() try: return getattr(self.__local, self.__name__) except AttributeError: raise RuntimeError('no object bound to %s' % self.__name__) @property def __dict__(self): try: return self._get_current_object().__dict__ except RuntimeError: raise AttributeError('__dict__') # 略略略
可以看出(當然,肯定不是這樣隨隨便便看兩眼,其實Flask的原始碼還是可以研究研究的),Flask的本質就是,如果你執行以下程式碼:
from flask import request @app.route("/") def foo(): print(request.args.get("hello"))
request本來是一個匯入的object,但實際上從中獲取屬性或者值時,會從棧頂的ctx裡,再取出來,所以他是個代理,哎,不多說了, 等你看過Flask原始碼之後你就知道這個設計有多麼不科學了(雖然很多人都似乎比較喜歡Flask。。。)。
對Flask有興趣的可以看看我的這篇部落格:https://jiajunhuang.com/articles/2016_09_15-flask_source_code.rst.html
Tornado
Tornado我還是比較喜歡,可惜除了web框架之外,資料庫或者其他幾乎都是阻塞的。Tornado與Flask的函式形式的寫法不一樣,Tornado 屬於class形式的寫法,我認為這個設計比較科學,舉個例子:
import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") def post(self): self.write("post :)") def make_app(): return tornado.web.Application([ (r"/", MainHandler), ]) if __name__ == "__main__": app = make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()
於是乎,對同一個URL的get,post,put等請求,我們都可以只要實現對應方法便可以,有人要說了,Flask也有class based view,emm, 是有,你去看看原始碼看看你會想用嗎?
Tornado比我們最上面所說的框架所需要的東西還多了什麼呢?還多了一個IOLoop,I/O多路複用,此外借助yield,用同步的方式寫非同步程式碼, 當然,前提是帶上病毒式傳播的decorator----只要想寫非阻塞程式碼,那麼這個decorator便一加到底。
關於yield是如何把本來回調式的程式碼連線起來程式設計同步式的程式碼,可以看這篇部落格:https://jiajunhuang.com/articles/2016_11_29-python_yield.md.html
此外我寫了一個類似的Tornado的程式碼,基於Cython和UVLoop:https://github.com/jiajunhuang/storm 有興趣的話可以看看。
既然Tornado這麼好用,效能又高,為什麼好像還沒有Flask受歡迎呢?因為Web開發雖然看起來就是分析一下請求,給一下響應,但是遠 不是這麼簡單,還需要和資料庫打交道,Tornado自身可以寫出非阻塞的程式碼,但是連資料庫,想用ORM的時候卻不行,所以也不是特別方便。
因此很多人選擇使用Flask或者是Django,然後Gunicorn擋在前面,加上Gevent加持,於是又可以愉快的用寫同步的方式寫非同步。說起Gunicorn, 我們就得說說WSGI了。
WSGI
WSGI全名Web Server Gateway Interface,Python界web框架百家爭鳴,怎麼統一一下呢?於是便有了WSGI這種,定義介面,而非定義實現的方式。 具體需要看看這裡:https://www.python.org/dev/peps/pep-3333/#specification-details 實現了對應的介面,便可以接入針對WSGI的 應用例如Gunicorn。
講完,收工 :)