翻译:introduce to tornado-Asynchronous Web Services

异步的web请求

迄今为止,咱们经过tornado这个强大的框架的功能建立了不少web应用,它简单,易用,它有足够强大的理由让咱们选择它作为一个web项目的框架.咱们最须要使用的功能是它在服务端的异步读取功能.这是一个很是好的理由:tornado可让咱们轻松地完成非阻塞请求的相应.帮助咱们更有效率地处理更多事情.在这一章节中,咱们将会讨论tornado最基本的异步请求.同时也会涉及一些长链接轮询的技术,咱们将会经过编写一个简单的web应用来演示服务器如何在较少资源的状况下,处理更多的web请求. javascript

异步的web请求

大部分的web应用(包括以前的全部例子),实际上都是阻塞运行的.这意味着当接到请求以后,进程将会一直挂起,直到请求完成.根据实际的统计数据,tornado能够更高效且快速地完成web请求,而不须要顾虑阻塞的问题.然而对于一些操做(例如大量的数据库请求或调用外部的API)tornado能够花费一些时间等待操做完成,这意味着这期间应用能够有效地锁定进程直到操做结束,很明显,这样将会解决不少大规模的问题. tornado给咱们提供了很是好的方法去分类地处理这样的事情.去更改那些锁死进程直到请求完成的场景.当请求开始时,应用能够跳出这个I/O请求的循环,去打开另外一个客户端的请求,当第一个请求完成以后,应用可使用callback去唤醒以前挂起的进程. 经过下面的例子,可以帮助你深刻了解tornado的异步功能,咱们经过调用一个twitter搜索的API去建立一个简单的web应用.这个web应用将一个变量q将查询字符串作为搜索条件传递给twitter的API,这样的操做将会获得一个大概的响应时间,如图片5-1展现的就是咱们应用想要实现的效果. html

图片5-1

对于这个应用,咱们将会展现三个版本: 1. 第一个版本将会同步的方式处理http请求 2. 第二个版本将会使用tornado的异步处理http请求 3. 最后的版本咱们将会使用tornado2.1的新模块 gen 让异步处理http的请求更加简洁和易用 html5

你不须要深刻了解例子中的twitter搜索API是如何工做的,固然你若是想要成为这方面的专家,能够经过阅读这些开发文档了解具体的实现:twitter search API java

开始同步处理

例子5-1的源代码是咱们统计tweer响应时间例子的同步处理版本.请记住:咱们在最开始导入了tornado的 httpclient 模块.咱们将会使用模块中的 httpclient 类去之行咱们的http请求. 而后,咱们将会使用模块中另外一个类 AsyncHTTPClient . 例子5-1:同步的HTTP请求:tweet_rate.py python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import tornado . httpserver
import tornado . ioloop
import tornado . options
import tornado . web
import tornado . httpclient
import urllib
import json
import datetime
import time
from tornado . options import define , options
define ( "port" , default = 8000 , help = "run on the given port" , type = int )
 
