Tornado學習

參考教程

為什么用Tornado?


異步編程原理

服務(wù)器同時要對許多客戶端提供服務(wù),他的性能至關(guān)重要采够。而服務(wù)器端的處理流程,只要遇到了I/O操作,往往需要長時間的等待。


屏幕快照 2018-10-31 上午11.43.02.png

當然,我們可以用多線程/多進程達到類似的目的雌续,但線程和進程都是系統(tǒng)控制的,消耗資源較多膝但,而且何時運行略贮,何時掛起不由程序本身做主拄丰,調(diào)度開銷較大。我們希望將多任務(wù)流程的調(diào)度工作方法放到自己的代碼里,精確的控制他的行蹤潦闲,與線程相似奕筐,我們稱這種任務(wù)流程為協(xié)程办成。
協(xié)程實在一個線程之內(nèi),無需操作系統(tǒng)參與奸晴,由程序自身調(diào)度的執(zhí)行單位走诞。
按照上述模式塞绿,在一個進程之內(nèi)同時處理多個協(xié)程,充分利用CPU時間射沟,就是我們需要的異步編程。

底層依賴epoll

在Linux下,底層對一個耗時操作(如網(wǎng)絡(luò)訪問)的處理流程為:
發(fā)起訪問括改,將網(wǎng)絡(luò)連接的文件描述符和期待事件注冊到epoll里。當期待事件發(fā)生家坎,epoll觸發(fā)事件處理機制嘱能,通過回調(diào)函數(shù)通知tornado,tornado切換協(xié)程虱疏。

用生成器實現(xiàn)協(xié)程

要實現(xiàn)上述異步的操作惹骂,必須判斷兩個點:
1.等待開始----協(xié)程走到這里,去epoll注冊做瞪,將CPU的控制權(quán)讓給別的協(xié)程对粪。
2.等待結(jié)束----協(xié)程走到這里,接到epoll回調(diào)装蓬,開始請求CPU的控制權(quán)衩侥,執(zhí)行后續(xù)操作。
在Tornado 里矛物,用一個yield語句來表示這兩個點。程序開始等待時跪但,向外yield一個Future對象履羞,知道等待結(jié)束才回來執(zhí)行yield的下一條語句峦萎。

Tornado異步HTTP服務(wù)器


Tornado最大用途當然是HTTP服務(wù)器

異步HTTP服務(wù)器

#http_0.py
from tornado.ioloop import IOLoop
from tornado import gen,web

class ExampleHandler(web.RequestHandler):
    @gen.coroutine
    def get(self):
        delay = self.get_argument('delay', 5)
        yield gen.sleep(int(delay))
        self.write({"status":1, "msg":"success"})
        self.finish()
    # @gen.coroutine
    # def post(self):
    # pass
application = web.Application([
    (r"/example", ExampleHandler), 
    #(r"/other", OtherHandler),
    ],autoreload = True)
application.listen(8765)
IOLoop.current().start()

運行以上代碼,用瀏覽器訪問http://localhost:8765/example?delay=1
即可在延遲1秒之后得到返回結(jié)果{
"status": 1,
"msg": "success"
}忆首。
delay傳入是幾就延遲幾秒爱榔,如果沒有傳入,默認值為5糙及。
在這個例子里详幽,我們用gen.sleep實現(xiàn)延遲。gen.sleep與time.sleep的用法相似浸锨,區(qū)別在于它是異步的唇聘,只阻塞當前協(xié)程,等待一段時間柱搜,但不影響同一進程內(nèi)的其他協(xié)程的執(zhí)行迟郎。
可以認為gen.sleep是time.sleep的一個異步版本。在tornado開發(fā)中聪蘸,我們經(jīng)常需要使用某個內(nèi)含等待的函數(shù)的異步版本宪肖,以免同步的等待阻塞住整個進程。這些異步的操作都返回Future對象健爬。

檢驗他能否并發(fā)服務(wù)

打開兩個瀏覽器(不要使用同一個瀏覽器的兩個標簽頁)控乾,各自在地址欄中輸入http://localhost:8765/example?delay=10
盡可能快的在兩個瀏覽器里先后敲回車加載頁面,你會發(fā)現(xiàn)娜遵,兩個頁面都是在你開始加載的10秒之后返回蜕衡,兩次加載的總用時是10秒稍多,遠不到20秒魔熏,這說明我們的事例程序雖然只有一個進程衷咽,一個線程,卻能在第一個請求完成之前開始處理第二個請求蒜绽。

