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

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

這樣一個強大的Tornado框架,究竟該如何使用,本文將帶領讀者按部就班深刻學習tornado做爲web服務器的基礎使用。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, worldredis

一個普通的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的運行,負責服務器內部定時任務的執行。json

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

同一個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,告訴你請求錯誤,也就是參數少了一個。bash

使用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。說明緩存結果再也不是存在本地內存中了。

圓周率計算服務

接下來咱們再增長一個服務,計算圓周率,圓周率的計算公式有不少種,咱們用它最簡單的。

咱們在服務裏提供一個參數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},這個值已經很是接近圓周率了。

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

相關文章
相關標籤/搜索