class IndexHandler ( tornado . web . RequestHandler ) :
     def get ( self ) :
         query = self . get_argument ( 'q' )
         client = tornado . httpclient . HTTPClient ( )
         response = client . fetch ( "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) )
         body = json . loads ( response . body )
         result_count = len ( body [ 'results' ] )
         now = datetime . datetime . utcnow ( )
         raw_oldest_tweet_at = body [ 'results' ] [ - 1 ] [ 'created_at' ]
         oldest_tweet_at = datetime . datetime . strptime ( raw_oldest_tweet_at ,
             "%a, %d %b %Y %H:%M:%S +0000" )
         seconds_diff = time . mktime ( now . timetuple ( ) ) - \
             time . mktime ( oldest_tweet_at . timetuple ( ) )
         tweets_per_second = float ( result_count ) / seconds_diff
         self . write ( """
<div style="text-align: center">
  <div style="font-size: 72px">
    %s
  </div>
     <div style="font-size: 144px">
    %.02f
  </div>
  <div style="font-size: 24px">
    tweets per second
  </div>
</div>""" % ( query , tweets_per_second ) )
 
if __name__ == "__main__" :
     tornado . options . parse_command_line ( )
     app = tornado . web . Application ( handlers = [ ( r "/" , IndexHandler ) ] )
     http_server = tornado . httpserver . HTTPServer ( app )
     http_server . listen ( options . port )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

这是目前为止咱们最熟悉的程序结构:咱们有一个 RequestHandler 类, IndexHandler 类,来自于根目录的应用请求将会被转发到 IndexHandler 类的get方法中.咱们q变量将经过 get_argument 抓取请求的字符串,传递q变量给 twitter 的搜索API并执行.这里是对应功能的代码: jquery

1
2
3
4
client = tornado . httpclient . HTTPClient ( )
         response = client . fetch ( "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) )
         body = json . loads ( response . body )

在例子中咱们演示了tornado的HTTPClient类,它将会调用返回对象的 fetch .这个同步版本的fetch 方法将会取到一个 URL 变量的参数作为参数. 咱们构造的这个 URL 变量与twitter 的搜索API 返回的结果关联(这个 rpp 变量将会记录咱们100个tweets 从第一个页面获取的结果, result_type 变量记录了咱们最近一次匹配的搜索结果).这个 fetch 方法将会返回一个 HTTPResponse 对象, 它的内容包括:从远程的 URL 返回结果的时间.twitter 返回的结果是 JSON 格式的,因此咱们可使用python的 json 模块去建立一个python能够处理的数据结构,获取返回的数据. “HTTPResponse对象的 fetch 方法容许你关联全部 HTTP 响应的结果.不只仅是body,若是想要了解更多能够阅读这个文档:official documentation git

剩下的代码处理的是咱们关心的tweets每一秒处理的图表.咱们使用全部tweets中最先的时间戳与当前时间比较,来肯定咱们有多少时间花费在搜索中.而后除以咱们的进行搜索操做的tweets数量,最后咱们将结果作成图表,以最基本的HTML格式返回给客户的浏览器. github

阻塞带来的麻烦

咱们已经编写了一个简单的tornado应用来完成统计从twitterAPI中获取搜索结果并显示到浏览器的功能,然而应用并无如想象地那样快速地将结果返回.它老是在每个twitter的搜索结果返回以前停滞.在同步(到目前为止,咱们的全部操做都是单线程的)应用,这意味着,服务器一次只能处理一个请求,因此,若是咱们的应用包含两个api的请求,你每一次(最多)只能处理一个请求.这不是一个高可用的应用,不利于分布到多进程或多服务器上进行操做. 以这个程序作为母版,你可使用任何工具对这个应用进行性能检测,咱们将会经过一个很是杰出的工具 Siege utility 作为咱们的测试例子.你能够像这样使用Siege Utility: web

1
$ siege http : / / localhost : 8000 / ? q = pants - c10 - t10s

在这个例子中, Siege 将会每十秒钟对咱们的应用进行一次有十个并发的请求.这个输出的结果能够查看图片5-2.你能够快速地在这里看到结果.这个API须要在等待中挂起,直到请求完成并返回处理结果才能继续.它不考虑是一个仍是两个请求,固然经过这一百个(每次十个并发)的模拟用户,它全部的操做结果显示会愈来愈慢. 图5-2 能够看到,每十秒一次,每次十个模拟用户完成请求的平均响应时间是1.99秒.成功完成了29次查询,请记住,这知识一个简单的web响应页面,若是咱们想要在添加更多调用数据库或其它web server 的请求,这个结果将会更糟糕.若是这种类型的代码被应用到生产环境中,到了必定程度的负载以后,请求的响应将会愈来愈慢,最后致使整个服务响应超时. ajax

基本的异步调用

很是幸运,tornado拥有一个叫作 AsyncHTTPClient 的类.它可以异步地处理 HTTP 请求,它的工做方式与例5-1同步工做的例子很像.下面咱们将重点讨论其中差别的内容.例5-2的源代码以下: 例5-2.tweet_rate_async.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import tornado . httpserver
import tornado . ioloop
import tornado . options
import tornado . web
import tornado . httpclient
 
import urllib
import json
import datetime
import time
 
from tornado . options import define , options
define ( "port" , default = 8000 , help = "run on the given port" , type = int )
 
class IndexHandler ( tornado . web . RequestHandler ) :
     @ tornado . web . asynchronous
     def get ( self ) :
         query = self . get_argument ( 'q' )
         client = tornado . httpclient . AsyncHTTPClient ( )
         client . fetch ( "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) ,
             callback = self . on_response )
 
     def on_response ( self , response ) :
         body = json . loads ( response . body )
         result_count = len ( body [ 'results' ] )
         now = datetime . datetime . utcnow ( )
         raw_oldest_tweet_at = body [ 'results' ] [ - 1 ] [ 'created_at' ]
         oldest_tweet_at = datetime . datetime . strptime ( raw_oldest_tweet_at ,
             "%a, %d %b %Y %H:%M:%S +0000" )
         seconds_diff = time . mktime ( now . timetuple ( ) ) - \
             time . mktime ( oldest_tweet_at . timetuple ( ) )
         tweets_per_second = float ( result_count ) / seconds_diff
         self . write ( """
<div style="text-align: center">
  <div style="font-size: 72px">
    %s
  </div>
  <div style="font-size: 144px">
    %.02f
  </div>
  <div style="font-size: 24px">
    tweets per second
  </div>
</div>""" % ( self . get_argument ( 'q' ) , tweets_per_second ) )
     self . finish ( )
if __name__ == "__main__" :
     tornado . options . parse_command_line ( )
     app = tornado . web . Application ( handlers = [ ( r "/" , IndexHandler ) ] )
     http_server = tornado . httpserver . HTTPServer ( app )
     http_server . listen ( options . port )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

AsyncHTTPClient 中的fetch方法并不会直接返回调用的结果.作为替代方案,它指定了一个回调函数,当咱们指定的方法或函数调用的 HTTP 请求有返回结果时,它将会把 HTTPResponse 对象作为变量返回,此时回调函数将会从新调用咱们设定的方法或者函数,继续执行.

1
2
3
4
client = tornado . httpclient . AsyncHTTPClient ( )
         client . fetch ( "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) ,
             callback = self . on_response )

在这个例子中,咱们指定了 on_response 作为回调,咱们想传送给 Twitter search API 的全部请求,都将放到 on_response 函数中.别忘了在前面使用 @tornado.web.asynchronous 修饰(在咱们定义的get 方法以前)并在这个回调的方法结束前调用 self.finish() ,咱们将在不久以后讨论其中的细节.这个版本的应用与阻塞版本对外输出的参数是同样的,可是性能将会更好一些,会有多大的提高呢?让咱们来看看实际的测试结果.正如你在图片5-3中看到的同样,咱们目前的版本测试结果为在12.59秒内,平均每秒钟发送3.20个请求.服务成功进行了118次一样的查询,性能有了很是明显的提高!正如预想的,使用长链接能够支持更多的并发用户,而不会像阻塞版本的例子那样逐渐变慢直到超时. 图片5-3

异步函数的修饰

tornado默认状况下会将函数处理结果返回以前与客户端的链接关闭.在通常状况下,这正是咱们想要使用的功能,可是在使用异步请求的回调功能时,咱们但愿与客户端的链接一直保持到回调执行完毕.你能够在你想要保持与客户端长链接的方法中使用 @tornado.web.asynchronous 修饰符告诉 tornado 保持这个链接.作为 tweet rate 例子的异步版本,咱们将会在 IndexHandler 的 get 方法中使用这个功能,相关的代码以下:

1
2
3
4
5
class IndexHandler ( tornado . web . RequestHandler ) :
     @ tornado . web . asynchronous
     def get ( self ) :
         query = self . get_argument ( 'q' )
         [ . . other request handler code here . . ]

请注意:在你使用 @tornado.web.asynchronous 修饰符的地方,tornado将不会关闭这个方法建立的链接.你必须明确地在 RequestHandler 对象的finish 方法中告诉 tornado 关闭这个请求建立的链接(不然,这个请求将会一直在后台挂起,而且浏览器可能没法正常显示你发送给客户端的数据).在目前这个异步的例子中,咱们将调用 finish 方法的操做写到了 on_response 函数中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[ . . other callback code . . ]
         self . write ( """
<div style="text-align: center">
  <div style="font-size: 72px">
    %s
  </div>
  <div style="font-size: 144px">
    %.02f
  </div>
  <div style="font-size: 24px">
    tweets per second
  </div>
</div>""" % ( self . get_argument ( 'q' ) , tweets_per_second ) )
     self . finish ( )

异步启动器

如今这个版本的 tweet rate 程序是性能更加优越的异步版本.不幸的是,它的结构显得稍微有些凌乱:咱们将会把处理请求的函数分离到两个方法中.当咱们有两个两个或者多个相互依赖的异步请求须要执行的时候:你可能须要在回调函数中调用回调函数.这可能会让代码的编写变得更加困难且难以维护.下面是一个很不科学(固然,它很是可能出现)的例子.

1
2
3
4
5
6
7
8
9
10
11
def get ( self ) :
     client = AsyncHTTPClient ( )
     client . fetch ( "http://example.com" , callback = on_response )
def on_response ( self , response ) :
     client = AsyncHTTPClient ( )
     client . fetch ( "http://another.example.com/" , callback = on_response2 )
def on_response2 ( self , response ) :
     client = AsyncHTTPClient ( )
     client . fetch ( "http://still.another.example.com/" , callback = on_response3 )
def on_response3 ( self , response ) :
     [ etc . , etc . ]

幸运的是,tornado2.1介绍了一个 tornado.gen 模块,它提供了很是简洁的范例给咱们演示了如何处理异步请求.例子5-3 是使用了 tornado.gen 到 tweet rate 应用进行异步处理的版本.请仔细阅读,而后咱们将会对它是如何工做的展开讨论. 例子5-3 tweet_rate_gen.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import tornado . httpserver
import tornado . ioloop
import tornado . options
import tornado . web
import tornado . httpclient
import tornado . gen
 
import urllib
import json
import datetime
import time
 
from tornado . options import define , options
define ( "port" , default = 8000 , help = "run on the given port" , type = int )
 
class IndexHandler ( tornado . web . RequestHandler ) :
     @ tornado . web . asynchronous
     @ tornado . gen . engine
     def get ( self ) :
         query = self . get_argument ( 'q' )
         client = tornado . httpclient . AsyncHTTPClient ( )
         response = yield tornado . gen . Task ( client . fetch ,
             "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) )
         body = json . loads ( response . body )
         result_count = len ( body [ 'results' ] )
         now = datetime . datetime . utcnow ( )
         raw_oldest_tweet_at = body [ 'results' ] [ - 1 ] [ 'created_at' ]
         oldest_tweet_at = datetime . datetime . strptime ( raw_oldest_tweet_at ,
             "%a, %d %b %Y %H:%M:%S +0000" )
         seconds_diff = time . mktime ( now . timetuple ( ) ) - \
             time . mktime ( oldest_tweet_at . timetuple ( ) )
         tweets_per_second = float ( result_count ) / seconds_diff
         self . write ( """
<div style="text-align: center">
  <div style="font-size: 72px">
    %s
  </div>
  <div style="font-size: 144px">
    %.02f
  </div>
  <div style="font-size: 24px">
    tweets per second
  </div>
  
</div>""" % ( query , tweets_per_second ) )
         self . finish ( )
if __name__ == "__main__" :
     tornado . options . parse_command_line ( )
     app = tornado . web . Application ( handlers = [ ( r "/" , IndexHandler ) ] )
     http_server = tornado . httpserver . HTTPServer ( app )
     http_server . listen ( options . port )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

正如你看到的,这个版本的代码大部分都和前两个版本的代码一致.主要的差别在于咱们如何调用 AsyncHTTPClient 对象中的 fetch 方法,这里是相关的代码:

1
2
3
4
5
client = tornado . httpclient . AsyncHTTPClient ( )
         response = yield tornado . gen . Task ( client . fetch ,
             "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) )
         body = json . loads ( response . body )

咱们使用了python的 yield 关键词标记了一个实例化的 tornado.gen.task 对象,咱们想要经过这个功能将变量传递给须要调用的函数,在这里咱们使用 yield 控制tornado 程序返回的结果,容许 tornado 在一个HTTP 请求执行的时候,去执行另一个任务.当 这个 HTTP 请求完成的时候, RequestHandler 方法会使用断开以前保存的变量,从上一次断开的地方继续执行.这是一个很是完美的控制方式,在 HTTP 请求返回正确的结果以后,将其转到正确的请求处理流程中,而不是放到回调函数中,这样操做的好处在于,代码变得更加容易理解:全部请求相关的逻辑代码都被放到相同的地方.这样的代码仍然支持异步执行.并且性能与咱们使用了 tornado.gen 处理异步请求的回调函数是一致的,具体信息能够查看图片5-4. 图片5-4

请记住, @tornado.gen.engine 修饰符要放到咱们定义的 get 方法前面,这样才可以在这个方法中使用 tornado 的 tornado.gen.task 类.这个 tornado.gen 模块有许多类和功能,使用它们可让咱们写出更简单且易于维护的 tornado 异步处理代码.更多资料能够查看这个文档.

使用异步处理

咱们已经在本章节使用 tornado 的异步终端作为例子演示如何对任务进行异步的处理.不一样的开发人员可能会选择不一样的异步终端库配合tornado解决问题,这里已经整理出了相关的类库在tornado wiki 上. bit.ly的 asyncmong 是其中最出名的例子.它可让咱们异步地去调用 mongodb 服务.这对咱们来讲是一个很是好的选择,让tornado 开发者能够利用它去完成异步调用数据库的操做,假如你但愿使用别的数据库,也能够查看 tornado wiki 列表中提供的其它异步处理库.

异步操做总结

正如咱们在前一个例子看到的,在tornado中使用异步的web服务超乎想象的简单和强大.使用异步地处理一些须要长时间等待的API和数据库请求,能够减小阻塞等待的时间,使得最终的服务处理请求更加快速.虽然不是每个操做都适于使用异步处理,但 tornado 试图让咱们更快速地使用异步处理去完成一个完整的应用.在构建一个依赖于缓慢的查询或外部服务的 web 应用时,tornado 非阻塞的功能会让问题变得更加易于处理. 然而值得值得一提的是这个例子并非很是合适.若是你来设计一个相似的应用,不管它是什么规模的应用,你也许更但愿将这个 twitter 搜索请求放到客户端的浏览器执行(使用javascript),让 web server 能够去处理其它的服务请求.在大部分的实例中,你也许还要考虑使用 cache 来缓存结果,这样就不须要对全部的搜索请求调用外部的API,相同的请求能够直接返回 cache 中缓存的结果.在通常状况下,若是你只须要在后台调用本身web 服务的 HTTP 请求,那么你应该再思考一下如何配置你的应用.

tornado的长轮询

tornado 异步架构的另一个优点是能够更方便地处理 HTTP 的长轮询.它能够处理实时更新状态:如一个很是简单的用户通知系统或复杂的多用户聊天室. 开发一个具备实时更新状态功能的 web 应用是全部 web 开发人员都必需要面对的挑战.更新用户状态,发送新的提示消息,或其它任何从服务端发送到客户端浏览器进行状态变动和加载的状态.早期一个相似的解决方案是按期让客户端浏览器发送一个更新状态的请求到服务器.这个技术带来的挑战是:询问的频率必须足够高才能保证通知能够实时更新,可是太频繁的 HTTP 请求在规模大的时候会面对更多挑战,当有成百上千的客户端时须要不断地建立新链接,频繁的轮询操做致使 web 服务端由于关闭上千个请求而挂掉. 因此使用”服务端推送”技术使得 web 应用能够合理地分配资源去保证明时分发状态更新更高效.实际上服务端推送技术能够更好地与现代浏览器结合起来.这个技术在由客户端发起链接接收服务端推送的更新状态的架构中很是流行.与短暂的 http 请求相对应,这项技术被成为长轮询或长链接请求. 长轮询意味着浏览器只须要简单地启动并保持一个与服务器的链接.这样浏览器只须要简单地等待响应服务器随时推送的更新状态请求便可.在服务器发送更新完毕以后就能够关闭链接(或这客户端的浏览器请求时间超时),这个客户端能够很是简单地建立新的链接,等待下一个更新信息. 这样分段处理全部 HTTP 的长轮询能够简单地完成实时更新的应用.tornado 的异步架构体系使得这样的应用搭建变得更加简单.

长轮询的好处

HTTP 长轮询最主要的好处是能够显著减小 web 服务器的负载.替换掉客户端频繁(服务器须要处理每个http 头信息)的短链接请求以后,服务器只须要处理第一次发送的初始化链接请求和须要发送更新信息所建立的链接.在大多数的时间,是没有新状态须要更新的,这时候这个链接将不会消耗任何cpu资源. 浏览器将得到更大的兼容性,全部的浏览器都支持 AJAX 建立的长轮询请求,不须要添加插件或使用额外的资源.相比其它服务端推送的技术, http 长轮询是为数很少被普遍应用的技术. 咱们已经接触了一些使用长轮询的技术,实际上前面提到的状态更新,消息提示,聊天功能都是当前比较流行的 web 页面功能. google Docs 使用长轮询来实现同步协做的功能,两我的能够同时编辑同一个文档,而且能够实时地看到对方进行的修改,twitter使用长轮询在浏览器上面实时地显示状态和消息的更新.facebook使用这个技术来构建它的聊天功能.长轮询如此流行的一个重要缘由就是:用户再也不须要反复刷新网页就能够看到最新的内容和状态.

例子:实时的库存报表

这个例子演示了一个服务如何在多个购买者的浏览器中保持最新的零售产品库存信息.这个应用服务将会在图书信息页面的 “添加购物车” 按钮被按下时从新计算图书的剩余库存,其余购买者的浏览器将会看到库存信息的减小. 要完成这个库存更新的功能,咱们须要编写一个 RequestHandler 的子类.在 HTTP 链接初始化方法被调用以后不要关闭链接.咱们须要使用 tornado 内置的 asynchronous 修饰符来完成这个功能.相关的代码能够查看例子5-4. 例子5-4: shopping-cart.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import tornado . web
import tornado . httpserver
import tornado . ioloop
import tornado . options
from uuid import uuid4
 
class ShoppingCart ( object ) :
     totalInventory = 10
     callbacks = [ ]
     carts = { }
     def register ( self , callback ) :
         self . callbacks . append ( callback )
 
     def moveItemToCart ( self , session ) :
         if session in self . carts :
             return
         self . carts [ session ] = True
         self . notifyCallbacks ( )
 
     def removeItemFromCart ( self , session ) :
         if session not in self . carts :
             return
         del ( self . carts [ session ] )
         self . notifyCallbacks ( )
 
     def notifyCallbacks ( self ) :
         for c in self . callbacks :
             self . callbackHelper ( c )
         self . callbacks = [ ]
    
     def callbackHelper ( self , callback ) :
         callback ( self . getInventoryCount ( ) )
 
     def getInventoryCount ( self ) :
         return self . totalInventory - len ( self . carts )
    
     class DetailHandler ( tornado . web . RequestHandler ) :
         def get ( self ) :
             session = uuid4 ( )
             count = self . application . shoppingCart . getInventoryCount ( )
             self . render ( "index.html" , session = session , count = count )
    
     class CartHandler ( tornado . web . RequestHandler ) :
         def post ( self ) :
             action = self . get_argument ( 'action' )
             session = self . get_argument ( 'session' )
             if not session :
                 self . set_status ( 400 )
                 return
             if action == 'add' :
                 self . application . shoppingCart . moveItemToCart ( session )
             elif action == 'remove' :
                 self . application . shoppingCart . removeItemFromCart ( session )
             else :
                 self . set_status ( 400 )
 
     class StatusHandler ( tornado . web . RequestHandler ) :
         @ tornado . web . asynchronous
         def get ( self ) :
             self . application . shoppingCart . register ( self . async_callback ( self . on_message ) )
         def on_message ( self , count ) :
             self . write ( '{"inventoryCount":"%d"}' % count )
             self . finish ( )
    
     class Application ( tornado . web . Application ) :
         def __init__ ( self ) :
             self . shoppingCart = ShoppingCart ( )
 
             handlers = [
                 ( r '/' , DetailHandler ) ,
                 ( r '/cart' , CartHandler ) ,
                 ( r '/cart/status' , StatusHandler )
             ]
             settings = {
                 'template_path' : 'templates' ,
                 'static_path' : 'static'
             }
             tornado . web . Application . __init__ ( self , handlers , * * settings )
 
if __name__ == '__main__' :
     tornado . options . parse_command_line ( )
 
     app = Application ( )
     server = tornado . httpserver . HTTPServer ( app )
     server . listen ( 8000 )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

在咱们仔细查看 shopping_cart.py 代码以前,了解一下 template 和 script 文件.咱们定义了一个 ShoppingCart 类来维护咱们的库存信息并在购买者添加这本书到购物车时显示实时的库存信息.而后咱们定义 DetailHandler 来提供html页面. CartHandler 提供了一个接口来维护购物车的信息.而后是 StatusHandler 咱们用它来通知咱们的最终库存的清单的改变. 这个 DetailHandler 将会为每个请求页面生成惟一的标识符.为每个查询库存的请求调用index.html 模板并发送到客户端浏览器. CartHandler 提供了一个 API 给浏览器让购买者进行书籍添加到购物车或者从购物车移除的操做,这个 javascript 将会在浏览器提交购物车操做的时候经过 POST 的方式运行.咱们来看看这个方法是如何与 StatusHandler 和 ShoppingCart 类共同影响实时库存信息的.

1
2
3
4
class StatusHandler ( tornado . web . RequestHandler ) :
         @ tornado . web . asynchronous
         def get ( self ) :
             self . application . shoppingCart . register ( self . async_callback ( self . on_message ) )

咱们首先要注意的事情是 StatusHandler 在 get 方法前使用了 @tornado.web.asynchronous 修饰符.这将会通知 tornado 不要关闭这个 get 方法返回的链接.在这个方法中,咱们为每个购物车操做注册了一个 callback . 咱们将会为要这个 callback 方法使用 self.async_callback 去保证这个 callback 不会被 RequestHandler 意外关闭.

在tornado1.1的版本中, callback 须要使用 self.async_callback() 方法来捕获任何由 wrapped 功能抛出的错误,在tornado1.1或更新的版本中,这个操做是必须的:

1
2
3
def on_message ( self , count ) :
     self . write ( '{"inventoryCount":"%d"}' % count )
     self . finish ( )

每当用户操做购物车的时候, ShoppingCart 控制器就会经过调用 on_message 方法来唤醒每个注册的 callback. 这个方法会把当前的库存信息发送给每个用而后关闭链接.(若是服务没有关闭链接,浏览器将没有办法知道请求是否完成,也不可以通知 script 更新数据.)如今咱们的长轮询链接将会被关闭,购物车控制器必须从注册的 callback 列表中移除这个 callback . 在这个例子中咱们将会使用新的 callback 列表替换空的列表.当咱们调用并完成请求处理时将 callback 的注册信息移除很是重要,若是使用 callback 唤醒以前已关闭的链接的并调用其对应的 finish() ,将会致使程序出错. 最后, ShoppingCart 控制器将会维护全部库存清单的分配状况和 callback 的状态. StatusHandler 将会经过 register 方法注册全部的 callback 状态,这些附加的功能都会被集成到 callback 列表中.

1
2
3
4
5
6
7
8
9
10
11
def moveItemToCart ( self , session ) :
         if session in self . carts :
             return
         self . carts [ session ] = True
         self . notifyCallbacks ( )
 
     def removeItemFromCart ( self , session ) :
         if session not in self . carts :
             return
         del ( self . carts [ session ] )
         self . notifyCallbacks ( )

ShoppingCart 控制器还为 CartHandler 构造了 addItemOcart 和 removeItemFromCart 方法.在咱们调用 notifyCallbacks 以前 CartHandler 将会调用这些方法的给请求的页面分配一个惟一的标识符( session 变量传递给这个方法)并使用它去给咱们的调用作标记.

1
2
3
4
5
6
7
def notifyCallbacks ( self ) :
         for c in self . callbacks :
             self . callbackHelper ( c )
         self . callbacks = [ ]
    
     def callbackHelper ( self , callback ) :
         callback ( self . getInventoryCount ( ) )

这些注册的 callback 在被调用的时候将会带上正确的库存数量和一个已清空的 callback 列表以保证 callback 不会调用到已关闭的链接.

这里是例子5-5对应的 html 模板,用于信息发生变动的书籍. 例子5-5 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[ crayon - 5194d2915e9ac inline = "true"    class = "xml" ]
< html >
     < head >
         < title > Burt 's Books – Book Detail</title>
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"
             type = "text/javascript" > </script>
        <script src="{{ static_url('scripts/inventory.js') }}"
             type = "application/javascript" > </script>
    </head>
    
    <body>
        <div>
            <h1>Burt' s Books < / h1 >
             < hr / >
             < p > < h2 > The Definitive Guide to the Internet < / h2 >
             < em > Anonymous < / em > < / p >
         < / div >
        
         < img src = "static/images/internet.jpg" alt = "The Definitive Guide to the Internet" / >
 
         < hr / >
         < input type = "hidden" id = "session" value = "{{ session }}" / >
         < div id = "add-to-cart" >
             < p > < span style = "color: red;" > Only < span id = "count" > { { count } } < / span >
             left in stock ! Order now ! < / span > < / p >
             < p > $ 20.00 < input type = "submit" value = "Add to Cart" id = "add-button" / > < / p >
         < / div >
         < div id = "remove-from-cart" style = "display: none;" >
             < p > < span style = "color: green;" > One copy is in your cart . < / p >
             < p > < input type = "submit" value = "Remove from Cart" id = "remove-button" / > < / p >
         < / div >
     < / body >
< / html >

[/crayon]

当 DetailHandler 返回这个 index.html 模板时.咱们能够很简单地将书籍的详细信息和包含请求的 javascript 代码返回给浏览器,这样咱们就能够动态地在页面显示惟一的 session id 和正确的库存统计信息. 最后,咱们来深刻探讨客户端的 javascript 代码,目前咱们使用的python 和 tornado 实现的图书管理系统中客户端代码是一个很是重要的例子,因此你必需要了解其中的细节.在例子5-6,咱们将会使用 jQuery 库对浏览器显示页面的效果进行操做. 例子5-6 inventory.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ ( document ) . ready ( function ( ) {
     document . session = $ ( '#session' ) . val ( ) ;
 
     setTimeout ( requestInventory , 100 ) ;
    
     $ ( '#add-button' ) . click ( function ( event ) {
         jQuery . ajax ( {
             url : '//localhost:8000/cart' ,
             type : 'POST' ,
             data : {
                 session : document . session ,
                 action : 'add'
             } ,
             dataType : 'json' ,
             beforeSend : function ( xhr , settings ) {
                 $ ( event . target ) . attr ( 'disabled' , 'disabled' ) ;
             } ,
             success : function ( data , status , xhr ) {
                 $ ( '#add-to-cart' ) . hide ( ) ;
                 $ ( '#remove-from-cart' ) . show ( ) ;
                 $ ( event . target ) . removeAttr ( 'disabled' ) ;
         }
     } ) ;
 
     $ ( '#remove-button' ) . click ( function ( event ) {
         jQuery . ajax ( {
             url : '//localhost:8000/cart' ,
             type : 'POST' ,
             data : {
                 session : document . session ,
                 action : 'remove'
             } ,
             dataType : 'json' ,
             beforeSend : function ( xhr , settings ) {
                 $ ( event . target ) . attr ( 'disabled' , 'disabled' ) ;
             } ,
             success : function ( data , status , xhr ) {
                 $ ( '#remove-from-cart' ) . hide ( ) ;
                 $ ( '#add-to-cart' ) . show ( ) ;
                 $ ( event . target ) . removeAttr ( 'disabled' ) ;
             }
         } ) ;
     } ) ;
} ) ;
 
function requestInventory ( ) {
     jQuery . getJSON ( '//localhost:8000/cart/status' , { session : document . session } ,
         function ( data , status , xhr ) {
             $ ( '#count' ) . html ( data [ 'inventoryCount' ] ) ;
             setTimeout ( requestInventory , 0 ) ;
         }
     ) ;
}

当文档加载完成以后,咱们须要添加一个单击事件的处理方法到 Add to Cart 按钮和一个隐藏的 Remove from Cart 按钮.这个事件处理的功能是让关联的 API 根据服务器的参数,显示 Add-To-Cart 或 Remove-From-Cart 中的一个.

1
2
3
4
5
6
7
8
function requestInventory ( ) {
     jQuery . getJSON ( '//localhost:8000/cart/status' , { session : document . session } ,
         function ( data , status , xhr ) {
             $ ( '#count' ) . html ( data [ 'inventoryCount' ] ) ;
             setTimeout ( requestInventory , 0 ) ;
         }
     ) ;
}

这个 requestInventory 函数调用了一个很短的延时,等待页面加载完成后执行.在函数的主体,咱们初始化了一个长轮询链接经过 HTTP Get 请求 /cart/status 的资源. 这个延迟容许加载的进程指示器在浏览器加载页面完成以后长轮询请求不会被 Esc 键或 停止按钮中断.当请求返回成功后,当前页面的数据将会被正确的统计数据替换. 图片5-5 展现了两个浏览器窗口中显示的全部库存清单. 图片5-5

如今,让咱们到服务器运行这个程序,你能够经过输入 URL 来查看书籍正确的库存统计数据.同时打开多个浏览器窗口查看详细信息的页面,并单击其中一个窗口的 Add to Cart 按钮.剩余书籍的数据将会马上更新到其它窗口中,就像图片5-6显示的那样. 这样的购物车实现可能有些简单,但能够确定的是,它在逻辑上确保了咱们不会超量销售.咱们尚未提到如何在同一台服务器中的两个 tornado 实例之间调用共享数据信息.这个实现咱们留给读者作为练习吧. 图片5-6

长轮询的缺陷

正如咱们看到的,HTTP 的长轮询在高度交互的页面反馈信息或用户状态上起着很是重要的做用.可是咱们仍然要注意其中存在的一些缺陷. 当咱们使用长轮询开发应用时,请记住,服务器是否能够控制浏览器请求超时的时间间隔,这是浏览器可能会出现异常中断,从新启动HTTP 链接,另外一种状况是,浏览器会限制打开一个特定主机的并发请求数,当某一个链接出现异常时,其它下载网站内容的请求可能也会受到限制. 此外,你还应该留意请求是如何影响服务器性能的.一旦大量的库存清单发生改变,全部的长链接请求须要同时响应变动并关闭链接时,服务器将会忽然接收到大量的浏览器从新创建链接的请求.对于应用程序来讲,相似于用户之间的聊天信息或通知,将会有少许的用户链接被关闭.这样的问题,如何处理.请查看下面的章节.

tornado 的 websockets

WebSockets 是html5中为客户端与服务端通讯提供的新协议.这个协议仍然是一个草案,因此至今只有一些最新版本的浏览器支持它.然而它拥有很是多的好处,以致于愈来愈多的浏览器开始支持它(对于web 开发来讲,最谨慎的作法是保持务实的策略,信赖新功能的可用性,若是有特别需求时也能够回滚到旧技术上) WebSockets 协议在客户端和服务端之间提供了一个持续的双向通讯链接.这个协议使用了一个新的 ws://URL 规则,可是其头部仍然是标准的 http.而且使用标准的 http 和 https 端口,它避免了许多使用 web 代理的网络链接到网站时出现的问题, html5 文档不只描述了它的通讯协议,还提供了在使用 WebSockets 时必需要在写入到浏览器客户端页面的 API 请求. 当 WebSockets 刚开始被一些新的浏览器支持的时候, tornado 已经提供了一个完整的模块支持它. WebSockets 值得咱们花费一些精力去了解如何在应用程序中使用它.

tornado 的 websocket 模块

tornado 的 websocket 模块提供了一个 websockethandler 类.这个类提供了如何在客户端链接中发起一个 websocket 事件并进行通讯. open 方法将会在客户端收到一个新消息时调用并经过 on_message 开一个新的 websocket 链接,在客户端关闭链接时调用 on_close关闭. 须要注意的是, WebSocktHandler 类提供了一个 write_message 方法发送信息到客户端,还提供了一个 close 方法去关闭这个链接.让咱们看看一个简单的例子,复制客户端发来的消息并回复给客户端.

1
2
3
4
5
6
class EchoHandler ( tornado . websocket . WebSocketHandler ) :
     def on_open ( self ) :
         self . write_message ( 'connected!' )
 
     def on_message ( self , message ) :
         self . write_message ( message )

正如你在咱们实现的 EchoHandler 看到的.咱们只是使用了 websockethandler 的基类中的 write_message 方法,经过执行 on_open 方法简单的发送了一个字符串 i”connected!” 返回给客户端,这个 on_message 方法在每一次收到客户端发来的新信息时都会执行. 而且咱们回复了一个相同的信息给客户端.它就是这样工做的,让咱们来看看如何经过这个协议去实现一个完整的例子.

例子:websockets实现的存货清单

咱们将会在这一部分的内容看到如何使用 websockets 更新上一个 http 长轮询的例子中的代码.请记住! websockets 是一个新标准,因此只有最新版本的浏览器提供了支持.tornado 支持的 websockets 协议版本须要在 firefox6.0以上版本, safari5.0.1, chrome6及以上版本和 internet explorer 10 开发版才能正常工做. 暂且不理会这些,让咱们来看看它的源代码.大部分的代码都没有改变,只有一部分服务端应用须要修改,如 shoppingcart 类和 statushandler 类. 例子5-7 看起来很是熟悉:

例子5-7: shopping_cart.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import tornado . web
import tornado . websocket
import tornado . httpserver
import tornado . ioloop
import tornado . options
from uuid import uuid4
 
class ShoppingCart ( object ) :
     totalInventory = 10
     callbacks = [ ]
     carts = { }
 
     def register ( self , callback ) :
         self . callbacks . append ( callback )
     def unregister ( self , callback ) :
         self . callbacks . remove ( callback )
     def moveItemToCart ( self , session ) :
         if session in self . carts :
             return
         self . carts [ session ] = True
         self . notifyCallbacks ( )
     def removeItemFromCart ( self , session ) :
         if session not in self . carts :
             return
     del ( self . carts [ session ] )
         self . notifyCallbacks ( )
     def notifyCallbacks ( self ) :
         for callback in self . callbacks :
             callback ( self . getInventoryCount ( ) )
     def getInventoryCount ( self ) :
         return self . totalInventory - len ( self . carts )
 
class DetailHandler ( tornado . web . RequestHandler ) :
     def get ( self ) :
         session = uuid4 ( )
         count = self . application . shoppingCart . getInventoryCount ( )
         self . render ( "index.html" , session = session , count = count )
 
class CartHandler ( tornado . web . RequestHandler ) :
     def post ( self ) :
     action = self . get_argument ( 'action' )
     session = self . get_argument ( 'session' )
     if not session :
         self . set_status ( 400 )
         return
     if action == 'add' :
         self . application . shoppingCart . moveItemToCart ( session )
     elif action == 'remove' :
         self . application . shoppingCart . removeItemFromCart ( session )
     else :
         self . set_status ( 400 )
 
class StatusHandler ( tornado . websocket . WebSocketHandler ) :
     def open ( self ) :
         self . application . shoppingCart . register ( self . callback )
     def on_close ( self ) :
         self . application . shoppingCart . unregister ( self . callback )
     def on_message ( self , message ) :
         pass
     def callback ( self , count ) :
         self . write_message ( '{"inventoryCount":"%d"}' % count )
 
class Application ( tornado . web . Application ) :
     def __init__ ( self ) :
         self . shoppingCart = ShoppingCart ( )
         handlers = [
             ( r '/' , DetailHandler ) ,
             ( r '/cart' , CartHandler ) ,
             ( r '/cart/status' , StatusHandler )
         ]
         settings = {
             'template_path' : 'templates' ,
             'static_path' : 'static'
         }
         tornado . web . Application . __init__ ( self , handlers , * * settings )
if __name__ == '__main__' :
     tornado . options . parse_command_line ( )
     app = Application ( )
     server = tornado . httpserver . HTTPServer ( app )
     server . listen ( 8000 )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

除了 import 导入的声明外,咱们只须要改变 shoppingcart 和statushandler 类. 首先要注意的是 tornado.websocker 模块必须用来获取 websockethandler 的功能. 在shoppingcart 类中,咱们须要对通知系统的 callback 作一些改变. websockets 一旦打开就会一直保持开启的状态,不须要从 callback 列表中移除这个通知. 只须要反复迭代这个列表,并调用 callback 获取最新的库存清单数据便可:

1
2
3
def notifyCallbacks ( self ) :
         for callback in self . callbacks :
             callback ( self . getInventoryCount ( ) )

另外一个改变是添加 unregister 方法, statushandler 将在 websocket 链接关闭时调用这个方法移除对应的 callback

1
2
def unregister ( self , callback ) :
         self . callbacks . remove ( callback )

在 statushandler 类中还有一个重要的改变.须要继承 tornado.websocket.websockethandler ,替换掉处理每个 http 方法的功能. websocket 操做执行在 open 和 on_message 方法.当链接打开或收到关闭链接的消息时分别调用它们.此外, on_close 方法在远程主机链接关闭时也会被调用.

1
2
3
4
5
6
7
8
9
class StatusHandler ( tornado . websocket . WebSocketHandler ) :
     def open ( self ) :
         self . application . shoppingCart . register ( self . callback )
     def on_close ( self ) :
         self . application . shoppingCart . unregister ( self . callback )
     def on_message ( self , message ) :
         pass
     def callback ( self , count ) :
         self . write_message ( '{"inventoryCount":"%d"}' % count )

在咱们的实现中,当 shoppingcart 开启一个新的链接时注册一个 callback 方法, 当链接关闭时, 注销这个 callback . 固然,咱们依旧使用 http API 调用 cartHandler 类, 咱们不用监听 websocket 链接是否有新的消息,由于 on_message 的实现是空的(咱们重载这个 on_message 实现是为了不在收到消息时, tornado 的NotImplementedError 意外抛出).最后 callback 方法在存货清单发生变动时,经过 websocket 链接通知客户端最新的数据. javascript 代码和上一个版本是相同的.咱们只须要改变 requestInventory 函数, 替换掉长轮询的 AJAX 请求. 咱们使用 html5 websocket API, 请查看 例子5-8的代码: 例子5-8 the new requestInventory function from inventory.js

1
2
3
4
5
6
7
8
9
function requestInventory ( ) {
     var host = 'ws://localhost:8000/cart/status' ;
     var websocket = new WebSocket ( host ) ;
     websocket . onopen = function ( evt ) { } ;
     websocket . onmessage = function ( evt ) {
         $ ( '#count' ) . html ( $ . parseJSON ( evt . data ) [ 'inventoryCount' ] ) ;
     } ;
     websocket . onerror = function ( evt ) { } ;
}

在经过URL ws://localhost:8000/cart/status建立一个新的 websocket 链接以后,咱们添加一个函数为每个实现发送咱们设置的响应.在这个例子中咱们只关心一个事件 onmessage ,与前面修改的 requestInventory 函数相似,咱们用它向全部目录更新相同的统计数据.(稍微不一样的地方在于,咱们在服务器发送的 JSON 对象须要进行分析) 像前面的例子同样,这个库存清单会在购物者添加书籍到他们的购物车时动态地调整库存清单.不一样的地方在于,咱们只须要为每个用户维护一个固定的 websocket 链接,替换掉了原先须要反复打开 http 请求的长轮询更新.

websockets的将来

websockets协议还在草案阶段,还须要进行许多修改才能定稿,然而,自从它提交到 IETF 进行终稿审核之后,实际上它已经不可能发生太大的改动.使用 websockets最大的缺陷咱们在刚开始已经提到了,那就是到目前为止,只有最新版本的浏览器才提供了对它的支持. 轻忽略掉以前的警告吧, websockets 在浏览器与服务器之间实现双向通讯这一方式的前景是光明的.这个协议将会获得充分的扩展和支持.咱们将会在愈来愈多优秀的应用上面看到它的实现.

这一章节翻译的很是痛苦,由于博主对异步的概念还有一些理不清楚的地方,因此翻译未必能完整的表达出做者想要表达的内容.本章依然求校订,有任何错误的地方,欢迎随时私信博主.

原创翻译:发布于http://blog.xihuan.de/tech/web/tornado/tornado_asynchronous_web_services.html

上一篇: 翻译:introduce to tornado - Databases

下一篇: 翻译: introduce to tornado - Writing Secure Applications

相关文章
相关标签/搜索