對代碼的詳細解說

tornado與webapp一樣镶骗,用一個繼承于web.RequestHandler的類構(gòu)造處理HTTP訪問的handler,不同于Django躲雅,flask鼎姊,bottle等用函數(shù)構(gòu)造handler。在這個類里相赁,我們重載get方法來處理GET請求相寇,也可以重載post, put,delete等其他HTTP方法。
你大概注意到了钮科,重載的方法前都加了@gen.coroutine這樣一個裝飾器唤衫,他的作用就是把一個普通的函數(shù)變成一個返回Future對象的函數(shù),即異步函數(shù)绵脯。
異步函數(shù)一定要用yield調(diào)用佳励,而且只有在另一個異步函數(shù)之內(nèi)的調(diào)用才能起作用休里。
用self.write輸出執(zhí)行結(jié)果。如果輸出的是一個字典赃承,tornado會自動把它變成JSON串
正常結(jié)束的HTTP調(diào)用須以self.finish()結(jié)束妙黍。但如果是渲染模版或者跳轉(zhuǎn)到其他網(wǎng)站,不應(yīng)該添加self.finish().
接下來要構(gòu)造Application對象瞧剖,然后監(jiān)聽端口拭嫁,啟動消息循環(huán),服務(wù)器就能運行起來了抓于。

創(chuàng)建Appliaction對象時可設(shè)置這些參數(shù)

autoreload:若設(shè)為True做粤,在程序運行起來之后,每次編輯代碼并保存時毡咏,可以自動重新運行
debug:若設(shè)制為true驮宴,會把運行出錯信息打印在屏幕上。
cookie_secret:用于cookie加密的密鑰呕缭,形如:“ofjf939.m%dw$#3fdn923hrfsp309-[2”堵泽。
static_path:靜態(tài)文件路徑。如果與當前文件同一路徑恢总,可設(shè)為:os.path.dirname(file).
xsrf_cookies:xsrf檢測

多進程服務(wù)器

前面討論過迎罗,一個典型的單進程,單線程片仿。需訪問數(shù)據(jù)庫的異步Tornado服務(wù)器每秒可以響應(yīng)500個請求纹安,在較低配置服務(wù)器上實際的負載能力大概也是這個數(shù)字。如果需要更高的負載能力砂豌,且服務(wù)器有多個CPU厢岂,應(yīng)開啟多個服務(wù)進程。
只要將

application.listen(8765)

改為

from tornado.httpserver import HTTPServer
server = HTTPServer(application)
server.bind(8765)
server.start(4)

就能同時啟動4個進程了阳距。
理論上講塔粒,在一個線程里也能運行多個協(xié)程,可以作出「多進程 × 多線程 × 多協(xié)程」的模式筐摘。而實際上卒茬,協(xié)程可以完全代替線程,「多進程 × 多協(xié)程」已經(jīng)能夠充分利用服務(wù)器的硬件資源咖熟。

流式響應(yīng)的HTTP服務(wù)器

如果響應(yīng)數(shù)據(jù)較大圃酵,為了節(jié)約內(nèi)存,或者是各部分數(shù)據(jù)的返回要有一個時間差馍管,我們需要將數(shù)據(jù)分成多次發(fā)送郭赐。

#http_stream.py
from tornado.ioloop import IOLoop
from tornado import gen,web

class ExampleHandler(web.RequestHandler):
    @gen.coroutine
    def get(self):
        for _ in range(5):
            yield gen.sleep(1)
            self.write('zzzzzzzzzzzz<br>')
            self.flush()
        self.finish()

application = web.Application([
    (r"/example", ExampleHandler), 
    ],autoreload = True)
application.listen(8765)
IOLoop.current().start()

演示及解說

與前面的http_0.py相比,明顯多了一行self.flush().他的作用是將此前由self.write()寫入緩沖區(qū)的內(nèi)容發(fā)送出去确沸。在self.finish()結(jié)束這次響應(yīng)之前,可以多次調(diào)用self.write和self.flush,逐步發(fā)送數(shù)據(jù).
運行這段代碼妓蛮,用瀏覽器訪問http://localhost:8765/example
可以看到頁面每隔一秒打印一行輸出。如果注釋掉self.flush()在運行,就會等到5秒時候才將5行輸出同時打印出來.

