这几天忙着研究Tornado,想着总得学以至用吧,因而就决定作个聊天室玩玩。
实际上在Tornado的源码里就有chat和websocket这2个聊天室的demo,分别采用Ajax Long-Polling和WebSocket技术构建。
而我要实现的则很简单:将这2种技术融合在一块儿。
固然,这样作并非为了好玩。
就技术而言,WebSocket的通讯开销不多,没有链接数的限制,但因为自己比较新,支持它的浏览器并很少(目前仅有Chrome 6+、Safari 5.0.1+、Firefox 4+和Opera 11+,且Firefox和Opera还因安全缘由默认禁用了)。
而现代的浏览器中,只要能用JavaScript的,几乎都支持Ajax,连古老的IE 6都不例外。但与WebSocket相比,每次通讯都须要传递header,这在小数据量的通讯时显得很低效。
因此若是实现2种技术,根据浏览器的支持度来自动切换,天然是一种较好的方式。
其实还有经过Flash来模拟WebSocket的,不过我是很讨厌Flash的,因而就无视了。另外还有用iframe实现的,感受比较影响用户体验,也无视。
考 虑到通讯开销,Ajax还须要与长链接技术搭配,以免客户端盲目地轮询,减小请求的数目。这里又存在一个问题:IE不支持在readyState为3时 读取服务器返回的数据,也就是不支持streaming方式。虽然说我从来就无视IE,但jQuery封装的ajax函数也不支持streaming方式, 让我去写原生的Ajax代码太麻烦了,因而只好采用long-polling方式了。
那么streaming和long-polling的差异在哪呢?
它们都是由客户端发起请求,服务器并不急于返回响应,等到事件发生后,才输出响应。
这时候,streaming方式并不关闭链接,所以服务器能够在将来的任意时刻继续发送响应;同时,客户端也会捕捉到这个响应事件,只不过readyState为3。
而若是用long-polling方式的话,服务器发送完响应就关闭链接;此时客户端检测到readyState为4,不存在兼容性问题;而后客户端再次发起Ajax请求,进入下一个轮回。
因而可知,long-polling方式在断开链接和从新链接时会存在时间差,所以若是不保存这段期间的事件的话,未连上的客户端就不会接收到。此外,从新链接也就意味着更多的通讯开销——TCP 3次握手和发送header。
值得一提的是,即便是streaming方式,由于服务器端阻塞了响应,客户端的更新须要经过另外一个Ajax请求来完成。而WebSocket没有这个限制,客户端能够随时用它发送数据。
此 外,HTTP 1.1还规定了客户端不该该与服务器端创建超过2个的HTTP链接,不然新链接会被阻塞。这也就意味着若是一个浏览器与一个服务器创建了2个长链接(不管 是在一个页面中,仍是2个窗口或标签中),那么就没法发起新请求了,包括Ajax请求和打开页面。这对streaming和long-polling来讲 都是一个不小的限制。
那么HTTP 1.0是怎样规定的呢?答案就是发送完了响应就必须关闭链接,所以streaming也被枪毙了。
而WebSocket采用的是WebSocket协议,并无规定链接数的限制。
原理介绍完了,就该开工了,首先来实现WebSocket:
import logging import os.path import uuid import tornado.httpserver import tornado.ioloop import tornado.options import tornado.web import tornado.websocket def send_message(message): for handler in ChatSocketHandler.socket_handlers: try:
handler.write_message(message) except:
logging.error('Error sending message', exc_info=True) class MainHandler(tornado.web.RequestHandler): def get(self): self.render('index.html') class ChatSocketHandler(tornado.websocket.WebSocketHandler): socket_handlers = set() def open(self): ChatSocketHandler.socket_handlers.add(self)
send_message('A new user has entered the chat room.') def on_close(self): ChatSocketHandler.socket_handlers.remove(self)
send_message('A user has left the chat room.') def on_message(self, message): send_message(message) def main(): settings = { 'template_path': os.path.join(os.path.dirname(__file__), 'templates'), 'static_path': os.path.join(os.path.dirname(__file__), 'static')
}
application = tornado.web.Application([
('/', MainHandler),
('/new-msg/', ChatHandler),
('/new-msg/socket', ChatSocketHandler)
], **settings)
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8000)
tornado.ioloop.IOLoop.instance().start() if __name__ == '__main__':
main()
看上去很简单。实际上Tornado提供了tornado.websocket.WebSocketHandler这个类,所以只须要实现open、on_close和on_message这3个方法就好了。
而我在open()的时候保存了handler,它与创建好的WebSocket是一一对应的关系,因此在发送信息时,只须要遍历ChatSocketHandler.socket_handlers就好了。
简单起见,我就没有保存信息队列了。这在WebSocket方式中并无问题,但Ajax Long-Polling方式会存在丢失事件的风险,因此若是要完善这个demo的话,这里须要特别注意。
接着看客户端的代码,简单起见我就没去管样式什么的了:
<!DOCTYPE html> <html> <head> <title>chat demo</title> </head> <body> <form action="/new-msg/" method="post"> <textarea id="text"></textarea> <input type="submit"/> </form> <div id="msg"></div> <script src="{{ static_url('jquery-1.6.4.js') }}"></script> <script src="{{ static_url('chat.js') }}"></script> </body> </html>
chat.js:
(function() { var $msg = $('#msg'); var $text = $('#text'); var WebSocket = window.WebSocket || window.MozWebSocket; if (WebSocket) { try { var socket = new WebSocket('ws://localhost:8000/new-msg/socket');
} catch (e) {}
} if (socket) {
socket.onmessage = function(event) { $msg.append('<p>' + event.data + '</p>');
}
$('form').submit(function() { socket.send($text.val());
$text.val('').select(); return false;
});
}
})();
一样是简单到不行了,建立一个WebSocket对象,而后实现onmessage方法便可获取服务器端的更新,发送数据则用send方法。此外还有onopen、onclose、onerror和close方法,都顾名思义而无需解释。
接着实现Ajax Long-Polling,它使用的是普通的tornado.web.RequestHandler类。
class ChatHandler(tornado.web.RequestHandler): callbacks = set()
users = set() @tornado.web.asynchronous def get(self): ChatHandler.callbacks.add(self.on_new_message)
self.user = user = self.get_cookie('user') if not user:
self.user = user = str(uuid.uuid4())
self.set_cookie('user', user) if user not in ChatHandler.users:
ChatHandler.users.add(user)
send_message('A new user has entered the chat room.') def on_new_message(self, message): if self.request.connection.stream.closed(): return self.write(message)
self.finish() def on_connection_close(self): ChatHandler.callbacks.remove(self.on_new_message)
ChatHandler.users.discard(self.user)
send_message('A user has left the chat room.') def post(self): send_message(self.get_argument('text'))
这里我用get来获取更新,post来发送信息。其中获取更新须要阻塞,所以要用@tornado.web.asynchronous修饰。
和WebSocket不一样的是,此次我保存的是callback,而非handler。
由 于每次广播信息都须要断开和从新链接,我就不能直接在get时断定用户有新用户进入。而我又懒得让客户端发送用户标识,因而就直接在cookie中进行设 置了。这个cookie是session类型,本站的全部窗口关闭后就实效,再次打开就会生成一个新的,正好符合个人需求。
而send_message也须要兼容新方式:
def send_message(message): for handler in ChatSocketHandler.socket_handlers: try:
handler.write_message(message) except:
logging.error('Error sending message', exc_info=True) for callback in ChatHandler.callbacks: try:
callback(message) except:
logging.error('Error in callback', exc_info=True)
ChatHandler.callbacks = set()
最后是客户端:
if (socket) { // ... } else { var error_sleep_time = 500; function poll() { $.ajax({
url: '/new-msg/',
type: 'GET',
success: function(event) { $msg.append('<p>' + event + '</p>');
error_sleep_time = 500;
poll();
},
error: function() { error_sleep_time *= 2;
setTimeout(poll, error_sleep_time);
}
});
}
poll();
$('form').submit(function() { $.ajax({
url: '/new-msg/',
type: 'POST',
data: {text: $text.val()},
success: function() { $text.val('').select();
}
}); return false;
});
}
稍微比WebSocket复杂一点,不过仍是很容易理解的。 试验一番后发现,WebSocket方式工做很是正常,只不过Chrome的调试控制台无法看到传输的数据。 而Ajax Long-Polling方式在打开2个标签时出现异常,只有1个标签能接收到更新,但发送新信息的请求并没被阻塞。 最后还得赞一句Tornado,对长链接的支持很是好,短短几行代码就能完成想要的功能。 此外还但愿愈来愈多的客户端和服务器可以支持WebSocket,毕竟它除了兼容性之外,没有其余缺点了。不但性能更好,限制更少,实现起来也更加轻松。