写教程,改变世界


2 推荐

作者:北极仙翁
2016-10-02 14:42:36
标签:tornado,python
有效阅读:1648
点击量:19017

免责声明:本站(guideep)之内的所有教程概由作者自由创作并承担全部责任。教程的内容和观点未经 guideep 审核。读者在阅读过程中应自行鉴别其是否真确、有效或安全。
特别地,如果教程内容涉及化学、生物、高压电气、放射性、医疗、气功、宗教、运动等可能造成严重后果的领域,请读者慎重鉴别,切勿盲目学习。
本站不对学习造成的后果承担任何责任。

Tornado 实例教程
[div=#ffffcc]本教程以大量简明的实例向熟悉 Python 服务端同步编程的读者介绍[b]异步网络框架[/b] Tornado 的使用,帮助读者快速进入 Tornado 异步编程的世界。 在阅读本教程之前,读者应掌握 Python 语言和基本的网络知识,并至少熟悉一种 Python web 编程框架[toggle=off] [title][sup][如][/sup][/title] [content][url=https://cloud.google.com/appengine/docs/python/tools/webapp/]webapp[/url], [wiki=zh]django[/wiki], [wiki=zh]flask[/wiki], [url=http://webpy.org/]web.py[/url] 等均可。[/content][/toggle]。 本教程中的代码依照 Python2.7 和 Tornado4.3 版本的标准编写。代码放在[url=https://github.com/saintthor/tornado-guide-examples]这里[/url]。 网页上的代码使用全角空格实现缩进效果,直接拷出去运行会报错。[/div] [b]缘起[/b] 前段时间学习 Tornado 时,找不到理想的教程。网上的中文教程内容不够全面,英文的官方文档却又缺少实例。我相信简单的例子胜于大段文字说明,特别是对于英文不太好的人。因此决定分享一部含有大量极简实例的 Tornado 入门教程,读者只要在这些实例基础上添加自己的代码,就能方便地构建 tornado 异步程序。 点开下面条目阅读。 [hr] [toggle=off] [title]为什么用 Tornado?[/title] [content][toggle=off] [title]异步编程原理[/title] [content]服务端同时要对许多客户端提供服务,它的性能至关重要。而服务端的处理流程,只要遇到 I/O 操作,往往需要长时间的等待。 一个典型的服务端处理流程如下: [table] [tr][td=|background-color=#fd8cb5]接收及处理输入数据(1ms)[/td][td=|background-color=#8cfd93]访问远程网络/数据库(80ms)[/td][td=|background-color=#fd8cb5]整理数据及输出(1ms)[/td][/tr] [/table] 整个流程 82ms,中间有 80ms 是在[b]等待[/b]耗时操作结束,这段时间里服务进程的 CPU 是空闲的,浪费掉了。按这样的方式执行,每个进程每秒只能响应 12 次访问。我们希望把等待的时间利用起来,让它在一个服务的等待期间,执行更多的服务中需要 CPU 来做的事。 [toggle=on] [title]理想的处理方式[/title] [content][a=hope]我们希望服务进程能这样工作: [table] [tr][td=|width=0px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|background-color=#8cfd93|width=500px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|width=40px][/td][/tr] [/table] [table] [tr][td=|width=10px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|background-color=#8cfd93|width=500px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|width=30px][/td][/tr] [/table] [table] [tr][td=|width=20px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|background-color=#8cfd93|width=500px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|width=20px][/td][/tr] [/table] [table] [tr][td=|width=30px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|background-color=#8cfd93|width=500px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|width=10px][/td][/tr] [/table] [table] [tr][td=|width=40px][/td][td=|background-color=#fd8cb5|width=10px][/td][td=|background-color=#8cfd93|width=500px][/td][td=|background-color=#fd8cb5|width=10px][/td][/tr][/table] 如上图,在每一行的响应流程中,红色是 CPU 忙的阶段,绿色是 CPU 空闲的阶段。前面的服务流程一旦开始等待 I/O 结束,服务进程就将此流程挂起来,开始接受下一个请求,直到前面的流程等待结束,可以向下走时,再回过头来完成前面的流程。 换个角度看,是 CPU 在各个流程间跳来跳去,专门处理那些红色的片段。 这种模式下,服务进程有效利用了等待时间,实际花费的只是一头一尾两段真正占用 CPU 的时间,共 2ms。这样,服务进程每秒钟可以处理 500 个请求了。 [/content][/toggle] 当然,我们可以用多线程/多进程达到类似的目的。但线程和进程都是由系统控制的,消耗资源较多,而且何时运行,何时挂起不由程序本身作主,调度开销较大。我们希望将多任务流程的调度工作放到自己的代码里,精确控制它的行藏。与线程相似,我们称这种任务流程为[b]协程(coroutine)[/b]。 协程是在一个线程之内,无须操作系统参与,由程序自身调度的执行单位。 按照上述模式,在一个进程之内同时处理多个协程(请求),充分利用 CPU 时间,就是我们需要的[b]异步编程[/b]。 [/content][/toggle] [toggle=off] [title]底层依赖 epoll[/title] [content]在 linux 下,底层对一个耗时操作(如网络访问)的处理流程为: 发起访问,将网络连接的[b]文件描述符[/b](fd)和期待事件(read)注册到 epoll 里。当期待事件发生,epoll 触发事件处理机制,通过回调函数通知 tornado,tornado 切换协程。 在 BSD 下,以 kqueue 代替 epoll,流程相似。 在 windows 下,用 select 代替 epoll,因而性能较差[toggle=off] [title][sup][?][/sup][/title] [content]select 性能不及 epoll,参考[url=https://segmentfault.com/a/1190000003063859]这篇文章[/url]。[/content][/toggle]。 后文中涉及底层的部分一律以 epoll 为例。 [/content][/toggle] [toggle=off] [title]用生成器实现协程[/title] [content]要实现上述异步的操作,必须判断两个点: 1、等待开始——协程走到这里,去 epoll 注册,将 CPU 的控制权让给别的协程。 2、等待结束——协程走到这里,接到 epoll 回调,开始请求 CPU 的控制权,执行后续操作。 在 Tornado 里,用一个 yield 语句来标识这两个点[toggle=off] [title][sup][注][/sup][/title] [content]较早版本的 Tornado 是用回调函数来实现等待的。那种写法已经过时,本文不涉。[/content][/toggle]。程序开始等待时,向外 yield 一个 Future 对象,直到等待结束才回来执行 yield 的下一条语句。代码如下: [div=#ffffcc] #前面的代码 yield gen.sleep( 1 ) #开始等待。gen.sleep 会返回一个 Future 对象,里面包装了一个等待过程。 #等待结束再执行这里 [/div] 你熟悉 Python,知道 yield 是生成器语句[toggle=off] [title][sup][否][/sup][/title] [content]如果不知道,需要补一下 [url=http://blog.csdn.net/orangleliu/article/details/8752058]Python 生成器[/url]的知识。[/content][/toggle]。一个函数执行中间,先通过 yield 出去一下,随后再回来执行后面部分。Tornado 就利用了 yield 的这个特性,在需要等待时先离开当前协程,等待结束了再回来。 [/content][/toggle] 下面可以看[goto=httpserver]完整的示例代码[/goto]了。 [/content][/toggle] [toggle=off] [title]Tornado 异步 HTTP 服务器[/title] [content][a=httpserver]Tornado 最大的用途当然是 HTTP 服务器。先看一个最基本的[toggle=on] [title]异步 HTTP 服务器[/title] [content][div=#ffffcc|float=right]#http_0.py from tornado.ioloop import IOLoop from tornado import gen, web class ExampleHandler( web.RequestHandler ):[a=reqhandler]  @gen.coroutine[a=coroutine]  def get( self ):[a=get]   delay = self.get_argument( '[size=1][a=prm]delay[/size]', [size=1][a=default]5[/size] )   [a=sleep]yield gen.sleep( int( delay ))   self.write( [a=write]{ "status": 1, "msg": "success" } )   self.finish()[a=finish] # @gen.coroutine # def post( self ):[a=post] #  pass application = web.Application( [       ( r"/[size=1][a=uri]example[/size]", ExampleHandler ),       #( r"/other", OtherHandler ),       ], [size=1][a=autoreload]autoreload[/size] = True ) [a=listen]application.listen( [size=1][a=port]8765[/size] ) [a=start]IOLoop.current().start() [/div] [a=http0]右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 http_0.py [select=1] [choice=on][title]演示[/title][content] 运行这段代码,用浏览器访问 http://localhost:[goto=port]8765[/goto]/[goto=uri]example[/goto]?[goto=prm]delay[/goto]=1 即可在延迟 1 秒之后得到返回结果 [goto=write]{ "status": 1, "msg": "success" }[/goto]。delay 传入是几就延迟几秒,如果没有传入,默认值为 [goto=default]5[/goto]。[toggle=off] [title][sup][注][/sup][/title] [content]你可能发现了,在本段里有一些绿色的链接,用鼠标指上去,右边代码中相关部分会高亮显示。[/content][/toggle] 在这个例子里,我们用 [goto=sleep]gen.sleep[/goto] 实现延迟。gen.sleep 与 time.sleep 的用法相近,区别在于它是异步的,只阻塞当前协程,等待一段时间,但不影响同一进程内的其它协程的执行。 可以认为 gen.sleep 是 time.sleep 的一个异步版本。在 tornado 开发中,我们经常需要使用某个内含等待的函数的异步版本,以免同步的等待阻塞住整个进程。这些异步的操作都返回 Future 对象。 [toggle=on] [title]检验它能否并发服务[/title] [content]打开两个浏览器(不要使用同一个浏览器的两个标签页),各自在地址栏中输入 http://localhost:8765/example?delay=10 ,尽可能快地在两个浏览器里先后敲回车加载页面,你会发现,两个页面都是在你开始加载的 10 秒钟之后返回,两次加载的总用时是 10 秒稍多,远不到 20 秒。这说明我们的示例程序虽然只有一个进程,一个线程,却能在第一个请求完成之前开始处理第二个请求,有如[goto=hope]我们期望的那样[/goto]。 如果你觉得两个请求不足以证明,可以通过[goto=testing]测试模块[/goto]发送更多的请求。 [/content][/toggle] [goto=explain0]点此看对代码的详细解说[/goto] [/content][/choice] [choice][title]解说[/title][content] [a=explain0]对代码的详细解说 tornado 与 webapp 一样,用一个[goto=reqhandler]继承于 web.RequestHandler 的类[/goto]构造处理 HTTP 访问的 handler,不同于 django, flask, bottle 等用函数构造 handler。在这个类里,我们[goto=get]重载 get 方法[/goto]来处理 GET 请求,也可以重载 [goto=post]post[/goto] 、put、delete 等其它 HTTP 方法。 你大概注意到了,重载的方法前都加了 [goto=coroutine]@gen.coroutine[/goto] 这样一个装饰器,它的作用就是把一个普通的函数变成一个返回 Future 对象的函数,即[b]异步函数[/b]。 异步函数一定要用 yield 调用,而且只有在另一个异步函数之内的调用才能起作用。例如,可以把 [goto=reqhandler]ExampleHandler[/goto] 改成[toggle=off] [title]这个样子[/title] [content][div=#ffffcc|float=right]class ExampleHandler( web.RequestHandler ):  @gen.coroutine  def get( self ):   delay = self.get_argument( 'delay', 5 )   times = yield self.delayTwice( int( delay ))[a=yieldout]   #yield self.delayTwice( int( delay ))[a=yieldcommet]   self.write( { "status": 1, "times": times } )   self.finish()  @gen.coroutine  def delayTwice( self, seconds ):[a=delayTwice]   [size=1]yield gen.sleep( seconds ) yield gen.sleep( seconds )[a=yieldin][/size]   raise gen.Return( 2 )[a=raisereturn] [/div]将 gen.sleep 放在另一个异步函数 [goto=delayTwice]delayTwice[/goto] 里,用浏览器访问时会得到两倍的延时。 [goto=yieldout]这行代码[/goto]中,对 times 赋值似乎不太好懂,如果写成[goto=yieldcommet]下面注掉的那行[/goto]的样子,只是单纯调用,就好懂了。事实上,赋给 times 的值是 [goto=delayTwice]delayTwice[/goto] 的返回值。 我们知道,内部使用了 yield 语句的函数都是生成器,不能像普通函数一样通过 return 返回一个结果。Tornado 的异步函数都是生成器,当它需要返回一个值时,同样无法 return,只能以变通的方式实现。 注意[goto=raisereturn]这一行代码[/goto],我们将返回值 2 包在一个 Return 异常里,然后 raise 出去,就能把它传给异步函数的调用者。调用结束之后,[goto=yieldout]times 的值就是 2[/goto]。 [/content][/toggle]。 用 [goto=write]self.write[/goto] 输出执行结果。如果输出的是一个字典,Tornado 会自动把它转成 JSON 串。 正常结束的 HTTP 调用须[goto=finish]以 self.finish() 结束[/goto] 。但如果是[toggle=off] [title]渲染模板[/title] [content][a=render]如果输出的是 html 页,需要渲染模板,get 方法可改为:[div=#ffffcc]  @gen.coroutine  def get( self ):   self.render( 'templates/test.html', subject = 'Test' ) [/div] 当然,还需要在[goto=static_path]静态文件路径[/goto]下的 /templates 有一个 test.html 模板文件,其内容为: [div=#ffffcc]<html><head><title>{{subject}}</title></head></html>[/div] 才能正确输出网页。 Tornado 的模板写法与其它框架比较相似,可以去[url=http://tornado-zh.readthedocs.io/zh/latest/guide/templates.html]这里[/url]学习。 [/content][/toggle]或者[toggle=off] [title]跳转到其它网址[/title] [content][div=#ffffcc] @gen.coroutine  def get( self ):   self.redirect( "/example2" ) [/div][/content][/toggle],不应添加 self.finish()。 接下来要[toggle=off] [title]构造 Application 对象[/title] [content]通过 Application 对象将构造好的 ExampleHandler [goto=uri]与一个 uri 联系起来[/goto],当然可以有更多的 Handler 和 uri。 [toggle=on] [title]创建 Application 对象时可设置这些参数[/title] [content][goto=autoreload][i]autoreload[/i][/goto]:若设为 True,在程序运行起来之后,每次编辑代码并保存时,可以自动重新运行。 [i]debug[/i]:若设为 True,会把运行出错信息打印在屏幕上。 [i]cookie_secret[/i]:用于 cookie 加密的密钥,形如:“ofjf939.m%dw$#3fdn923hrfsp309-[2”。 [a=static_path][i]static_path[/i]:静态文件路径。如果与当前文件同一路径,可设为:os.path.dirname( __file__ )。 [i]xsrf_cookies[/i]:xsrf 检测。 还有更多参数,从略。 [/content][/toggle][/content][/toggle],然后[goto=listen]监听端口[/goto],[goto=start]启动消息循环[/goto],服务器就能运行起来了。 实际应用中,我们可能需要[toggle=off] [title]多进程服务器[/title] [content][goto=hope]前面论证过[/goto],一个典型的单进程、单线程,须访问数据库的异步 Tornado 服务器每秒可以响应 500 个请求,在较低配置服务器上实际的负载能力大概也是这个数字。如果需要更高的负载能力,且服务器有多个 CPU(基本都有),应开启多个服务进程。 只要把[goto=listen]这行代码[/goto]改为: [div=#ffffcc] from tornado.httpserver import HTTPServer server = HTTPServer( application ) server.bind( 8765 ) server.start( [size=1][a=procnum]4[/size] ) [/div] 就能同时启动 [goto=procnum]4[/goto] 个进程了。 理论上讲,在一个线程里也能运行多个协程,可以做出「多进程 × 多线程 × 多协程」的模式。而实际上,协程可以完全代替线程,「多进程 × 多协程」已经能够充分利用服务器的硬件资源。 [/content][/toggle]。 [/content][/choice] [/select] [/content][/toggle]。 还有[toggle=off] [title]流式响应的 HTTP 服务器[/title] [content][div=#ffffcc|float=right]#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>' )[a=write2]    self.flush()[a=flush]   self.finish()[a=finish2] application = web.Application( [       ( r"/example", ExampleHandler ),       ], autoreload = True ) application.listen( 8765 ) IOLoop.current().start() [/div] 如果响应数据较大,为了节约内存,或者是各部分数据的返回要有一个时间差,我们需要将数据分成多次发送。 右边是示例代码中的 http_stream.py [toggle=on] [title]演示及解说[/title] [content]与前面的 [goto=get]http_0.py[/goto] 相比,明显多了一行 [goto=flush]self.flush()[/goto]。它的作用是将此前由 [goto=write2]self.write()[/goto] 写入缓冲区的内容发送出去。在 [goto=finish2]self.finish()[/goto] 结束这次响应之前,可以多次调用 self.write 和 self.flush,逐步发出数据。 运行这段代码,用浏览器访问 http://localhost:8765/example 可以看到页面每隔一秒打印一行输出。如果注释掉 [goto=flush]self.flush()[/goto] 再运行,就会等到五秒之后才将五行输出同时打印出来。 [/content] [/toggle][/content] [/toggle]。 [goto=sleep]前面例子[/goto]用一个简单的 sleep 模拟了等待操作。而在真实使用中,最常见的等待是[goto=db]数据库操作[/goto]和[goto=web]网络访问[/goto]。 [/content][/toggle] [toggle=off][title]访问数据库[/title] [content][div=#ffffcc|float=right]#http_db.py from tornado.ioloop import IOLoop from tornado import gen, web from tornado_mysql import pools [size=1][a=connprm]connParam = { 'host': 'localhost', 'port': 3306, 'user': 'root',    'passwd': 'zzzzzz', 'db': 'testdb' }[/size] 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' )   [a=asyncdb]cursor = yield self.POOL.execute( 'select name from user                where id = %s', userid )   if cursor.rowcount > 0:    [a=dbfound]self.write( { "status": 1, "name": cursor.fetchone()[0] } )   else:    [a=dbnotfound]self.write( { "status": 0, "name": "" } )   self.finish() application = web.Application( [        ( r"/getuser", GetUserHandler ),          ], autoreload = True ) application.listen( 8765 ) IOLoop.current().start()[/div] [a=db]右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 http_db.py,从 mysql 库中读取数据。 [select=1] [choice=on] [title]演示[/title] [content]就像[goto=sleep]前一个例子[/goto]中以异步的 gen.sleep 代替通常的 time.sleep 一样,我们必须使用一套异步的数据库接口代替通常的 MySQLdb。[url=https://github.com/PyMySQL/Tornado-MySQL]tornado_mysql[/url] 就是这样一个模块。[toggle=off] [title][sup][更多][/sup][/title] [content][url=https://github.com/tornadoweb/tornado/wiki/Links]这里[/url]有支持 Tornado 的各种异步组件接口。 [/content][/toggle] 首先,安装 tornado_mysql : [div=#ffffcc]pip install tornado-MySQL[/div] 然后,在你的 mysql 中建立一个名为 user 的表,至少有两个字段:一个整数型的 id,和一个字符串型的 name。 编辑 http_db.py,将 mysql 的连接参数填入 [goto=connprm]connParam[/goto] 中。 运行 http_db.py,用浏览器访问: http://localhost:8765/getuser?id=1 如果你的 user 表里有 id = 1 的数据,它的 name 字段是 Tom,你将看到返回值: [goto=dbfound] { "status": 1, "name": "Tom" }[/goto] 如果没有找到数据,你将看到返回值: [goto=dbnotfound] { "status": 0, "name": "" }[/goto][/content][/choice] [choice] [title]解说[/title] [content]代码中没有什么难点。tornado_mysql 的函数与你熟悉的 MySQLdb 高度相似,只不过它的 execute 方法以异步方式等待数据库执行 SQL,必须[goto=asyncdb]通过 yield 调用[/goto]。 [toggle=on] [title]需要 ORM?[/title] [content]很多人喜欢通过 ORM 访问数据库,而 Tornado 没有提供异步的 ORM 工具。 我认为 ORM 是一个鸡肋式的工具,在业务代码与 SQL 之间增加了一层逻辑,带来了复杂度,却并未带来便利。因为: 1、ORM 代码并未比直接使用 SQL 的代码精简; 2、ORM 并不能完全屏蔽 SQL 一层。如果数据库操作的结果不对或者性能不佳,仍要回到 SQL 层去排查问题。特别是即使改正了 SQL,还不知道如何从 ORM 正确地生成这个 SQL。 因此,我不推荐使用 ORM。如果确实需要写一些“优雅”的接口,也可基于 tornado-MySQL 作一些简单包装。[/content][/toggle] 这是访问 mysql 的例子。如果你要使用 postgresql 或其它数据库,也需要先安装这些数据库的 tornado 接口,就像 tornado_mysql 一样。 [/content][/choice][/select] [/content] [/toggle] [toggle=off][title]访问网络[/title] [content][div=#ffffcc|float=right]#http_req.py from tornado.ioloop import IOLoop from tornado import gen, web from tornado.httpclient import AsyncHTTPClient [a=fetchurl]url = 'http://hq.sinajs.cn/list=sz000001' class GetPageHandler( web.RequestHandler ):  @gen.coroutine  def get( self ):  client = AsyncHTTPClient()  [a=asyncreq]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()[/div] [a=web]右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 http_req.py,读取网页内容。 [toggle=on] [title]演示[/title] [content]运行 http_req.py,用浏览器访问: http://localhost:8765/getpage 即可看到从 [goto=fetchurl]http://hq.sinajs.cn/list=sz000001[/goto] 抓取的内容。 [/content][/toggle] [toggle=on] [title]解说[/title] [content]Tornado 有一个 AsyncHTTPClient 用于访问其它网页,用法比较简单。它在 fetch 方法中等待远端网页返回内容,因此也要[goto=asyncreq]以 yield 调用[/goto]。[/content][/toggle] 我们从示例网址抓到的是股票实时行情数据。在真实使用场景中,我们可能希望周期性地抓取这个数据,这时,需要使用[goto=crons]定时任务[/goto]。 [/content] [/toggle] [toggle=off] [title]Tornado 用户认证[/title] [content][div=#ffffcc|float=right]from tornado.ioloop import IOLoop from tornado import gen, web [a=loginhandler]class LoginHandler( web.RequestHandler ):  @gen.coroutine  def get( self ):   self.[size=1][a=setcookie]set_secure_cookie[/size]( 'username', 'Tom' )   self.write( 'login ok.' )   self.finish() class LogoutHandler( web.RequestHandler ):  @gen.coroutine  def get( self ):   self.[size=1][a=clrcookie]clear_cookie[/size]( 'username' )   self.write( 'logout ok.' )   self.finish() class WhoHandler( web.RequestHandler ):  [a=getuser]def get_current_user( self ):   return self.[size=1][a=getcookie]get_secure_cookie[/size]( 'username' ) or 'unknown'  @gen.coroutine  [a=whoget]def get( self ):   [a=youare]self.write( 'you are ' + [size=1][a=curuser]self.current_user[/size] )   self.finish() application = web.Application( [       [a=login] ( r"/login", LoginHandler ),       [a=logout]( r"/logout", LogoutHandler ),       [a=whoami] ( r"/whoami", WhoHandler ),          ],       autoreload = True,       [a=cksec]cookie_secret="feljjfesrh48thfe2qrf3np2zl90bmw",        ) application.listen( 8765 ) IOLoop.current().start() [/div]右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 http_auth.py,处理用户身份认证。 [toggle=on] [title]演示[/title] [content]1、运行 http_auth.py,用浏览器访问 http://localhost:8765/[goto=whoami]whoami[/goto] 可以看到输出:[goto=youare]you are unknown[/goto],说明还没有登录。 2、访问 http://localhost:8765/[goto=login]login[/goto] 看到:login ok. 登录成功。再访问 http://localhost:8765/[goto=whoami]whoami[/goto] 这时会看到:[goto=youare]you are Tom[/goto],说明你已经以 Tom 身份登录了。 3、访问 http://localhost:8765/[goto=logout]logout[/goto] 看到:logout ok. 退出登录。再访问 http://localhost:8765/[goto=whoami]whoami[/goto] 又变回了:[goto=youare]you are unknown[/goto],说明你已经退出登录。 [/content][/toggle] [toggle=on] [title]解说[/title] [content]代码有点长,其实很简单。 你知道服务端通常用 cookie 保存用户的身份信息。login 时创建 cookie;logout 时清除 cookie;需要检查用户身份时,读取 cookie。 在 tornado 里,创建 cookie 通常用 [goto=setcookie]set_secure_cookie[/goto],这样创建的 cookie 是加密的。与之对应的读取加密 cookie 的方法是 [goto=getcookie]get_secure_cookie[/goto]。 为了给 cookie 加密,要在创建 Application 时添加 [goto=cksec]cookie_secret[/goto] 属性,这是加密的密钥,它的值用一串乱写的字符就行了。 清除 cookie 用 [goto=clrcookie]clear_cookie[/goto]。 在 RequestHandler 里有一个 get_current_user 方法,它会在 [goto=whoget]get / post[/goto] 之前调用,其返回值会赋予 current_user 属性。我们在 WhoHandler 里重载了 [goto=getuser]get_current_user[/goto],后面在 [goto=whoget]get[/goto] 里就能直接使用 [goto=curuser]self.current_user[/goto] 了。 为便于演示,我们极大地简化了 [goto=loginhandler]LoginHandler[/goto]。实际使用中,一般通过 post 方法登录,登录时大概还要传入用户名和密码。 [/content][/toggle] [/content][/toggle] [toggle=off] [title]Tornado 定时任务[/title] [content][a=crons]定时任务分两种,一种是每隔一定的时间周期性地执行,另一种是在某个钟点单次执行。 [toggle=off] [title]周期性定时任务[/title] [content][div=#ffffcc|float=right]from tornado import ioloop, gen @gen.coroutine def Count():  [a=cronoutput]print '1 second has gone.' if __name__ == '__main__':  ioloop.[size=1][a=period]PeriodicCallback[/size]( Count, [size=1][a=interval]1000[/size] ).start()  ioloop.IOLoop.current().start()[/div] 右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 cron_0.py。 [toggle=on] [title]演示及解说[/title] [content]在启动消息循环之前,用 [goto=period]PeriodicCallback[/goto] 设定每 [goto=interval]1000 毫秒[/goto]执行一次异步函数 Count。 直接运行,每过一秒会打印一行:[goto=cronoutput]1 second has gone[/goto]. [/content][/toggle] [/content][/toggle] [toggle=off] [title]单次定时任务[/title] [content][div=#ffffcc|float=right]from tornado import ioloop, gen from time import time @gen.coroutine def Ring():  [a=ringoutput]print 'it\'s time to get up.' if __name__ == '__main__':  loop = ioloop.IOLoop.current()  [a=callatln]loop.[size=1][a=callat]call_at[/size]( time() + [size=1][a=crondelay]5[/size], Ring )  loop.start() [/div] 右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 cron_1.py。 [toggle=on] [title]演示及解说[/title] [content]在启动消息循环之前,用 [goto=callat]call_at[/goto] 设定 [goto=crondelay]5 秒[/goto]后执行一次异步函数 Ring。如果想在明早 9 点执行,需要输入明早 9 点的 unix 时间戳。 直接运行右边代码,五秒之后会打印一行:[goto=ringoutput]it's time to get up.[/goto],仅此一次。 如果要在一个相对的时间(例如五秒钟后)而不是一个绝对时间(例如八点整)运行定时任务,用 call_later 会比 call_at 更简单一点。可以将[goto=callatln]这一行代码[/goto]改为: [div=#ffffcc][a=calllater]loop.call_later( 5, Ring )[/div] 执行效果是一样的。 [/content][/toggle][/content][/toggle] [/content][/toggle] [toggle=off] [title]Tornado 单元测试[/title] [content][div=#ffffcc|float=right]#test.py from tornado.testing import gen_test, AsyncTestCase from tornado.httpclient import AsyncHTTPClient import unittest [a=testclass]class MyAsyncTest( AsyncTestCase ):  [a=gentest]@gen_test  [a=testprefix]def test_xx( self ):   client = AsyncHTTPClient( self.io_loop )   path = 'http://localhost:8765/example?[size=1][a=testdelay]delay=2[/size]'   responses = yield [client.fetch( path, method = 'GET' )           for _ in range( [size=1][a=testtimes]10[/size] )]   for response in responses:    print response.body if __name__ == '__main__':  [a=starttest]unittest.main() [/div] [a=testing]右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 test.py,用于测试 http_0.py。 [select=1][choice=on] [title]演示[/title] [content]1、先运行 [goto=http0]http_0.py[/goto],保证通过浏览器访问 http://localhost:8765/example?delay=2 能得到结果。 2、运行 test.py,看到如下的输出: [div=#ffffcc]$ python test.py {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} {"status": 1, "msg": "success"} . ---------------------------------------------------------------------- Ran 1 test in [size=1][a=timelen]2.011s[/size] OK[/div] 可见,测试模块在 [goto=timelen]2.011秒[/goto]之内完成了 [goto=testtimes]10[/goto] 次请求,每个请求都要延迟 [goto=testdelay]2[/goto] 秒返回,因此,这十个请求必定是并行执行的。 [/content][/choice] [choice] [title]解说[/title] [content]与普通的 unittest 用法相似,先定义好基于 AsyncTestCase 的[goto=testclass]测试类[/goto],在执行 [goto=starttest]unittest.main()[/goto]时,测试类中所有 [goto=testprefix]以 test 开头的方法[/goto]都会执行。 注意,测试类中的方法要以 [goto=gentest]gen_test[/goto] 装饰。 每次测试整体的用时不能超过 5 秒,超则报错。 [/content][/choice][/select] [/content][/toggle] [toggle=off] [title]Tornado 异步 TCP 连接[/title] [content]Tornado 有 TCPClient 和 TCPServer 两个类,可用于实现 tcp 的客户端和服务端。事实上,这两个类都是对 [goto=iostream]iostream[/goto] 的简单包装。 [toggle=on] [title]真正重要的是 iostream[/title] [content][a=iostream]iostream 是 client 与 server 之间的 tcp 通道。被动等待创建 iostream 的一方是 server,主动找对方创建 iostream 的一方是 client。 在 iostream 创建之后,[b]client 与 server 的操作再无分别[/b],在任何时候都可以通过 iostream.write 向对方传送内容,或者通过 iostream.read_xx[toggle=off] [title][sup][注][/sup][/title] [content]这是一组以 read_ 开头的方法,有: read_bytes 读取指定长度; read_until 读取直到特定字符; read_until_regex 读取直到特定正则表达式; read_until_close 读取直到连接关闭; 详见[url=http://www.tornadoweb.org/en/stable/iostream.html]官方文档[/url]。 [/content][/toggle] 接收对方传来的内容,或者以 iostream.close 关闭连接。[/content][/toggle]。 [toggle=off] [title]TCPServer[/title] [content][div=#ffffcc|float=right][a=tcpserver]#tcp_server.py from tornado import ioloop, gen, iostream from tornado.tcpserver import TCPServer [a=mytcpserver]class MyTcpServer( TCPServer ):  @gen.coroutine  [a=handlestream]def handle_stream( self, [size=1][a=hdstreamprm]stream, address[/size] ):   try:    while True:     [a=tcpread]msg = yield stream.read_bytes( 20, partial = True )     print msg, 'from', address     [a=tcpwrite]yield stream.write( msg[::-1] )     if msg == 'over':      [a=tcpclose]stream.close()   [a=streamcloseerr]except iostream.StreamClosedError:    pass if __name__ == '__main__':  server = MyTcpServer()  [a=tcplisten]server.listen( 8760 )  [a=tcpstart]server.start()  [a=tcploopstart]ioloop.IOLoop.current().start() [/div] 右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 tcp_server.py。 [toggle=on] [title]解说[/title] [content]创建一个[goto=mytcpserver]继承于 TCPServer 的类[/goto]的实例,[goto=tcplisten]监听端口[/goto],[goto=tcpstart]启动服务器[/goto],[goto=tcploopstart]启动消息循环[/goto],服务器开始运行。 这时,如果有 client 连接过来,Tornado 会创建一个 iostream,然后调用[goto=handlestream]handle_stream[/goto] 方法,调用时传入的[goto=hdstreamprm]两个参数[/goto]是 iostream 和 client 的地址。 我们示例的功能很简单,每[goto=tcpread]收到[/goto]一段 20 字符以内的内容,将之[goto=tcpwrite]反序回传[/goto],如果收到 'over‘,就[goto=tcpclose]断开连接[/goto]。注意,[goto=tcpclose]断开连接[/goto]不用 yield 调用。 无论是谁断开连接,连接双方都会各自触发一个 [goto=streamcloseerr]StreamClosedError[/goto]。 [/content][/toggle] [/content][/toggle] [toggle=off] [title]TCPClient[/title] [content][div=#ffffcc|float=right]#tcp_client.py from tornado import ioloop, gen, iostream from tornado.tcpclient import TCPClient @gen.coroutine def Trans():  [size=1][a=cliios]stream[/size] = yield TCPClient().[size=1][a=tcpconn]connect( 'localhost', 8760 )[/size]  try:   for msg in ( 'zzxxc', 'abcde', 'i feel lucky', [size=1][a=over]'over'[/size] ):    [a=cliwrite]yield stream.write( msg )    back = yield stream.read_bytes( 20, partial = True )    print back  except iostream.StreamClosedError:   pass if __name__ == '__main__':  [a=runsync]ioloop.IOLoop.current().run_sync( Trans )[/div] 右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 tcp_client.py。 [toggle=on] [title]解说[/title] [content]使用 TCPClient 比 TCPServer 更简单,无须继承,只要用 connect 方法[goto=tcpconn]连接到 server[/goto],就会返回 [goto=cliios]iostream[/goto] 对象了。 在本例中,我们向 server [goto=cliwrite]发送[/goto]一些字符串,它都会反序发回来。最后发个 [goto=over]'over'[/goto],让 [goto=tcpclose]server 断开连接[/goto]。当然也可以由 client 断开。 值得注意,这段代码与之前的几个例子有个根本的区别,之前都是服务器,被动等待行为发生,而这段代码是一运行就主动发起行为(连接),因此它的运行方式不同于以往,需要我们主动通过 ioloop 的 [goto=runsync]run_sync[/goto] 来调用。以往那些实例中的异步处理方法实际是由 Tornado 调用的。在 run_sync 里,tornado 会先启动消息循环,执行目标函数,之后再结束消息循环。 [/content][/toggle] [/content][/toggle] [toggle=on] [title]演示[/title] [content]在第一个终端窗口运行 tcp_server.py,在第二个终端窗口运行 tcp_client.py,即可看到它们之间的交互和断开的过程。[/content][/toggle] [/content][/toggle] [toggle=off] [title]Tornado 的 ioloop 消息循环[/title] [content]Tornado 的异步功能都是通过 ioloop 实现的。 前面的每一段示例代码的最后一行都是启动 ioloop: [div=#ffffcc]ioloop.IOLoop.current().start()[/div] 每个进程有一个默认的 ioloop,虽然还可以有更多个,通常使用默认的就够了。在上面这行代码里,我们通过 current() 获得当前的 ioloop,让它 start(),ioloop 就会一直跑下去;让它 run_sync,就会跑起来,执行目标函数,执行完就停止。 一般编程中很少用到 ioloop 的其它功能,只要简简单单地处理好 RequestHandler 的 get / post 方法,调用 tornado-MySQL 的异步函数访问数据库,返回结果就可以了。 可是,如果有些内含等待(CPU 休息)的操作,找不到现成的异步库,该怎么办?只要深入到这个操作的底层,靠 ioloop,可以[toggle=on] [title]把同步的操作变成异步[/title] [content]内含等待的操作大概有几种情况: [toggle=off] [title]1、为了拖时间而等待[/title] [content]对应前面用过的 [goto=sleep]gen.sleep[/goto],如果没有 gen.sleep 呢?可以很简单地通过也在前面用过的 [goto=callat]call_at[/goto] 或 [goto=calllater]call_later[/goto] 来实现这个功能。这里就不细说了。[/content][/toggle] [toggle=on] [title]2、等待网络返回[/title] [content]这一部分是本章的重点。我们将不再使用 [goto=iostream]iostream[/goto],而是在同步代码的基础上,用 ioloop 自己实现一个与 iostream 同样异步、高效的 tcp client。 [toggle=off] [title]先看同步的代码[/title] [content][div=#ffffcc|float=right]import socket from time import time sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) sock.connect(( 'localhost', 8760 )) t0 = time() [a=repeat]for _ in range( 1000 ):  sock.send( 'test message.' )  [a=syncrecv]sock.recv( 99 ) print 'time cost', time() - t0 sock.close()[/div] 右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 sync_client.py。 [toggle=on] [title]演示及解说[/title] [content]这是一段十分普通的 tcp client 代码,当然它是同步的。连上服务端之后,发一条,收一条,[goto=repeat]重复一千次[/goto],看它耗时多少。 服务端就用我们前面用过的 [goto=tcpserver]tcp_server.py[/goto]。 先启动 tcp_server.py,再运行 sync_client.py,转瞬之间就结束了。看输出,我这里是 [div=#ffffcc]time cost 0.0606229305267[/div] 这么快,还需要异步吗? 慢着,快是因为我们服务端的处理极其简单,又放在本地,实际的情况复杂得多。为了模拟真实场景,我们在服务端[goto=delay5ms]加一个只有 5ms 的时延[/goto]。这是[toggle=off] [title]加上时延的代码[/title] [content][div=#ffffcc|float=right]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     [b][a=delay5ms]yield gen.sleep( 0.005 )[/b]     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() [/div][/content][/toggle]。 加上时延之后重新启动 tcp_server.py,再运行 sync_client.py,这回就慢多了。我这里是 [div=#ffffcc][a=synccost]time cost 5.32902908325[/div] 由于时延,client 每次发出消息之后要等 5 ms 以上才能收到回复,时间浪费在 [goto=syncrecv]recv[/goto] 里,一千次当然要五秒多。我们的目标就是[goto=asynccli]通过异步编程优化等待的开销[/goto]。 [/content][/toggle] [/content][/toggle] [toggle=off] [title]再看异步的代码[/title] [content][div=#ffffcc|float=right]import socket from time import time from tornado import ioloop loop = ioloop.IOLoop.current() socks = [socket.socket( socket.AF_INET, socket.SOCK_STREAM )     [a=socks]for _ in range( 50 )] [a=conn50][sock.connect(( 'localhost', 8760 )) for sock in socks] [a=socketd]SockD = { sock.[size=1][a=fileno]fileno[/size](): sock for sock in socks } t0 = time() n = 0 [a=onevent]def OnEvent( [size=1][a=fd_env]fd, event[/size] ):  [a=onwrite]if event == loop.WRITE:   [a=fdread]loop.update_handler( fd, loop.READ )  [a=onread]elif event == loop.READ:   [a=getsock]sock = SockD[fd]   [a=asyncrecv]sock.recv( 99 )   global n   n += 1   [a=n1000]if n >= 1000:    print 'time cost', time() - t0    sock.close()    [a=rmhandler]loop.remove_handler( fd )    loop.stop()    return   [a=fdwrite]loop.update_handler( fd, loop.WRITE )   [a=sendnext]sock.send( 'test message.' ) for fd, sock in SockD.items():  [a=addhandler]loop.add_handler( [size=1][a=fd]fd[/size], OnEvent, [size=1][a=event]loop.WRITE[/size] )  [a=asyncsend]sock.send( 'test message.' ) loop.start() [/div] [a=asynccli]右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 async_client.py。 [toggle=on] [title]演示及解说[/title] [content]先看看效果,启动加了时延的 tcp_server.py,再运行 async_client.py,瞬间结束。我这里的输出是 [div=#ffffcc]time cost 0.12104010582[/div] 比[goto=synccost]同步的[/goto]快四、五十倍。 为什么这么快?因为现在的 [goto=asyncrecv]recv[/goto] 虽然写法与[goto=syncrecv]同步版本[/goto]一样,调用的时机已经不同。 同步的版本,send 之后紧接着调用 [goto=syncrecv]recv[/goto],却不知道数据多久才能返回,从调用 recv 到获得数据之间只能等待。而现在的异步版本,[goto=onwrite]send 完成[/goto]时只是[goto=fdread]注册一个读事件[/goto],直到[goto=onread]真有数据到来[/goto]时才[goto=asyncrecv]调用 recv[/goto],于是 recv 不用等待,时间就节省下来了。 节省下来的时间给了别的协程。可以看到,我们[goto=socks]创建了 50 个连接[/goto]来完成这[goto=n1000]一千次[/goto]收发,每个连接一个协程,send 之后数据未来之际,别的协程可以发送自己的数据。 [toggle=on] [title]程序运行步骤[/title] [content]1、[goto=socks]创建 50 个 socket 对象[/goto]并全部[goto=conn50]连接到服务器[/goto]。 2、为这些 socket 对象[goto=socketd]建立[/goto][b]文件描述符[/b][toggle=off] [title][sup][?][/sup][/title] [content]文件描述符(file descriptor)是一个整数,每个 socket 连接有一个独有的文件描述符,向 epoll 注册事件时就用文件描述符标识 socket 连接。 socket 可通过 [goto=fileno]fileno[/goto] 方法获取文件描述符。 [/content][/toggle]的索引。后面,在回调函数里,我们要通过文件描述符[goto=getsock]获取 socket 对象[/goto]。 3、对每个 socket 对象[goto=addhandler]注册[/goto]一个 WRITE 事件的回调函数,之后[goto=asyncsend]发送[/goto]第一条消息。消息发送完成时,WRITE 事件发生,触发 ioloop 执行刚刚注册的回调函数,传入[goto=fd_env]文件描述符和触发回调的事件[/goto]。这次的事件是 WRITE。接下来我们要等待服务端返回的消息,因此将注册等待的事件[goto=fdread]改成 READ[/goto]。 4、对于每个等待 READ 事件的 socket 对象,一旦有数据来,又以事件 READ 触发回调,我们[goto=n1000]检查一下 n 的值[/goto],未满 1000 就[goto=sendnext]发下一条数据[/goto]。发之前要再把注册等待的事件[goto=fdwrite]改成 WRITE[/goto]。 5、重复 3、4 两步,直到[goto=n1000]发满一千条[/goto],[goto=rmhandler]取消[/goto]事件注册,结束。 [/content][/toggle] 我们用了 ioloop 的三个方法来实现消息注册与回调: [goto=addhandler]add_handler[/goto]:注册一个[goto=onevent]回调函数[/goto]到一个[goto=fd]文件描述符[/goto]的[goto=event]指定事件[/goto][toggle=off] [title][sup][?][/sup][/title] [content]ioloop 定义的事件有四种: NONE = 0 READ = _EPOLLIN #这些 _EPOLL 开头的,是 epoll 定义的事件。 WRITE = _EPOLLOUT ERROR = _EPOLLERR | _EPOLLHUP [/content][/toggle]上; [goto=fdread]update_handler[/goto]:改变 add_handler 注册的事件,文件描述符和回调函数不变; [goto=rmhandler]remove_handler[/goto]:取消事件注册。 可以看出,在上面的代码里,对 WRITE 事件的回调没有实质的操作,如果只要实现这个测试 client 的功能,我们可以不处理 WRITE,只关注 READ 事件即可。这是[toggle=off] [title]经过简化的代码[/title] [content][div=#ffffcc|float=right]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.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   sock.send( 'test message.' ) for fd, sock in SockD.items():  loop.add_handler( fd, OnEvent, loop.READ )  sock.send( 'test message.' ) loop.start()[/div]右边是[url=https://github.com/saintthor/tornado-guide-examples]示例代码[/url]中的 simple_async_cli.py。 [/content][/toggle],演示效果比上面的代码稍快一点。 [/content][/toggle] [/content][/toggle] [/content][/toggle] [/content][/toggle]。 [/content][/toggle][drawdata]{}[/drawdata]
uu

对本段内容的讨论

点击书签可编辑,清空即删除。