訪問數(shù)據(jù)庫


#http_db.py
from tornado.ioloop import IOLoop
from tornado import gen,web
from tornado_mysql import pools

connParam = {'host':'localhost', 'port':3306, 'user':'root', 'passwd':'123456', 'db':'testdb'}

class GetUserHandler(web.RequestHandler):
    POOL = pools.Pool(
        connParam, 
        max_idle_connections=1, 
        max_recycle_sec=3,
        )

    @gen.coroutine
    def get(self):
        userid = self.get_argument('id')
        cursor = yield self.POOL.execute('select name from user where id = %s', userid)

        if cursor.rowcount > 0:
            self.write( { "status": 1, "name": cursor.fetchone()[0] } )
        else:
            self.write( { "status": 0, "name": "" } )

        self.finish()

application = web.Application([
    (r"/getuser", GetUserHandler), 
    ],autoreload = True)
application.listen(8765)
IOLoop.current().start()

以上代碼從mysql庫中讀取數(shù)據(jù)叶洞。

演示

首先,安裝tornado-MySQL
然后帖池,在你的mysql中建立一個名為user的表年扩,至少有兩個字段:一個整數(shù)型的id,和一個字符串型的name。
編輯http_db.py雷激,將mysql的連接參數(shù)填入connParam中替蔬。運行http_db.py,用瀏覽器訪問:http://localhost:8765/getuser?id=1
如果你的user表中有id=1的數(shù)據(jù)屎暇,他的name字段是jim,你將看到返回值:{
"status": 1,
"name": "jim"
}
如果沒有找到數(shù)據(jù)承桥,你將看到返回值:{
"status": 0,
"name": ""
}

需要ORM?

很多人喜歡通過ORM訪問數(shù)據(jù)庫根悼,而Tornado沒有提供異步的ORM工具凶异。
這是訪問mysql的例子,如果你要使用postgresql或其他數(shù)據(jù)庫挤巡,也需要先安裝這些數(shù)據(jù)庫的tornado接口剩彬,就像tornado_mysql一樣。

訪問網(wǎng)路


http_req.py
from tornado.ioloop import IOLoop
from tornado import gen,web
from tornado.httpclient import AsyncHTTPClient

url = 'http://hq.sinajs.cn/list=sz000001'

class GetPageHandler(web.RequestHandler):
    @gen.coroutine
    def get(self):
        client = AsyncHTTPClient()
        response = yield client.fetch(url, method='GET')
        self.write(response.body.decode('gbk'))
        self.finish()

application = web.Application([
    (r"/getpage", GetPageHandler), 
    ],autoreload = True)
application.listen(8765)
IOLoop.current().start()

演示

運行http_req.py矿卑,用瀏覽器訪問:http://localhost:8765/getpage,即可看到從http://hq.sinajs.cn/list=sz000001抓取的內(nèi)容喉恋。

解說

Tornado有一個AsyncHTTPClient用于訪問其他網(wǎng)頁,用法比較簡單母廷。他在fetch方法中等待遠端網(wǎng)頁返回內(nèi)容轻黑,因此也要以yield調(diào)用。

tornado用戶認證


#http_auth.py
from tornado.ioloop import IOLoop
from tornado import gen,web

class LoginHandler(web.RequestHandler):
    @gen.coroutine
    def get(self):
        self.set_secure_cookie('username', 'Jim')
        self.write('login ok.')
        self.finish()

class LogoutHandler(web.RequestHandler):
    @gen.coroutine
    def get(self):
        self.clear_cookie('username')
        self.write('logout ok.')
        self.finish()

class WhoHandler(web.RequestHandler):
    def get_current_user(self):
        if self.get_secure_cookie('username'):
            return str(self.get_secure_cookie('username'), encoding = "utf-8")
        else:
            return 'unknown'
    @gen.coroutine
    def get(self):
        self.write('you are ' + self.current_user)
        self.finish()
        

application = web.Application([
    (r"/login", LoginHandler), 
    (r"/logout", LogoutHandler), 
    (r"/whoami", WhoHandler), 
    ],autoreload = True,
    cookie_secret="feljjfesrh48thfe2qrf3np2zl90bmw")
