在程序的最顶部,我们导入了一些Tornado模块。虽然Tornado还有另外一些有用的模块,但在这个例子中我们必须至少包含这四个模块。
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
Tornado包括了一个有用的模块(tornado.options)来从命令行中读取设置。我们在这里使用这个模块指定我们的应用监听HTTP请求的端口。
from tornado.options import define, options define("port", default=8000, help="run on the given port", type=int)
Tornado包括了一个有用的模块(tornado.options)来从命令行中读取设置。我们在这里使用这个模块指定我们的应用监听HTTP请求的端口。它的工作流程如下:如果一个与define语句中同名的设置在命令行中被给出,那么它将成为全局options的一个属性。如果用户运行程序时使用了–help选项,程序将打印出所有你定义的选项以及你在define函数的help参数中指定的文本。如果用户没有为这个选项指定值,则使用default的值进行代替。
greeting = self.get_argument('greeting', 'Hello')
Tornado的RequestHandler类有一系列有用的内建方法,包括get_argument,我们在这里从一个查询字符串中取得参数greeting的值。(如果这个参数没有出现在查询字符串中,Tornado将使用get_argument的第二个参数作为默认值。)
self.write(greeting + ', friendly user!')
RequestHandler的另一个有用的方法是write,它以一个字符串作为函数的参数,并将其写入到HTTP响应中。在这里,我们使用请求中greeting参数提供的值插入到greeting中,并写回到响应中。
if __name__ == "__main__":
tornado.options.parse_command_line()
app = tornado.web.Application(handlers=[(r"/", IndexHandler)])
这是真正使得Tornado运转起来的语句。首先,我们使用Tornado的options模块来解析命令行。然后我们创建了一个Tornado的Application类的实例。传递给Application类init方法的最重要的参数是handlers。它告诉Tornado应该用哪个类来响应请求。
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
从这里开始的代码将会被反复使用:一旦Application对象被创建,我们可以将其传递给Tornado的HTTPServer对象,然后使用我们在命令行指定的端口进行监听(通过options对象取出。)最后,在程序准备好接收HTTP请求后,我们创建一个Tornado的IOLoop的实例。
从一个传入的HTTP请求中获得信息(使用get_argument和传入到get和post的参数)以及写HTTP响应(使用write方法)
Tornado支持任何合法的HTTP请求(GET、POST、PUT、DELETE、HEAD、OPTIONS)。你可以非常容易地定义上述任一种方法的行为,只需要在RequestHandler类中使用同名的方法。
在一些情况下,tornado会自动设置状态码:
- 404 Not Found:请求的路径无法匹配任何RequestHandler类相对应的模式
- 400 Bad Request:调用了一个没有默认值的get_argument函数,并且没有发现给定名称的参数
- 405 Method Not Allowed:传入的请求使用了RequestHandler中没有定义的HTTP方法
- 500 Internal Server Error:当程序遇到任何不能让其退出的错误
- 200 OK:如果响应成功,并且没有其他返回码被设置
当上述任何一种错误发生时,Tornado将默认向客户端发送一个包含状态码和错误信息的简短片段。如果你想使用自己的方法代替默认的错误响应,你可以重写write_error方法在你的RequestHandler类中。
PyMongo是一个简单的包装MongoDB客户端API的Python库
首先,你需要导入PyMongo库,并创建一个到MongoDB数据库的连接。
>>>import pymongo
>>>conn = pymongo.Connection("localhost", 27017)
前面的代码向我们展示了如何连接运行在你本地机器上默认端口(27017)上的MongoDB服务器。如果你正在使用一个远程MongoDB服务器,替换localhost和27017为合适的值。
Connection对象可以让你访问你连接的服务器的任何一个数据库。你可以通过对象属性或像字典一样使用对象来获得代表一个特定数据库的对象。如果数据库不存在,则被自动建立。
>>>db = conn.example or: db = conn['example']
一个数据库可以拥有任意多个集合。一个集合就是放置一些相关文档的地方。我们使用MongoDB执行的大部分操作(查找文档、保存文档、删除文档)都是在一个集合对象上执行的。你可以在数据库对象上调用collection_names方法获得数据库中的集合列表。
>>>db.collection_names()
[]
当然,我们还没有在我们的数据库中添加任何集合,所以这个列表是空的。
当我们插入第一个文档时,MongoDB会自动创建集合。你可以在数据库对象上通过访问集合名字的属性来获得代表集合的对象,然后调用对象的insert方法指定一个Python字典来插入文档。比如,在下面的代码中,我们在集合widgets中插入了一个文档。因为widgets集合并不存在,MongoDB会在文档被添加时自动创建。
>>> widgets = db.widgets or: widgets = db['widgets'] (see below)
>>> widgets.insert({"foo": "bar"})
ObjectId('4eada0b5136fc4aa41000000')
>>> db.collection_names()
[u'widgets', u'system.indexes']
(system.indexes集合是MongoDB内部使用的。处于本章的目的,你可以忽略它。)
在之前展示的代码中,你既可以使用数据库对象的属性访问集合,也可以把数据库对象看作一个字典然后把集合名称作为键来访问。比如,如果db是一个pymongo数据库对象,那么db.widgets和db[‘widgets’]同样都可以访问这个集合。
MongoDB是一个”无模式”数据库:同一个集合中的文档通常拥有相同的结构,但是MongoDB中并不强制要求使用相同结构。在内部,MongoDB以一种称为BSON的类似JSON的二进制形式存储文档。PyMongo允许我们以Python字典的形式写和取出文档。
既然文档在数据库中,我们可以使用集合对象的find_one方法来取出文档。你可以通过传递一个键为文档名、值为你想要匹配的表达式的字典来告诉find_one找到 一个特定的文档。比如,我们想要返回文档名域name的值等于flibnip的文档(即,我们刚刚创建的文档),可以像下面这样调用find_oen方法:
>>> widgets.find_one({"name": "flibnip"})
{u'description': u'grade-A industrial flibnip',
u'_id': ObjectId('4eada3a4136fc4aa41000001'),
u'name': u'flibnip', u'quantity': 3}
请注意_id域。当你创建任何文档时,MongoDB都会自动添加这个域。它的值是一个ObjectID,一种保证文档唯一的BSON对象。
find_one方法返回的值是一个简单的Python字典。你可以从中访问独立的项,迭代它的键值对,或者就像使用其他Python字典那样修改值。
>>> doc = db.widgets.find_one({"name": "flibnip"})
>>> type(doc)
<type 'dict'>
>>> print doc['name']
flibnip
>>> doc['quantity'] = 4
然而,字典的改变并不会自动保存到数据库中。如果你希望把字典的改变保存,需要调用集合的save方法,并将修改后的字典作为参数进行传递:
>>> doc['quantity'] = 4
>>> db.widgets.save(doc)
>>> db.widgets.find_one({"name": "flibnip"})
{u'_id': ObjectId('4eb12f37136fc4b59d000000'),
u'description': u'grade-A industrial flibnip',
u'quantity': 4, u'name': u'flibnip'}
让我们在集合中添加更多的文档:
>>> widgets.insert({"name": "smorkeg", "description": "for external use only", "quantity": 4})
ObjectId('4eadaa5c136fc4aa41000002')
>>> widgets.insert({"name": "clobbasker", "description": "properties available on request", "quantity": 2})
ObjectId('4eadad79136fc4aa41000003')
我们可以通过调用集合的find方法来获得集合中所有文档的列表,然后迭代其结果:
>>> for doc in widgets.find():
... print doc
最后,我们可以使用集合的remove方法从集合中删除一个文档。remove方法和find、find_one一样,也可以使用一个字典参数来指定哪个文档需要被删除。比如,要删除所有name键的值为flipnip的文档,输入:
>>> widgets.remove({"name": "flibnip"})
Python的json模块并不知道如何转换MongoDB的ObjectID类型到JSON。
最简单的方法(也是我们在本章中采用的方法)是在我们序列化之前从字典里简单地删除_id键。
>>> del doc["_id"]
>>> json.dumps(doc)
'{"description": "grade-A industrial flibnip", "quantity": 4, "name": "flibnip"}'
一个更复杂的方法是使用PyMongo的json_util库,它同样可以帮你序列化其他MongoDB特定数据类型到JSON。
现在我们知道编写一个Web服务,可以访问MongoDB数据库中的数据。首先,我们要编写一个只从MongoDB读取数据的Web服务。然后,我们写一个可以读写数据的服务。
代码1-1 只读字典
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import pymongo
from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)
class Application(tornado.web.Application):
def __init__(self):
handlers = [(r"/(\w+)", WordHandler)]
conn = pymongo.Connection("localhost", 27017)
self.db = conn["example"]
tornado.web.Application.__init__(self, handlers, debug=True)
class WordHandler(tornado.web.RequestHandler):
def get(self, word):
coll = self.application.db.words
word_doc = coll.find_one({"word": word})
if word_doc:
del word_doc["_id"]
self.write(word_doc)
else:
self.set_status(404)
self.write({"error": "word not found"})
if __name__ == "__main__":
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
在命令行中像下面这样运行这个程序:
$ python definitions_readonly.py
现在使用curl或者你的浏览器来向应用发送一个请求。
$ curl http://localhost:8000/perturb
{"definition": "Bother, unsettle, modify", "word": "perturb"}
如果我们请求一个数据库中没有添加的单词,会得到一个404错误以及一个错误信息:
$ curl http://localhost:8000/snorkle
{"error": "word not found"}
代码1-2 可写字典
主要添加了post()方法:
def post(self, word):
definition = self.get_argument("definition")
coll = self.application.db.words
word_doc = coll.find_one({"word": word})
if word_doc:
word_doc['definition'] = definition
coll.save(word_doc)
else:
word_doc = {'word': word, 'definition': definition}
coll.insert(word_doc)
del word_doc["_id"]
self.write(word_doc)
Tornado给了我们更好的方法来处理这种情况:
应用程序在等待第一个处理完成的过程中,让I/O循环打开以便服务于其他客户端,直到处理完成时启动一个请求并给予反馈,而不再是等待请求完成的过程中挂起进程。
先是同步版本的核心代码:
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)
尽管应用程序本身响应相当快,但是向Twitter发送请求到获得返回的搜索数据之间有相当大的滞后。在同步(到目前为止,我们假定为单线程)应用,这意味着同时只能提供一个请求。所以,如果你的应用涉及一个2秒的API请求,你将每间隔一秒才能提供(最多!)一个请求。
Tornado包含一个AsyncHTTPClient类,可以执行异步HTTP请求。
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方法并不返回调用的结果。取而代之的是它指定了一个callback参数;你指定的方法或函数将在HTTP请求完成时被调用,并使用HTTPResponse作为其参数。
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搜索API请求到网页中的所有逻辑被搬到了on_response函数中。还需要注意的是@tornado.web.asynchronous装饰器的使用(在get方法的定义之前)以及在回调方法结尾处调用的self.finish()。
Tornado默认在函数处理返回时关闭客户端的连接。在通常情况下,这正是你想要的。但是当我们处理一个需要回调函数的异步请求时,我们需要连接保持开启状态直到回调函数执行完毕。你可以在你想改变其行为的方法上面使用@tornado.web.asynchronous装饰器来告诉Tornado保持连接开启,正如我们在异步版本的推率例子中IndexHandler的get方法中所做的。下面是相关的代码片段:
class IndexHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
query = self.get_argument('q')
[... other request handler code here...]
记住当你使用@tornado.web.asynchonous装饰器时,Tornado永远不会自己关闭连接。你必须在你的RequestHandler对象中调用finish方法来显式地告诉Tornado关闭连接。(否则,请求将可能挂起,浏览器可能不会显示我们已经发送给客户端的数据。)在前面的异步示例中,我们在on_response函数的write后面调用了finish方法:
[... 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()
不幸的是,之前的还是有点麻烦:为了处理请求 ,我们不得不把我们的代码分割成两个不同的方法。当我们有两个或更多的异步请求要执行的时候,编码和维护都显得非常困难,每个都依赖于前面的调用:不久你就会发现自己调用了一个回调函数的回调函数的回调函数。
Tornado 2.1版本引入了tornado.gen模块,可以提供一个更整洁的方式来执行异步请求。
下面是相关的代码部分:
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)
HTTP长轮询的主要吸引力在于其极大地减少了Web服务器的负载。相对于客户端制造大量的短而频繁的请求(以及每次处理HTTP头部产生的开销),服务器端只有当其接收一个初始请求和再次发送响应时处理连接。大部分时间没有新的数据,连接也不会消耗任何处理器资源。
浏览器兼容性是另一个巨大的好处。任何支持AJAX请求的浏览器都可以执行推送请求。不需要任何浏览器插件或其他附加组件。对比其他服务器端推送技术,HTTP长轮询最终成为了被广泛使用的少数几个可行方案之一。
这个应用提供一个带有”Add to Cart”按钮的HTML书籍细节页面,以及书籍剩余库存的计数。一个购物者将书籍添加到购物车之后,其他访问这个站点的访客可以立刻看到库存的减少。
为了提供库存更新,我们需要编写一个在初始化处理方法调用后不会立即关闭HTTP连接的RequestHandler子类。我们使用Tornado内建的asynchronous装饰器完成这项工作,如代码所示:
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()
Tornado在websocket模块中提供了一个WebSocketHandler类。这个类提供了和已连接的客户端通信的WebSocket事件和方法的钩子。当一个新的WebSocket连接打开时,open方法被调用,而on_message和on_close方法分别在连接接收到新的消息和客户端关闭时被调用。
此外,WebSocketHandler类还提供了write_message方法用于向客户端发送消息,close方法用于关闭连接。
class EchoHandler(tornado.websocket.WebSocketHandler):
def open(self):
self.write_message('connected!')
def on_message(self, message):
self.write_message(message)
正如你在我们的EchoHandler实现中所看到的,open方法只是使用WebSocketHandler基类提供的write_message方法向客户端发送字符串”connected!”。每次处理程序从客户端接收到一个新的消息时调用on_message方法,我们的实现中将客户端提供的消息原样返回给客户端。
还需要注意的是,为了获得WebSocketHandler的功能,需要使用tornado.websocket模块。
在ShoppingCart类中,我们只需要在通知回调函数的方式上做一个轻微的改变。因为WebSOckets在一个消息发送后保持打开状态,我们不需要在它们被通知后移除内部的回调函数列表。我们只需要迭代列表并调用带有当前库存量的回调函数:
def notifyCallbacks(self):
for callback in self.callbacks:
callback(self.getInventoryCount())
另一个改变是添加了unregisted方法。StatusHandler会在WebSocket连接关闭时调用该方法移除一个回调函数。
def unregister(self, callback):
self.callbacks.remove(callback)
大部分改变是在继承自tornado.websocket.WebSocketHandler的StatusHandler类中的。WebSocket处理函数实现了open和on_message方法,分别在连接打开和接收到消息时被调用,而不是为每个HTTP方法实现处理函数。此外,on_close方法在连接被远程主机关闭时被调用。
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方法,并在连接关闭时注销了这个回调函数。因为我们依然使用了CartHandler类的HTTP API调用,因此不需要监听WebSocket连接中的新消息,所以on_message实现是空的。(我们覆写了on_message的默认实现以防止在我们接收消息时Tornado抛出NotImplementedError异常。)最后,callback方法在库存改变时向WebSocket连接写消息内容。