為什么用Tornado?
異步編程原理
服務(wù)器同時要對許多客戶端提供服務(wù),他的性能至關(guān)重要采够。而服務(wù)器端的處理流程,只要遇到了I/O操作,往往需要長時間的等待。
當然,我們可以用多線程/多進程達到類似的目的雌续,但線程和進程都是系統(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,看到如下輸出
可見商乎,測試模塊在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:取消事件注冊祝高。