知乎後端主力框架Tornado入門體驗

老錢發表於2018-05-28

Tornado在知乎廣為使用,當你用Chrome開啟網頁版本的知乎,使用開發者工具仔細觀察Network裡面的請求,就會發現有一個特別的狀態碼為101的請求,它是用瀏覽器的websocket技術和後端伺服器建立了長連線用來接收伺服器主動推送過來的通知訊息。這裡的後端伺服器使用的就是tornado伺服器。Tornado伺服器除了可以提供websocket服務外,還可以提供長連線服務,HTTP短連結服務,UDP服務等。Tornado伺服器由facebook開源,在掌閱的後端也廣為使用。

這樣一個強大的Tornado框架,究竟該如何使用,本文將帶領讀者循序漸進深入學習tornado作為web伺服器的基礎使用。

Hello, World

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
複製程式碼

這是官方提供了Hello, World例項,執行python hello.py,開啟瀏覽器訪問http://localhost:8888/就可以看到伺服器的正常輸出Hello, world

一個普通的tornado web伺服器通常由四大元件組成。

  1. ioloop例項,它是全域性的tornado事件迴圈,是伺服器的引擎核心,示例中tornado.ioloop.IOLoop.current()就是預設的tornado ioloop例項。
  2. app例項,它代表著一個完成的後端app,它會掛接一個服務端套接字埠對外提供服務。一個ioloop例項裡面可以有多個app例項,示例中只有1個,實際上可以允許多個,不過一般幾乎不會使用多個。
  3. handler類,它代表著業務邏輯,我們進行服務端開發時就是編寫一堆一堆的handler用來服務客戶端請求。
  4. 路由表,它將指定的url規則和handler掛接起來,形成一個路由對映表。當請求到來時,根據請求的訪問url查詢路由對映表來找到相應的業務handler。

這四大元件的關係是,一個ioloop包含多個app(管理多個服務埠),一個app包含一個路由表,一個路由表包含多個handler。ioloop是服務的引擎核心,它是發動機,負責接收和響應客戶端請求,負責驅動業務handler的執行,負責伺服器內部定時任務的執行。

當一個請求到來時,ioloop讀取這個請求解包成一個http請求物件,找到該套接字上對應app的路由表,通過請求物件的url查詢路由表中掛接的handler,然後執行handler。handler方法執行後一般會返回一個物件,ioloop負責將物件包裝成http響應物件序列化傳送給客戶端。

知乎後端主力框架Tornado入門體驗

同一個ioloop例項執行在一個單執行緒環境下。

階乘服務

下面我們編寫一個正常的web伺服器,它將提供階乘服務。也就是幫我們計算n!的值。伺服器會提供階乘的快取,已經計算過的就存起來,下次就不用重新計算了。使用Python的好處就是,我們不用當心階乘的計算結果會溢位,Python的整數可以無限大。

# fact.py
import tornado.ioloop
import tornado.web


class FactorialService(object):  # 定義一個階乘服務物件

    def __init__(self):
        self.cache = {}   # 用字典記錄已經計算過的階乘

    def calc(self, n):
        if n in self.cache:  # 如果有直接返回
            return self.cache[n]
        s = 1
        for i in range(1, n):
            s *= i
        self.cache[n] = s  # 快取起來
        return s


class FactorialHandler(tornado.web.RequestHandler):

    service = FactorialService()  # new出階乘服務物件

    def get(self):
        n = int(self.get_argument("n"))  # 獲取url的引數值
        self.write(str(self.service.calc(n)))  # 使用階乘服務