application.listen(8765)
IOLoop.current().start()

演示

1.運行http_auth.py琴昆,用瀏覽器訪問http://localhost:8765/whoami氓鄙,可以看到輸出:you are unknown,說明還沒有登錄
2.訪問http://localhost:8765/login,看到:login ok.登錄成功椎咧。在訪問http://localhost:8765/whoami玖详,這是會看到: you are Jim,說明你已經(jīng)以Jim身份登錄了勤讽。
3.訪問http://localhost:8765/logout蟋座,看到:logout ok,退出登錄脚牍,在訪問http://localhost:8765/whoami向臀,又變回了:you are unknown,說明你已經(jīng)退出了登錄。

解說

你知道服務(wù)端通常用 cookie 保存用戶的身份信息诸狭。login 時創(chuàng)建 cookie券膀;logout 時清除 cookie君纫;需要檢查用戶身份時,讀取 cookie芹彬。
在 tornado 里蓄髓,創(chuàng)建 cookie 通常用 set_secure_cookie,這樣創(chuàng)建的 cookie 是加密的舒帮。與之對應(yīng)的讀取加密 cookie 的方法是 get_secure_cookie会喝。
為了給 cookie 加密,要在創(chuàng)建 Application 時添加 cookie_secret 屬性玩郊,這是加密的密鑰肢执,它的值用一串亂寫的字符就行了。
清除 cookie 用 clear_cookie译红。
在 RequestHandler 里有一個 get_current_user 方法预茄,它會在 get / post 之前調(diào)用,其返回值會賦予 current_user 屬性侦厚。我們在 WhoHandler 里重載了 get_current_user耻陕,后面在 get 里就能直接使用 self.current_user 了。

tornado定時任務(wù)


定時任務(wù)分兩種假夺,一種是每隔一定的時間周期性地執(zhí)行淮蜈,另一種是在某個鐘點單次執(zhí)行。

周期性定時任務(wù)

#cron_0.py
from tornado import ioloop, gen

@gen.coroutine
def Count():
    print("1 second has gone.")

if __name__ == '__main__':
    ioloop.PeriodicCallback(Count, 1000).start()
    ioloop.IOLoop.current().start()

演示及解說

在啟動消息循環(huán)之前已卷,用PeriodicCallback設(shè)定每1000毫秒執(zhí)行一次異步函數(shù)Count梧田。直接運行,每過一秒會打印一行:1 second has gone.

單次定時任務(wù)

#cron_1.py
from tornado import ioloop, gen
from time import time

@gen.coroutine
def Ring():
    print('it\'s time to get up')

if __name__ == '__main__':
    loop = ioloop.IOLoop.current()
    loop.call_at(time() + 5, Ring)
    loop.start()

演示及解說

在啟動消息循環(huán)之前侧蘸,用call_at設(shè)定5秒后執(zhí)行一次異步函數(shù)Ring裁眯。如果想在明早9點執(zhí)行,需要輸入明早9點的unix時間戳讳癌。
直接運行上述代碼穿稳,5秒之后會打印一行:it's time to get up.僅此一次。
如果要在一個相對的時間(例如五秒鐘后)而不是一個絕對時間(例如八點整)運行定時任務(wù)晌坤,用 call_later 會比 call_at 更簡單一點逢艘。可以將

loop.call_at(time() + 5, Ring)

改為

loop.call_later(5,Ring)

執(zhí)行效果是一樣的骤菠。

單元測試


#test.py
from tornado.testing import gen_test, AsyncTestCase
from tornado.httpclient import AsyncHTTPClient
import unittest

class MyAsyncTest(AsyncTestCase):
    @gen_test
    def test_xx(self):
        client = AsyncHTTPClient(self.io_loop)
        path = 'http://localhost:8765/example?delay=2'
        responses = yield [client.fetch(path, method = 'GET') for _ in range(10)]
        for response in responses:
            print(response.body)

if __name__ == '__main__':
    unittest.main()

演示

1.先運行http_0.py它改,保證通過瀏覽器訪問http://localhost:8765/example?delay=2能得到結(jié)果
2.運行test.py,看到如下輸出

屏幕快照 2018-11-01 上午9.52.49.png

