顯示具有 tornado 標籤的文章。 顯示所有文章
顯示具有 tornado 標籤的文章。 顯示所有文章

2014/04/05

Coroutine in Tornado Web Framework

Coroutine 可以讓我們在程式中按照自己的意思去安排執行順序,有點像是 jump 的概念,它允許短暫離開 function 並且保留 local variable 的狀態,等到某個時間點再跳回來,從上一次離開的地方繼續。第一次接觸到 coroutine 的概念是從 python,coroutine 是一種語言特性,從 wiki 可以看到很多語言都有這種特性。

那我們沒事幹嘛讓程式跳來跳去的?思考一種狀況,當在 single thread 下,你執行到一個 blocking function,這時候如果讓 CPU 去做其他事情是不是很好,等到 I/O 有回應的,再跳回來原本的地方繼續執行。等等,這不就是 event-driven 的 programming 嗎?它們的表達方式還是有點區別。


舉一個例子,我們發出一個 HTTP request 去抓 Yahoo weather 的資訊,然後利用 XML parser 從回應得資料中取出溫度,以下是利用一般非同步的方式去撰寫:
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler, asynchronous
from tornado.httpclient import AsyncHTTPClient
from xml.dom import minidom

class MainHandler(RequestHandler):
    url = "http://weather.yahooapis.com/forecastrss?w=2306179&u=c"

    @asynchronous
    def get(self):
        http_client = AsyncHTTPClient()
        http_client.fetch(self.url, callback=self._on_fetch)

    def _on_fetch(self, response):
        degree = self._parse_xml(response.body)
        self.finish("Taipei: %d" % degree)

    def _parse_xml(self, xml):
        xml_doc = minidom.parseString(xml)
        weather_list = xml_doc.getElementsByTagName('yweather:condition')
        degree = float(weather_list[0].attributes['temp'].value)
        return degree

if __name__ == "__main__":
    application = Application([
        (r"/", MainHandler),
    ])
    application.listen(8888)
    IOLoop.instance().start()
第 12 行:在發出 request 同時指定 callback
第 14 行:在收到 server 回應後,執行 _on_fetch()


換成 coroutine 的方式
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler, asynchronous
from tornado.httpclient import AsyncHTTPClient
import tornado.gen as gen
from xml.dom import minidom

class MainHandler(RequestHandler):
    url = "http://weather.yahooapis.com/forecastrss?w=2306179&u=c"

    @gen.coroutine
    def get(self):
        http_client = AsyncHTTPClient()
        response = yield http_client.fetch(self.url)
        degree = self._parse_xml(response.body)
        self.finish("Taipei: %d" % degree)

    def _parse_xml(self, xml):
        xml_doc = minidom.parseString(xml)
        weather_list = xml_doc.getElementsByTagName('yweather:condition')
        degree = float(weather_list[0].attributes['temp'].value)
        return degree

if __name__ == "__main__":
    application = Application([
        (r"/", MainHandler),
    ])
    application.listen(8888)
    IOLoop.instance().start()
第 13 行:程式執行完 yield 後面的 statement 這個 function 就會立刻 return,直到 tornado io loop 收到 server 回應,然後跳回第 13 行,把 fetch() 的結果 assign 給 response,然後繼續執行下去。
底層一樣是非同步I/O,但這種表達方式擁有在寫同步 I/O 般的直覺。


