5.3 Tornado 与 WebSockets
WebSockets 是 HTML5 规范中新提出的客户-服务器通讯协议。这个协议目前仍是草案,只有最新的一些浏览器可以支持它。但是,它的好处是显而易见的,随着支持它的浏览器越来越多,我们将看到它越来越流行。(和以往的 Web 开发一样,必须谨慎地坚持依赖可用的新功能并能在必要时回滚到旧技术的务实策略。)
WebSocket 协议提供了在客户端和服务器间持久连接的双向通信。协议本身使用新的 ws://URL 格式,但它是在标准 HTTP 上实现的。通过使用 HTTP 和 HTTPS 端口,它避免了从 Web 代理后的网络连接站点时引入的各种问题。HTML5 规范不只描述了协议本身,还描述了使用 WebSockets 编写客户端代码所需要的浏览器 API。
由于 WebSocket 已经在一些最新的浏览器中被支持,并且 Tornado 为之提供了一些有用的模块,因此来看看如何使用 WebSockets 实现应用是非常值得的。
5.3.1 Tornado 的 WebSocket 模块
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 方法,我们的实现中将客户端提供的消息原样返回给客户端。这就是全部!让我们通过一个完整的例子看看实现这个协议是如何简单的吧。
5.3.2 示例:使用 WebSockets 的实时库存
在本节中,我们可以看到把之前使用 HTTP 长轮询的例子更新为使用 WebSockets 是如何简单。但是,请记住,WebSockets 还是一个新标准,只有最新的浏览器版本可以支持它。Tornado 支持的特定版本的 WebSocket 协议版本只在 Firefox 6.0 或以上、Safari 5.0.1 或以上、Chrome 6 或以上、IE 10 预览版或以上版本的浏览器中可用。
不去管免责声明,让我们先看看源码吧。除了服务器应用需要在 ShoppingCart 和 StatusHandler 类中做一些修改外,大部分代码保持和之前一样。代码清单 5-7 看起来会很熟悉。
代码清单 5-7 WebSockets:shopping_cart.py
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()
除了额外的导入语句外,我们只需要改变 ShoppingCart 和 StatusHandler 类。首先需要注意的是,为了获得 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 连接写消息内容。
这个版本的 JavaScript 代码和之前的非常相似。我们只需要改变其中的 requestInventory 函数。我们使用 HTML5 WebSocket API 取代长轮询资源的 AJAX 请求。参见代码清单 5-8.
代码清单 5-8 WebSockets:inventory.js 中新的 requestInventory 函数
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) { };
}
在创建了一个到 ws://localhost:8000/cart/status 的心得 WebSocket 连接后,我们为每个希望响应的事件添加了处理函数。在这个例子中我们唯一关心的事件是 onmessage,和之前版本的 requestInventory 函数一样更新 count 的内容。(轻微的不同是我们必须手工解析服务器送来的 JSON 对象。)
就像前面的例子一样,在购物者添加书籍到购物车时库存量会实时更新。不同之处在于一个持久的 WebSocket 连接取代了每次长轮询更新中重新打开的 HTTP 请求。
5.3.3 WebSockets 的未来
WebSocket 协议目前仍是草案,在它完成时可能还会修改。然而,因为这个规范已经被提交到 IETF 进行最终审查,相对而言不太可能会再面临重大的改变。正如本节开头所提到的那样,WebSocket 的主要缺陷是目前只支持最新的一些浏览器。
尽管有上述警告,WebSockets 仍然是在浏览器和服务器之间实现双向通信的一个有前途的新方法。当协议得到了广泛的支持后,我们将开始看到更加著名的应用的实现。
[1] 书中网页已不存在,替换为当前网址。
[2] 下面的这组代码书中使用的不是前面的代码,这里为了保持一致修改为和前面的代码一样。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论