翻译:introduce to tornado-Asynchronous Web Services

不打扰是莪最后的温柔 提交于 2019-12-05 01:25:55

异步的web请求

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

异步的web请求

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

图片5-1

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

你不需要深入了解例子中的twitter搜索API是如何工作的,当然你如果想要成为这方面的专家,可以通过阅读这些开发文档了解具体的实现:twitter search API

开始同步处理

例子5-1的源代码是我们统计tweer响应时间例子的同步处理版本.请记住:我们在最开始导入了tornado的 httpclient 模块.我们将会使用模块中的 httpclient 类去之行我们的http请求. 然后,我们将会使用模块中另一个类 AsyncHTTPClient . 例子5-1:同步的HTTP请求:tweet_rate.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
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并执行.这里是对应功能的代码:

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

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

阻塞带来的麻烦

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

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

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

基本的异步调用

非常幸运,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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!