def make_app():
    return tornado.web.Application([
        (r"/fact", FactorialHandler),  # 註冊路由
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
複製程式碼

執行python fact.py ,開啟瀏覽器,鍵入http://localhost:8888/fact?n=50,可以看到瀏覽器輸出了 608281864034267560872252163321295376887552831379210240000000000,如果我們不提供n引數,訪問http://localhost:8888/fact,可以看到瀏覽器輸出了400: Bad Request,告訴你請求錯誤,也就是引數少了一個。

使用Redis

上面的例子是將快取存在本地記憶體中,如果換一個埠再其一個階乘服務,通過這個新埠去訪問的話,對於每個n,它都需要重新計算一遍,因為本地記憶體是無法跨程式跨機器共享的。

所以這個例子,我們將使用Redis來快取計算結果,這樣就可以完全避免重複計算。另外我們將不在返回純文字,而是返回一個json,同時在響應裡增加欄位來說名本次計算來源於快取還是事實計算出來的。另外我們提供預設引數,如果客戶端沒有提供n,那就預設n=1。

import json
import redis
import tornado.ioloop
import tornado.web


class FactorialService(object):

    def __init__(self):
        self.cache = redis.StrictRedis("localhost", 6379)  # 快取換成redis了
        self.key = "factorials"

    def calc(self, n):
        s = self.cache.hget(self.key, str(n))  # 用hash結構儲存計算結果
        if s:
            return int(s), True
        s = 1
        for i in range(1, n):
            s *= i
        self.cache.hset(self.key, str(n), str(s))  # 儲存結果
        return s, False


class FactorialHandler(tornado.web.RequestHandler):

    service = FactorialService()

    def get(self):
        n = int(self.get_argument("n") or 1)  # 引數預設值
        fact, cached = self.service.calc(n)
        result = {
            "n": n,
            "fact": fact,
            "cached": cached
        }
        self.set_header("Content-Type", "application/json; charset=UTF-8")
        self.write(json.dumps(result))


def make_app():
    return tornado.web.Application([
        (r"/fact", FactorialHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
複製程式碼

當我們再次訪問http://localhost:8888/fact?n=50,可以看到瀏覽器輸出如下 {"cached": false, "fact": 608281864034267560872252163321295376887552831379210240000000000, "n": 50} ,再重新整理一下,瀏覽器輸出{"cached": true, "fact": 608281864034267560872252163321295376887552831379210240000000000, "n": 50},可以看到cached欄位由true程式設計了false,表明快取確實已經儲存了計算的結果。我們重啟一下程式, 再次訪問這個連線,觀察瀏覽器輸出,可以發現結果的cached依舊等於true。說明快取結果不再是存在本地記憶體中了。

圓周率計算服務

接下來我們再增加一個服務,計算圓周率,圓周率的計算公式有很多種,我們用它最簡單的。

知乎後端主力框架Tornado入門體驗

我們在服務裡提供一個引數n,作為圓周率的精度指標,n越大,圓周率計算越準確,同樣我們也將計算結果快取到Redis伺服器中,避免重複計算。

# pi.py
import json
import math
import redis
import tornado.ioloop
import tornado.web


class FactorialService(object):

    def __init__(self, cache):
        self.cache = cache
        self.key = "factorials"

    def calc(self, n):
        s = self.cache.hget(self.key, str(n))
        if s:
            return int(s), True
        s = 1
        for i in range(1, n):
            s *= i
        self.cache.hset(self.key, str(n), str(s))
        return s, False


class PiService(object):

    def __init__(self, cache):
        self.cache = cache
        self.key = "pis"

    def calc(self, n):
        s = self.cache.hget(self.key, str(n))
        if s:
            return float(s), True
        s = 0.0
        for i in range(n):
            s += 1.0/(2*i+1)/(2*i+1)
        s = math.sqrt(s*8)
        self.cache.hset(self.key, str(n), str(s))
        return s, False


class FactorialHandler(tornado.web.RequestHandler):

    def initialize(self, factorial):
        self.factorial = factorial

    def get(self):
        n = int(self.get_argument("n") or 1)
        fact, cached = self.factorial.calc(n)
        result = {
            "n": n,
            "fact": fact,
            "cached": cached
        }
        self.set_header("Content-Type", "application/json; charset=UTF-8")
        self.write(json.dumps(result))


class PiHandler(tornado.web.RequestHandler):

    def initialize(self, pi):
        self.pi = pi

    def get(self):
        n = int(self.get_argument("n") or 1)
        pi, cached = self.pi.calc(n)
        result = {
            "n": n,
            "pi": pi,
            "cached": cached
        }
        self.set_header("Content-Type", "application/json; charset=UTF-8")
        self.write(json.dumps(result))


def make_app():
    cache = redis.StrictRedis("localhost", 6379)
    factorial = FactorialService(cache)
    pi = PiService(cache)
    return tornado.web.Application([
        (r"/fact", FactorialHandler, {"factorial": factorial}),
        (r"/pi", PiHandler, {"pi": pi}),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
複製程式碼

因為兩個Handler都需要用到redis,所以我們將redis單獨抽出來,通過引數傳遞進去。另外Handler可以通過initialize函式傳遞引數,在註冊路由的時候提供一個字典就可以傳遞任意引數了,字典的key要和引數名稱對應。我們執行python pi.py,開啟瀏覽器訪問http://localhost:8888/pi?n=200,可以看到瀏覽器輸出{"cached": false, "pi": 3.1412743276, "n": 1000},這個值已經非常接近圓周率了。

知乎後端主力框架Tornado入門體驗

閱讀更多Python高階文章,關注公眾號「碼洞」

相關文章