可見商乎,測試模塊在2.027s之內(nèi)完成了10次請求央拖,每個請求都要延遲2秒返回,因此,這10個請求必定是并行執(zhí)行的鲜戒。

解說

與普通的 unittest 用法相似专控,先定義好基于 AsyncTestCase 的測試類,在執(zhí)行 unittest.main()時遏餐,測試類中所有 以 test 開頭的方法都會執(zhí)行伦腐。
注意,測試類中的方法要以 gen_test 裝飾失都。
每次測試整體的用時不能超過 5 秒蔗牡,超則報錯。

tornado異步TCP連接


tornado有TCPClient和TCPServer兩個類嗅剖,可用于實現(xiàn)tcp的客戶端和服務(wù)端。事實上嘁扼,這兩個類都是對iostream的簡單包裝信粮。

真正重要的是iostream

iostream是client與server之間的tcp通道,被動等待創(chuàng)建iostream的一方是server趁啸,主動找對方創(chuàng)建的一方是client强缘。
在iostream創(chuàng)建之后,client與server的操作再無分別不傅,在任何時候都可以通過iostream.write向?qū)Ψ絺魉蛢?nèi)容旅掂,或者通過iostream.read_xx接受對方傳來的內(nèi)容,或者以iostream.close關(guān)閉連接访娶。

TCPServer

#tcp_server.py
from tornado import ioloop,gen,iostream
from tornado.tcpserver import TCPServer

class MyTcpServer(TCPServer):
    @gen.coroutine
    def handle_stream(self,stream,address):
        try:
            while True:
                msg = yield stream.read_bytes(20, partial = True)
                print(msg, 'from', address)
                yield stream.write(msg[::-1])
                if msg == 'over':
                    stream.close()
        except iostream.StreamClosedError:
            pass

if __name__ == '__main__':
    server = MyTcpServer()
    server.listen(8760)
    server.start()
    ioloop.IOLoop.current().start()

解說

創(chuàng)建一個繼承于TCPServer的類的實例商虐,監(jiān)聽端口,啟動服務(wù)器崖疤,啟動消息循環(huán)秘车,服務(wù)器開始運行。
這時劫哼,如果有client連接過來叮趴,tornado會創(chuàng)建一個iostream,然后調(diào)用handle_stream方法,調(diào)用時傳入的兩個參數(shù)是iostream和client的地址权烧。
我們示例的功能很簡單眯亦,每收到一段 20 字符以內(nèi)的內(nèi)容,將之反序回傳般码,如果收到 'over'妻率,就斷開連接。注意侈询,斷開連接不用 yield 調(diào)用舌涨。
無論是誰斷開連接,連接雙方都會各自觸發(fā)一個 StreamClosedError。

TCPClient

#tcp_client.py
from tornado import ioloop,gen,iostream
from tornado.tcpclient import TCPClient

@gen.coroutine
def Trans():
    straem = yield TCPClient().connect('localhost', 8760)
    try:
        for msg in ('zzxxc', 'abcde', 'i feel lucky', 'over'):
            yield straem.write(msg.encode('utf-8'))
            back = yield straem.read_bytes(20, partial = True)
            print(back)
    except iostream.StreamClosedError:
        pass

if __name__ == '__main__':
    ioloop.IOLoop.current().run_sync(Trans)

解說

使用TCPClient比TCPServer更簡單囊嘉,無需繼承温技,只要用connect方法連接到server,就會返回iostream對象了扭粱。
在本例中舵鳞,我們像server發(fā)送一些字符串,他都會反序發(fā)回來琢蛤,最后發(fā)個“over”蜓堕,讓sever斷開連接。
值得注意博其,這段代碼與之前的幾個例子有個根本的區(qū)別套才,之前都是服務(wù)器,被動等待行為發(fā)生慕淡,而這段代碼是一運行就主動發(fā)起行為(連接)背伴,因此它的運行方式不同于以往,需要我們主動通過 ioloop 的 run_sync 來調(diào)用峰髓。以往那些實例中的異步處理方法實際是由 Tornado 調(diào)用的傻寂。在 run_sync 里,tornado 會先啟動消息循環(huán)携兵,執(zhí)行目標函數(shù)疾掰,之后再結(jié)束消息循環(huán)。

演示

在第一個終端端口運行tcp_server.py,在第二個終端端口運行tcp_client.py徐紧,即可看到他們之間的交互和斷開的過程静檬。

