• WebSocket的应用
    • WebSocket简介
    • WebSocket服务器端编程
    • WebSocket客户端编程
    • 项目:Web聊天室

    WebSocket的应用

    Tornado的异步特性使其非常适合处理高并发的业务,同时也适合那些需要在客户端和服务器之间维持长连接的业务。传统的基于HTTP协议的Web应用,服务器和客户端(浏览器)的通信只能由客户端发起,这种单向请求注定了如果服务器有连续的状态变化,客户端(浏览器)是很难得知的。事实上,今天的很多Web应用都需要服务器主动向客户端(浏览器)发送数据,我们将这种通信方式称之为“推送”。过去很长一段时间,程序员都是用定时轮询(Polling)或长轮询(Long Polling)等方式来实现“推送”,但是这些都不是真正意义上的“推送”,而且浪费资源且效率低下。在HTML5时代,可以通过一种名为WebSocket的技术在服务器和客户端(浏览器)之间维持传输数据的长连接,这种方式可以实现真正的“推送”服务。

    WebSocket简介

    WebSocket 协议在2008年诞生,2011年成为国际标准(RFC 6455),现在的浏览器都能够支持它,它可以实现浏览器和服务器之间的全双工通信。我们之前学习或了解过Python的Socket编程,通过Socket编程,可以基于TCP或UDP进行数据传输;而WebSocket与之类似,只不过它是基于HTTP来实现通信握手,使用TCP来进行数据传输。WebSocket的出现打破了HTTP请求和响应只能一对一通信的模式,也改变了服务器只能被动接受客户端请求的状况。目前有很多Web应用是需要服务器主动向客户端发送信息的,例如股票信息的网站可能需要向浏览器发送股票涨停通知,社交网站可能需要向用户发送好友上线提醒或聊天信息。

    Day64 WebSocket的应用 - 图1

    WebSocket的特点如下所示:

    1. 建立在TCP协议之上,服务器端的实现比较容易。
    2. 与HTTP协议有着良好的兼容性,默认端口是80(WS)和443(WSS),通信握手阶段采用HTTP协议,能通过各种 HTTP 代理服务器(不容易被防火墙阻拦)。
    3. 数据格式比较轻量,性能开销小,通信高效。
    4. 可以发送文本,也可以发送二进制数据。
    5. 没有同源策略的限制,客户端(浏览器)可以与任意服务器通信。

    Day64 WebSocket的应用 - 图2

    WebSocket服务器端编程

    Tornado框架中有一个tornado.websocket.WebSocketHandler类专门用于处理来自WebSocket的请求,通过继承该类并重写openon_messageon_close 等方法来处理WebSocket通信,下面我们对WebSocketHandler的核心方法做一个简单的介绍。

    1. open(*args, **kwargs)方法:建立新的WebSocket连接后,Tornado框架会调用该方法,该方法的参数与RequestHandlerget方法的参数类似,这也就意味着在open方法中可以执行获取请求参数、读取Cookie信息这样的操作。

    2. on_message(message)方法:建立WebSocket之后,当收到来自客户端的消息时,Tornado框架会调用该方法,这样就可以对收到的消息进行对应的处理,必须重写这个方法。

    3. on_close()方法:当WebSocket被关闭时,Tornado框架会调用该方法,在该方法中可以通过close_codeclose_reason了解关闭的原因。

    4. write_message(message, binary=False)方法:将指定的消息通过WebSocket发送给客户端,可以传递utf-8字符序列或者字节序列,如果message是一个字典,将会执行JSON序列化。正常情况下,该方法会返回一个Future对象;如果WebSocket被关闭了,将引发WebSocketClosedError

    5. set_nodelay(value)方法:默认情况下,因为TCP的Nagle算法会导致短小的消息被延迟发送,在考虑到交互性的情况下就要通过将该方法的参数设置为True来避免延迟。

    6. close(code=None, reason=None)方法:主动关闭WebSocket,可以指定状态码(详见RFC 6455 7.4.1节)和原因。

    WebSocket客户端编程

    1. 创建WebSocket对象。

      1. var webSocket = new WebSocket('ws://localhost:8000/ws');

      说明:webSocket对象的readyState属性表示该对象当前状态,取值为CONNECTING-正在连接,OPEN-连接成功可以通信,CLOSING-正在关闭,CLOSED-已经关闭。

    2. 编写回调函数。

      1. webSocket.onopen = function(evt) { webSocket.send('...'); };
      2. webSocket.onmessage = function(evt) { console.log(evt.data); };
      3. webSocket.onclose = function(evt) {};
      4. webSocket.onerror = function(evt) {};

      说明:如果要绑定多个事件回调函数,可以用addEventListener方法。另外,通过事件对象的data属性获得的数据可能是字符串,也有可能是二进制数据,可以通过webSocket对象的binaryType属性(blob、arraybuffer)或者通过typeof、instanceof运算符检查类型进行判定。

    项目:Web聊天室

    1. """
    2. handlers.py - 用户登录和聊天的处理器
    3. """
    4. import tornado.web
    5. import tornado.websocket
    6. nicknames = set()
    7. connections = {}
    8. class LoginHandler(tornado.web.RequestHandler):
    9. def get(self):
    10. self.render('login.html', hint='')
    11. def post(self):
    12. nickname = self.get_argument('nickname')
    13. if nickname in nicknames:
    14. self.render('login.html', hint='昵称已被使用,请更换昵称')
    15. self.set_secure_cookie('nickname', nickname)
    16. self.render('chat.html')
    17. class ChatHandler(tornado.websocket.WebSocketHandler):
    18. def open(self):
    19. nickname = self.get_secure_cookie('nickname').decode()
    20. nicknames.add(nickname)
    21. for conn in connections.values():
    22. conn.write_message(f'~~~{nickname}进入了聊天室~~~')
    23. connections[nickname] = self
    24. def on_message(self, message):
    25. nickname = self.get_secure_cookie('nickname').decode()
    26. for conn in connections.values():
    27. if conn is not self:
    28. conn.write_message(f'{nickname}说:{message}')
    29. def on_close(self):
    30. nickname = self.get_secure_cookie('nickname').decode()
    31. del connections[nickname]
    32. nicknames.remove(nickname)
    33. for conn in connections.values():
    34. conn.write_message(f'~~~{nickname}离开了聊天室~~~')
    1. """
    2. run_chat_server.py - 聊天服务器
    3. """
    4. import os
    5. import tornado.web
    6. import tornado.ioloop
    7. from handlers import LoginHandler, ChatHandler
    8. if __name__ == '__main__':
    9. app = tornado.web.Application(
    10. handlers=[(r'/login', LoginHandler), (r'/chat', ChatHandler)],
    11. template_path=os.path.join(os.path.dirname(__file__), 'templates'),
    12. static_path=os.path.join(os.path.dirname(__file__), 'static'),
    13. cookie_secret='MWM2MzEyOWFlOWRiOWM2MGMzZThhYTk0ZDNlMDA0OTU=',
    14. )
    15. app.listen(8888)
    16. tornado.ioloop.IOLoop.current().start()
    1. <!-- login.html -->
    2. <!DOCTYPE html>
    3. <html lang="en">
    4. <head>
    5. <meta charset="utf-8">
    6. <title>Tornado聊天室</title>
    7. <style>
    8. .hint { color: red; font-size: 0.8em; }
    9. </style>
    10. </head>
    11. <body>
    12. <div>
    13. <div id="container">
    14. <h1>进入聊天室</h1>
    15. <hr>
    16. <p class="hint">{{hint}}</p>
    17. <form method="post" action="/login">
    18. <label>昵称:</label>
    19. <input type="text" placeholder="请输入你的昵称" name="nickname">
    20. <button type="submit">登录</button>
    21. </form>
    22. </div>
    23. </div>
    24. </body>
    25. </html>
    1. <!-- chat.html -->
    2. <!DOCTYPE html>
    3. <html lang="en">
    4. <head>
    5. <meta charset="UTF-8">
    6. <title>Tornado聊天室</title>
    7. </head>
    8. <body>
    9. <h1>聊天室</h1>
    10. <hr>
    11. <div>
    12. <textarea id="contents" rows="20" cols="120" readonly></textarea>
    13. </div>
    14. <div class="send">
    15. <input type="text" id="content" size="50">
    16. <input type="button" id="send" value="发送">
    17. </div>
    18. <p>
    19. <a id="quit" href="javascript:void(0);">退出聊天室</a>
    20. </p>
    21. <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    22. <script>
    23. $(function() {
    24. // 将内容追加到指定的文本区
    25. function appendContent($ta, message) {
    26. var contents = $ta.val();
    27. contents += '\n' + message;
    28. $ta.val(contents);
    29. $ta[0].scrollTop = $ta[0].scrollHeight;
    30. }
    31. // 通过WebSocket发送消息
    32. function sendMessage() {
    33. message = $('#content').val().trim();
    34. if (message.length > 0) {
    35. ws.send(message);
    36. appendContent($('#contents'), '我说:' + message);
    37. $('#content').val('');
    38. }
    39. }
    40. // 创建WebSocket对象
    41. var ws= new WebSocket('ws://localhost:8888/chat');
    42. // 连接建立后执行的回调函数
    43. ws.onopen = function(evt) {
    44. $('#contents').val('~~~欢迎您进入聊天室~~~');
    45. };
    46. // 收到消息后执行的回调函数
    47. ws.onmessage = function(evt) {
    48. appendContent($('#contents'), evt.data);
    49. };
    50. // 为发送按钮绑定点击事件回调函数
    51. $('#send').on('click', sendMessage);
    52. // 为文本框绑定按下回车事件回调函数
    53. $('#content').on('keypress', function(evt) {
    54. keycode = evt.keyCode || evt.which;
    55. if (keycode == 13) {
    56. sendMessage();
    57. }
    58. });
    59. // 为退出聊天室超链接绑定点击事件回调函数
    60. $('#quit').on('click', function(evt) {
    61. ws.close();
    62. location.href = '/login';
    63. });
    64. });
    65. </script>
    66. </body>
    67. </html>