8 应用 WebSocket

回顾 WebSocket

框架班的学习和练习

什么是 WebSocket

定义

WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。WebSocket API也被W3C定为标准。

WebSocket提供了客户端和服务端之间的全双工跨域通信,通过客户端和服务端之间建立WebSocket连接,在同一时刻能够实现客户端到服务器服务器到客户端的数据发送。

基本操作

​ (1). 开启连接
​ (2). 客户端给服务器端发送数据
​ (3). 服务器端接收数据
​ (4). 服务器端给客户端发送数据
​ (5). 客户端接收数据

优点

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

和 HTTP 对比

传统 HTTP 客户端与服务器的请求-响应模式 对比 WebSocket 模式

websocket2

适用场景

实时响应的应用

  • 聊天室
  • 通知等

限制:

不是所有浏览器都支持

WebSocket 关键方法

tornado 的 WebSocketHandler

  • open 客户端连接成功时,自动调用
  • on_message 客户端连发送消息时,自动调用
  • on_close 客户端关闭连接时,自动调用

浏览器客户端

  • ws = new WebSocket(url)
  • ws.onmessage 接受服务端发来的信息
  • ws.send() 发信息给服务端
  • 其他 ws.onopen, ws.onerror

客户端

文档全部加载完成的事件,也就是:

$(document).ready(function (){
}

因为并不是所有版本的浏览器都能够支持WebSocket,所以通过下面代码来兼容

// 创建空console对象,避免JS报错  
// 兼容Firefox/IE使用console.log
if (!window.console)
    window.console = {};
if (!window.console.log)
    window.console.log = function () {
    };

实现发送表单:messageform的submit事件:

$("#messageform").on("submit", function () {  // 点击提交时执行
        newMessage($(this)); // 发送新消息给服务器
        return false;
    });

一旦有message提交了,立马执行newMessage函数,也就是给服务器发消息

下面同样的作用,只不过是监控keyCode == 13的按键,也就是我们键盘上的enter键

 $("#messageform").on("keypress", function (e) {  // 回车提交时执行
        if (e.keyCode == 13) {
            newMessage($(this)); // 发送新消息给服务器
            return false;
        }
    });

其中的newmessage()函数实现如下:

// 发送新消息给服务器
function newMessage(form) {
    var message = form.formToDict(); // 将提取的数据转化成字典 {body: "2"}
    updater.socket.send(JSON.stringify(message)); // 向服务器发送json形式的新的消息 {"body":"2"}
    $("input[name='body']").val("").select(); //清空并选中输入框
}

webSocket.send()向服务器发送数据。

就是向服务器以json的格式,发送一个新的消息,其中的formToDict 函数实现如下:

// 将提取的数据转化成字典
jQuery.fn.formToDict = function () {
    var fields = this.serializeArray(); // [{name: "body", value: "2"}]
    var json = {};
    for (var i = 0; i < fields.length; i++) {
        json[fields[i].name] = fields[i].value; // json["body"]="2"
    }
    if (json.next)
        delete json.next;
    return json;  // {body: "2"}
};

作用是把表单中所有的输入保存到json对象中去,最后返回客户端要发给服务器的消息字典,也是一个json对象。

serializeArray()方法序列化表单元素,返回 JSON 数据结构数据。

注意:此方法返回的是 JSON 对象而非 JSON 字符串。需要使用插件或者第三方库进行字符串化操作。

返回的 JSON 对象是由一个对象数组组成的,其中每个对象包含一个或两个名值对 —— name 参数和 value 参数(如果 value 不为空的话)。举例来说:

[ 
  {name: 'firstname', value: 'Hello'}, 
  {name: 'lastname', value: 'World'},
  {name: 'alias'}, // 值为空
]

另外我这里设置了一个,一旦选中了我们之前html文件里定义的id为message编辑框的控件,就开始发起一个连接,动作如下:

$("#message").select();
updater.start();   // 开始 WebSocket

其中,start()内容大致为:

start: function () {
    var url = "ws://" + location.host + "/ws"; //ws://127.0.0.1:8000/ws
    updater.socket = new WebSocket(url);  // 初始化 WebSocket  客户端与服务器建立连接

    updater.socket.onmessage = function (event) {  // 获取到服务器的信息时响应
        updater.showMessage(JSON.parse(event.data));//信息展示在页面上
    }
},

这里一旦选中了编辑框,客户端js代码就开始新建一个websocket连接,其中url 就是我们服务器的地址,并且设置了我们的onmessage()函数,也就是响应服务器消息的函数,其内容大致如下:

showMessage: function (message) {
    var existing = $("#m" + message.id);
    if (existing.length > 0)
        return;
    var node = $(message.html);
    node.hide();
    $("#inbox").append(node);  // 添加消息 DIV 到页面
    node.slideDown();
}

先选中了消息,为新收到的消息建立一个新的节点node,先隐藏消息(因为发送出去要清空的啊),然后把消息添加到我们的html文件中的inbox标签的尾部,进行显示,inbox标签内容如下:

<div class="inbox">
    {% for message in messages %}
        {% include "message.html" %}
    {% end %}
</div>

WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。

var ws = new WebSocket('ws://127.0.0.1:8000/ws');

执行上面语句之后,客户端就会与服务器进行连接。

对于客户端,主要就是updater这个对象,该对象会创建并维护了一个WebSocket对象,通过这个WebSocket对象就可以跟服务端进行交互(收取或发送消息)。

var updater = {
    socket: null,

    start: function () {
        var url = "ws://" + location.host + "/ws"; //ws://127.0.0.1:8000/ws
        updater.socket = new WebSocket(url);  // 初始化 WebSocket  客户端与服务器建立连接
        
        // 获取到服务器的信息时响应
        updater.socket.onmessage = function (event) {
            updater.showMessage(JSON.parse(event.data));//信息展示在页面上
        }
    },

    showMessage: function (message) {
        var existing = $("#m" + message.id);
        if (existing.length > 0)
            return;
        var node = $(message.html);
        // node.hide();
        $("#inbox").append(node);  // 添加消息 DIV 到页面
        // node.slideDown();
    }
};

服务端

服务端通过ChatSocketHandler这个类来管理所有的消息,以及所有的WebSocket client。由于WebSocket是一种长连接的方式,所以可以很容易的统计出当前在线的client的数量。

class ChatSocketHandler(tornado.websocket.WebSocketHandler):
   """
   处理响应websocket连接
   """
   waiters = set()  # 等待接受信息的用户
   cache = []  # 存放历史信息的列表
   cache_size = 200  # 消息列表的大小

   def open(self, *args, **kwargs):
      """新的websocket连接打开时 自动调用此函数"""
      print('new connection:%s' % self)
      ChatSocketHandler.waiters.add(self)

   def on_close(self):
      """websocketa连接关闭时 自动调用此函数"""
      print('close connection: %s' % self)
      ChatSocketHandler.waiters.remove(self)

   @classmethod
   def update_cache(cls, message):
      """# 更新历史消息列表 加入新的消息"""
      cls.cache.append(message)
      if len(cls.cache) > cls.cache_size:
         cls.cache = cls.cache[-cls.cache_size:]

   @classmethod
   def send_updates(cls, chat):
      """# 向所有在线用户发送消息"""
      for waiter in cls.waiters:
         waiter.write_message(chat)

   def on_message(self, message):
      """websocket服务端接收到消息 自动调用此函数"""
      print('got message %s' % message) # got message {"body":"2"}
      parsed = tornado.escape.json_decode(message) # {"body":"2"}
      chat = {
         "id": str(uuid.uuid4()),
         "body": parsed["body"],
      }
      chat["html"] = tornado.escape.to_basestring(
         self.render_string("message.html", message=chat))

      ChatSocketHandler.update_cache(chat)
      ChatSocketHandler.send_updates(chat)

当客户端发起"/ws"请求后,服务器就会跟客户端建立连接,并将客户端加入ChatSocketHandler.waiters集合中;当客户端断开连接,就会将客户端从ChatSocketHandler.waiters集合中移除。

当服务器收到新消息后,自动调用on_message()函数,再通过ChatSocketHandler.send_updates()方法,将新消息推送到所有的客户端。

其中write_message()方法用于向客户端发送消息

用户发送的消息会以json的格式收到 {“body”:“2”},所以要先解析 tornado.escape.json_decode(),然后提取其中的信息 parsed[“body”],包装成我们自己的chat字典,然后群发:

parsed = tornado.escape.json_decode(message) # {"body":"2"}
chat = {
   "id": str(uuid.uuid4()),
   "body": parsed["body"],
}

广播消息也就是向在线的每一个用户,使用websocket的功能发送一次消息

历史记录的实现

可以缓存200条消息记录放在一个列表中(列表中每一项是一个chat的字典类型),每次新用户访问就把这个列表发给他,让他也能看到历史记录,每当记录满了,那就截取最后200条保存下来

cache = []  # 存放历史信息的列表
cache_size = 200  # 消息列表的大小


if len(cls.cache) > cls.cache_size:
         cls.cache = cls.cache[-cls.cache_size:]

历史消息的显示格式,历史消息也是新用户上线的时候服务器在渲染模板html的时候加入的参数,html模板中大致以如下格式进行渲染:

<div class="inbox">
    {% for message in messages %}
        {% include "message.html" %}
    {% end %}
</div>

聊天界面显示的每一条消息,按格式包装起来会更方便,所以每条消息就以一个格式包装与显示:

<div class="message" id="m{{ message["id"] }}">
    {% module linkify(message["body"]) %}
</div>

最后就是我们的消息内容输入与发送,这一部分需要服务器JavaScript代码把我们用户输入的消息接收处理,然后发给我们的服务器,服务器进行解析后给与响应,html格式大致如下:

<form action="/a/message/new" method="post" id="messageform">
  <table>
    <tr>
      <td><input name="body" id="message" style="width:500px"></td>
      <td style="padding-left:5px">
        <input type="submit" value="提交">
        <input type="hidden" name="next" value="{{ request.path }}">
      </td>
    </tr>
  </table>
</form>

作业

运行起来 WebSocket

code

app.py

import tornado.web
import tornado.options
import tornado.ioloop
from tornado.options import define, options

from handlers import main,auth,chat

define(name='port', default='8000', type=int, help='run port')


class Application(tornado.web.Application):
   def __init__(self):
      handlers = [
         (r'/', main.IndexHandler),
         (r'/explore', main.ExploreHandler),
         (r'/post/(?P<post_id>[0-9]+)', main.PostHandler),
         (r'/upload', main.UploadHandler),
         (r'/profile', main.ProfileHandler),
         (r'/login', auth.LoginHandler),
         (r'/logout', auth.LogoutHandler),
         (r'/signup', auth.SignupHandler),
         (r'/room', chat.RoomHandler),
         (r'/ws', chat.ChatSocketHandler),
      ]
      settings = dict(
         debug=True,
         template_path='templates',
         static_path='static',
         login_url='/login',
         cookie_secret='bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=',
         pycket={
            'engine': 'redis',
            'storage': {
               'host': 'localhost',
               'port': 6379,
               # 'password': '',
               'db_sessions': 5,  # redis db index
               'db_notifications': 11,
               'max_connections': 2 ** 30,
            },
            'cookies': {
               'expires_days': 30,
            },
         }
      )

      super(Application, self).__init__(handlers, **settings)


application = Application()

if __name__ == '__main__':
   tornado.options.parse_command_line()
   application.listen(options.port)
   print("Server start on port {}".format(str(options.port)))
   tornado.ioloop.IOLoop.current().start()

服务端chat.py

import tornado.web
import tornado.websocket
from .main import AuthBaseHandler
import uuid


class RoomHandler(AuthBaseHandler):
   """
   聊天室页面
   """

   def get(self, *args, **kwargs):
      self.render('room.html', messages=ChatSocketHandler.cache)


class ChatSocketHandler(tornado.websocket.WebSocketHandler):
   """
   处理响应websocket连接
   """
   waiters = set()  # 等待接受信息的用户
   cache = []  # 存放历史信息的列表
   cache_size = 200  # 消息列表的大小

   def open(self, *args, **kwargs):
      """新的websocket连接打开时 自动调用此函数"""
      print('new connection:%s' % self)
      ChatSocketHandler.waiters.add(self)

   def on_close(self):
      """websocketa连接关闭时 自动调用此函数"""
      print('close connection: %s' % self)
      ChatSocketHandler.waiters.remove(self)

   @classmethod
   def update_cache(cls, message):
      """# 更新历史消息列表 加入新的消息"""
      cls.cache.append(message)
      if len(cls.cache) > cls.cache_size:
         cls.cache = cls.cache[-cls.cache_size:]

   @classmethod
   def send_updates(cls, chat):
      """# 向所有在线用户发送消息"""
      for waiter in cls.waiters:
         waiter.write_message(chat)

   def on_message(self, message):
      """websocket服务端接收到消息 自动调用此函数"""
      print('got message %s' % message) # got message {"body":"2"}
      parsed = tornado.escape.json_decode(message) # {"body":"2"}
      chat = {
         "id": str(uuid.uuid4()),
         "body": parsed["body"],
      }
      chat["html"] = tornado.escape.to_basestring(
         self.render_string("message.html", message=chat))
      ChatSocketHandler.update_cache(chat) # 更新历史消息列表
      ChatSocketHandler.send_updates(chat) # 向所有在线用户发送消息

客户端chat.js

// 浏览器在页面加载完成后调用此方法
$(document).ready(function () {
    // 创建空console对象,避免JS报错  //兼容Firefox/IE使用console.log
    if (!window.console)
        window.console = {};
    if (!window.console.log)
        window.console.log = function () {
        };

    $("#messageform").on("submit", function () {  // 点击提交时执行
        newMessage($(this)); // 发送新消息给服务器
        return false;
    });
    $("#messageform").on("keypress", function (e) {  // 回车提交时执行
        if (e.keyCode == 13) {
            newMessage($(this)); // 发送新消息给服务器
            return false;
        }
    });
    $("#message").select(); // 选中输入框
    updater.start();   // 开始 WebSocket 连接服务器 获取信息 展示在页面上
});

// 发送新消息给服务器
function newMessage(form) {
    var message = form.formToDict(); // 将提取的数据转化成字典 {body: "2"}
    updater.socket.send(JSON.stringify(message)); // 向服务器发送json形式的新的消息 {"body":"2"}
    $("input[name='body']").val("").select(); //清空并选中输入框
}

// 将提取的数据转化成字典
jQuery.fn.formToDict = function () {
    var fields = this.serializeArray(); // [{name: "body", value: "2"}]
    var json = {};
    for (var i = 0; i < fields.length; i++) {
        json[fields[i].name] = fields[i].value; // json["body"]="2"
    }
    if (json.next)
        delete json.next;
    return json;  // {body: "2"}
};

var updater = {
    socket: null,

    start: function () {
        var url = "ws://" + location.host + "/ws"; //ws://127.0.0.1:8000/ws
        updater.socket = new WebSocket(url);  // 初始化 WebSocket  客户端与服务器建立连接

        // 收到服务器数据event.data后的回调函数
        updater.socket.onmessage = function (event) {
            updater.showMessage(JSON.parse(event.data));//信息展示在页面上
        }
    },

    showMessage: function (message) {
        var existing = $("#m" + message.id);
        if (existing.length > 0)
            return;
        var node = $(message.html);
        // node.hide();
        $("#inbox").append(node);  // 添加消息 DIV 到页面
        // node.slideDown();
    }
};

room.html

{% extends 'base.html' %}

{% block title %}
    room page
{% end %}

{% block content %}
    <div id="body">
      <div id="inbox">
        {% for message in messages %}
          {% include "message.html" %}
        {% end %}
      </div>
      <div id="input">
        <form action="#" method="post" id="messageform">
          <table>
            <tr>
              <td><input name="body" id="message" style="width:500px"></td>
              <td style="padding-left:5px">
                <input type="submit" value="提交">
                <input type="hidden" name="next" value="{{ request.path }}">
              </td>
            </tr>
          </table>
        </form>
      </div>
    </div>
{% end %}

{% block extra_scripts %}
    <script src="{{ static_url('js/chat.js') }}" type="text/javascript"></script>

{% end %}

message.html

<div class="message" id="m{{ message["id"] }}">
    {% module linkify(message["body"]) %}
</div>

base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ static_url('css/bootstrap.css') }}">
    <link rel="stylesheet" href="{{ static_url('font-awesome-4.7.0/css/font-awesome.css') }}">
    <title>{% block title %}base{% end %}</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <a class="navbar-brand" href="#">
            <i class="fa fa-camera"></i>
            Tudo 图片
        </a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item active">
                    <a class="nav-link" href="/">首页<span class="sr-only">(current)</span></a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/explore">发现</a>
                </li>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                        用户中心
                    </a>
                    <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                      <a class="dropdown-item" href="/profile">个人信息</a>
                      <a class="dropdown-item" href="#">收藏</a>
                      <div class="dropdown-divider"></div>
                      <a class="dropdown-item" href="#">Something else here</a>
                    </div>
                </li>
                <li class="nav-item">
                    <a class="nav-link disabled" href="/logout">{{ current_user }}登出</a>
                </li>
            </ul>
            <a class="btn btn-info" href="/upload">
                <i class="fa fa-upload"></i>
                上传
            </a>
        </div>
    </nav>

    <div class="container">
        {% block content %}
            base
        {% end %}
    </div>
    <script src="{{ static_url('js/jquery-3.3.1.slim.min.js') }}"></script>
    <script src="{{ static_url('js/popper.min.js') }}"></script>
    <script src="{{ static_url('js/bootstrap.js') }}"></script>
    {% block extra_scripts %}{% end %}
</body>
</html>

猜你喜欢

转载自blog.csdn.net/qq_14993591/article/details/83073157