tornado的ioloop消息循環(huán)


tornado的異步功能都是通過ioloop實現(xiàn)的。
前面的每一段示例代碼的最后一行都是啟動ioloop:

ioloop.IOLoop.current().start()

每個進程都有一個磨人的ioloop并级,雖然還可以有更多個巴柿,通常使用默認的就夠了。在上面的這行代碼里死遭,我們通過current()獲得當前的ioloop广恢,讓他start(),ioloop就會一直跑下去,讓他run_sync呀潭,就會跑起來钉迷,執(zhí)行目標函數(shù),執(zhí)行完就停止钠署。
一般編程中很少用到ioloop的其他功能糠聪,只要簡簡單單的處理好RequestHandler的get/post方法,調(diào)用tornado-Mysql的異步函數(shù)訪問數(shù)據(jù)庫谐鼎,返回結(jié)果就可以了舰蟆。
可是,如果有些內(nèi)含等待(CPU休息)的操作,找不到現(xiàn)成的異步庫身害,只要深入到這個操作的底層味悄,靠ioloop,可以把同步的操作變成異步塌鸯。

把同步的操作變成異步

內(nèi)含等待的操作大概有幾種情況:
1.為了拖時間而等待
對應(yīng)前面用過的 gen.sleep侍瑟,如果沒有 gen.sleep 呢?可以很簡單地通過也在前面用過的 call_at 或 call_later 來實現(xiàn)這個功能丙猬。
2.等待網(wǎng)絡(luò)返回
這一部分是本章的重點涨颜。我們將不再使用 iostream,而是在同步代碼的基礎(chǔ)上茧球,用 ioloop 自己實現(xiàn)一個與 iostream 同樣異步庭瑰、高效的 tcp client。

同步的代碼

#sync_client.py
import socket
from time import time

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 8760))
t0 = time()
for _ in range(1000):
    sock.send(b'test message.')
    sock.recv(99)

print('time cost', time() - t0)
sock.close()

演示及解說

這是一段十分普通的tcp client代碼抢埋,當然它是同步的见擦,連上服務(wù)器之后,發(fā)一條羹令,收一條,重復(fù)一千次损痰,看他耗時多少福侈。
服務(wù)器端就用我們前面用過的tcp_server.py,在運行sync_client.py,轉(zhuǎn)瞬之間就結(jié)束了卢未,輸出:time cost 0.15320992469787598
這么快肪凛,還需要異步嗎?
慢著辽社,快是因為我們服務(wù)端的處理極其簡單伟墙,又放在本地,實際的情況復(fù)雜得多滴铅。為了模擬真實場景戳葵,我們在服務(wù)端加一個只有 5ms 的時延。

yield stream.write(msg[::-1])

之前加上

yield gen.sleep( 0.005 )

加上時延之后重新啟動 tcp_server.py汉匙,再運行 sync_client.py拱烁,這回就慢多了。輸出:time cost 6.7937400341033936
由于時延噩翠,client 每次發(fā)出消息之后要等 5 ms 以上才能收到回復(fù)戏自,時間浪費在 recv 里,一千次當然要五秒多伤锚。我們的目標就是通過異步編程優(yōu)化等待的開銷擅笔。

再看看異步的代碼

#async_client.py
import socket
from time import time
from tornado import ioloop

loop = ioloop.IOLoop.current()

socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(50)]
[sock.connect(('localhost', 8760)) for sock in socks]
SockD = {sock.fileno(): sock for sock in socks}
t0 = time()
n = 0
def OnEvent(fd, event):
    if event == loop.WRITE:
        loop.update_handler(fd, loop.READ)
    elif event == loop.READ:
        sock = SockD[fd]
        sock.recv(99)
        global n
        n += 1
        if n >= 1000:
            print('time cost ', time() - t0)
            sock.close()
            loop.remove_handler(fd)
            loop.stop()
            return
        loop.update_handler(fd, loop.WRITE)
        sock.send(b'test message.')

for fd, sock in SockD.items():
    loop.add_handler(fd, OnEvent, loop.WRITE)
    sock.send(b'test message.')
loop.start()

演示及解說