單一個 callback 可能顯示不出直覺在哪裡,如果連存取 database/memcach... 任何跟I/O相關的事情都採用非同步方式,那就會需要在 callback 中執行另一個 callback
class MainHandler(RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        req1(argument1, callback=self._res1)

    @tornado.web.asynchronous
    def _res1(self, response1):
        ...do something with response
        req2(argument2, callback=self._res2)

    def _res2(self, response2):
        ...do something with response
        self.finish("result...")

改用 coroutine 的方式
class MainHandler(RequestHandler):    
    @tornado.gen.coroutine
    def get(self):
        response1 = yield req1(argument1)
        ...do something with response1
        response2 = yield req2(argument2)
        ...do something with response2
        self.finish("result...")

是不是直覺很多!

2013/04/23

WebSocket on Tornado

Tornado 一直是我愛用的 python web framework,這幾年工作上一直在寫 event-drivent socket programming,所以被它 non-blocking 的特性給深深吸引,如果每件 block 的工作都能用 asynchronous 的方式來解決,那程式執行的效率一定超好!


延續『WebSocket 雙向即時通訊 - 初探』,今天就以 Tornado 來寫個簡單的 WebSocket 範例。當 client 連到 server 後,會傳送 hello ,而 server 回應一樣的訊息,傳送到第五次,server 就把 client 斷掉。

Server (ws_server.py)
import tornado.ioloop
import tornado.web
from tornado import websocket


class WSHandler(websocket.WebSocketHandler):
    count = 0
    def open(self):
        print "Client connected"

    def on_message(self, msg):
        print "[%d] Got message [%s] from client" % (self.count, msg)
        if self.count == 4:
            self.close()
            return
        # Write back message
        self.write_message("[%d] Echo: %s" % (self.count, msg))
        self.count += 1

    def on_close(self):
        print "Client disconnected"


application = tornado.web.Application([
    (r"/", WSHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()



Client (ws_client.py)
from tornado.websocket import websocket_connect
from tornado.ioloop import IOLoop


class ws_client():
    conn = None # This is WebSocketClientConnection
    def __init__(self):
        websocket_connect('ws://localhost:8888/', callback=self.conn_cb)
        
    def conn_cb(self, future):
        self.conn = future.result()
        self.conn.write_message("hello")
        self.conn.read_message(self.read_cb)
        
    def read_cb(self, future):
        msg = future.result()
        if msg is None:
            print "Server disconnected"
            IOLoop.instance().stop()
        else:
            print msg
            self.conn.write_message("hello")
            self.conn.read_message(self.read_cb)

if __name__ == '__main__':
    ws_client()
    IOLoop.instance().start()

2012/03/04

實現網頁即時傳訊 Web instant message (IM)

最近想弄一個Web IM,在web上做即時聊天有一定的困難,因為受限在http協定原生的特性:
client:request -> server:response -> 結束
傳輸一定要從client開始,而且一來一往誰也不能多,想做到server push有一定的難度,不像tcp raw socket自由度那麼高。


早期的聊天室會使用polling的方式定期 refresh (post back) 來更新頁面,同步的時間取決於refresh的頻率,但server loading也隨著頻率上升而增加。後來有了Ajax,頁面更新不用再一直閃爍,但還是有原本耗資源的問題。

近期較廣泛被使用的是Ajax搭配long polling,做到省資源又可以與server同步的效果。
Polling就是client定期的發request,long polling就是定期發request,但server不馬上response,而是等到有需要的時候再response (例如有人傳新訊息給你時),通常browser timeout都蠻長的,所以這段等待的期間相對polling就很省資源。如果還不是很懂,可以參考這一篇:Browser 與 Server 持續同步的作法介紹。

要做到long polling這個效果需要server support,server必需要把request object先keep住,等到有需要的時候再抓出來回應給client。用Tornado來舉例,就是RequestHandler被call時,先把Handler存起來,等到時機到了再執行finish() function。


下面列出幾種IM的 model,我想實現的是最後一種 ── 跨平台而且server可以隨著loading scale-up。

2012/02/29

Tornado web framework + HTTPS

從官網得知 Tornado web framework 原本就有支援SSL協定
HTTPServer can serve SSL traffic with Python 2.6+ and OpenSSL. To make this server serve SSL traffic, send the ssl_options dictionary argument with the arguments required for the ssl.wrap_socket method, including “certfile” and “keyfile”:
    HTTPServer(applicaton, ssl_options={
        "certfile": os.path.join(data_dir, "mydomain.crt"),
        "keyfile": os.path.join(data_dir, "mydomain.key"),
    })

SSL Sample
import os
from tornado.httpserver import HTTPServer
from tornado.web import Application, RequestHandler
from tornado.ioloop import IOLoop

class MainHandler(RequestHandler):
    def get(self):
        self.write("Hello world!")

def main():
    application = Application([
        (r"/", MainHandler), 
    ])  

    http_server = HTTPServer(application,  
        ssl_options={
            "certfile": os.path.join(os.path.abspath("."), "test.crt"), 
            "keyfile": os.path.join(os.path.abspath("."), "test.key"), 
        }   
    )

    http_server.listen(8080)
    IOLoop.instance().start()

if __name__ == "__main__":
    main()

過程中需要ssl certificate 與 private key,可以用openssl產生,不過過程很繁雜,建議可以利用online tool:Create self-signed SSL certificates online

測試
開啓 http://localhost:8080 遇到錯誤:
ERROR:root:Exception in I/O handler for fd 3 Traceback (most recent call last): File "/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/tornado-2.0-py2.6.egg/tornado/ioloop.py", line 287, in start self._handlers[fd](fd, events) File "/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/tornado-2.0-py2.6.egg/tornado/stack_context.py", line 159, in wrapped callback(*args, **kwargs) File "/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/tornado-2.0-py2.6.egg/tornado/httpserver.py", line 294, in _handle_events except ssl.SSLError, err: AttributeError: 'module' object has no attribute 'SSLError' 

少了甚麼module嗎?
重裝openssl 0.9.8t -> 不行


安裝ssl package
# pip install ssl
ValueError: This extension should not be used with Python 2.6 or later (already built in), and has not been tested with Python 2.3.4 or earlier.
python 2.6以後的版本已經內建了


安裝pyopenssl package
# pip install pyopenssl
安裝成功,但結果還是一樣。看起來該有的都有了阿!


最後把deploy系統從mac os換到centos 5,一模一樣的步驟就...成功了!上面的問題至今未解...


如果看到這個warning,表示client的request是用http不是https,並不是server的問題唷!
WARNING:root:SSL Error on 7: [Errno 1] _ssl.c:490: error:1407609C:SSL routines:SSL23_GET_CLIENT_HELLO:http request

2011/09/22

Implement tornado session in memcached

示範將tornado的session實作在memcached裡面,我是參考這個blog:
玩蛇记-给tornado加上session支持 - 亚历山大同志 - 博客园

Requirement
  • Tornado 2.0
  • python-libmemcached 0.4
    • Pyrex 0.9.4+
    • libmemcached 0.52

邏輯


流程

2011/09/19

初學 Tornado web framework

最近需要架設一個線上平台,對於架設平台我實在沒什麼經驗,從前有開發過網站,會用asp.net、php自己刻東西,但時代不一樣了,為了講求快速,依附在framework上是有必要的。Tornado — 對我來說是個全新的開始,我不會python,對物件導向的程式語言也不熟,平常都在寫C語言居多,更別提用過web framework了...但我堅信我的底子跟熱情可以支撐我去學習這一切!

話說剛開始學習真的超不順,中文英文的教學都很少,官方的文件又寫的很簡略...但看著看著,突然有一天感覺就來了,就像任督二脈被打通般的,我看得懂了,我會使用了,程式寫這麼久了,我覺得對我幫助最大就是學會這種感覺。如果看懂Overview又學會使用Source code裡面的demo程式,對Tornado的掌握度就很高了!

API
Tornado真是個強大的東西,它陳列的API不多,但每一個都是在建立平台時很常見的需求:
  • OAuth第三方認證 - 使用facebook、google、twitter帳號來登入自己的網站,API都很精簡了
  • Python decorator tornado.web.authenticated - Sorry我不知道怎麼翻譯這個,簡單來說就是user尚未通過認證,存取到任何網頁都會被導回login頁面
  • Non-blocking, asynchronous http request - facebook graph API query、backend server query
  • Template - MVC架構開發,這是每個framewrok都必備的
  • Security cookie - 防止cookie被篡改
  • L10N, CSRF protect

IDE選擇