啟動加了時延的tcp_server.py,在運行aync_client.py,瞬間結(jié)束猛们,輸出:time cost 0.27816009521484375
比同步的快四念脯,五十倍。
為什么這么快阅懦?因為現(xiàn)在的recv雖然寫法與同步版本一樣和二,調(diào)用的時機已經(jīng)不同。
同步的版本耳胎,send之后緊接著調(diào)用recv惯吕,卻不知道數(shù)據(jù)多久才能返回,從調(diào)用recv到獲得數(shù)據(jù)之間只能等待怕午。而現(xiàn)在的異步版本废登,send完成時只是注冊了一個讀事件,直到真有數(shù)據(jù)到來時才調(diào)用recv郁惜,于是recv不用等待堡距,時間就節(jié)省下來了。
節(jié)省下來的時間給了別的協(xié)程兆蕉∮鸾洌可以看到,我們創(chuàng)建了50個連接來完成這一千次收發(fā)虎韵,每個連接一個協(xié)程易稠,send之后數(shù)據(jù)未來之際,別的協(xié)程可以發(fā)送自己的數(shù)據(jù)包蓝。

程序運行步驟

1.創(chuàng)建50個socket對象并全部連接到服務(wù)器驶社。
2.為這些socket對象創(chuàng)立文件描述符的索引。后面测萎,在回調(diào)函數(shù)里亡电,我們要通過文件描述符獲取socket對象
3.對每個socket對象注冊一個WRITE事件的回調(diào)函數(shù),之后發(fā)送第一條消息硅瞧。消息發(fā)送完成時份乒,WRITE事件發(fā)生,觸發(fā)i oloop執(zhí)行剛剛注冊的回調(diào)函數(shù)腕唧,傳入文件描述符和觸發(fā)回調(diào)的事件冒嫡,這次的事件是WRITE。接下來我們要等待服務(wù)器端返回的消息四苇,因此將注冊等待的事件改為READ孝凌。
4.對于每個等待READ事件的socket對象,一旦有數(shù)據(jù)來月腋,又以事件READ觸發(fā)回調(diào)蟀架,我們檢查一下n的值瓣赂,未滿1000就發(fā)下一條數(shù)據(jù)。發(fā)之前要再把注冊等待的事件改為WRITE片拍。
5.重復(fù)3煌集,4兩步,直到發(fā)滿一千條捌省,取消事件注冊苫纤,結(jié)束。

我們用了 ioloop 的三個方法來實現(xiàn)消息注冊與回調(diào):
add_handler:注冊一個回調(diào)函數(shù)到一個文件描述符的指定事件上纲缓;
update_handler:改變 add_handler 注冊的事件卷拘,文件描述符和回調(diào)函數(shù)不變;
remove_handler:取消事件注冊祝高。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栗弟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子工闺,更是在濱河造成了極大的恐慌乍赫,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陆蟆,死亡現(xiàn)場離奇詭異雷厂,居然都是意外死亡,警方通過查閱死者的電腦和手機叠殷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門改鲫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人溪猿,你說我怎么就攤上這事∪宜” “怎么了诊县?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長措左。 經(jīng)常有香客問我依痊,道長,這世上最難降的妖魔是什么怎披? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任胸嘁,我火速辦了婚禮,結(jié)果婚禮上凉逛,老公的妹妹穿的比我還像新娘性宏。我一直安慰自己,他們只是感情好状飞,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布毫胜。 她就那樣靜靜地躺著书斜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪酵使。 梳的紋絲不亂的頭發(fā)上荐吉,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機與錄音口渔,去河邊找鬼样屠。 笑死,一個胖子當著我的面吹牛缺脉,可吹牛的內(nèi)容都是我干的痪欲。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼枪向,長吁一口氣:“原來是場噩夢啊……” “哼勤揩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起秘蛔,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤陨亡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后深员,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體负蠕,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年倦畅,在試婚紗的時候發(fā)現(xiàn)自己被綠了遮糖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡叠赐,死狀恐怖欲账,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芭概,我是刑警寧澤赛不,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站罢洲,受9級特大地震影響踢故,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惹苗,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一殿较、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧桩蓉,春花似錦淋纲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玷或。三九已至,卻和暖如春片任,著一層夾襖步出監(jiān)牢的瞬間偏友,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工对供, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留位他,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓产场,卻偏偏與公主長得像鹅髓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子京景,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容