zoukankan      html  css  js  c++  java
  • WebSocket 实现服务端给客户端推送消息

    代码发布

    服务端主动给客户端推送消息

    截至目前为止,我们所写的 web 项目基本都是基于 HTTP 协议的

    HTTP 协议有四大特性:无链接

    基于 HTTP 协议实现服务端主动给客户端推送消息好像有点麻烦~~~

    我们都经历过,浏览器打开一个网站不动,网站过一会儿自动弹出消息

    再比如网页版本的微信和 qq,我们所有人创建一个群聊,所有人加入群聊之后都不动

    我朝群中发送一个消息,你们所有人的页面上都会出现我发送的消息

    应用场景

    • 大屏幕投票实时展示
    • 任务的执行流程
    • 群聊功能

    ajax 操作

    异步提交,局部刷新

    用它就可以偷偷的朝服务端发送请求

    $.ajax({
    url:'',  # 控制后端提交路径
    type:'',  # 控制请求方式 
    data:{},  # 控制提交的数据
    dataType:"JSON",  # django后端用HttpResponse返回json格式字符串,args不会自动反序列化,拿到的还是json格式字符串string字符类型,而如果是用JsonResponse返回的那么args会自动返序列化成前端js的对象类型
    success:function(args){
    	# 异步回调机制
    })
    
    def index(request):
     if request.method == 'POST':
         back_dic = {'msg':'hahaha'}
         return HttpResponse(json.dumps(back_dic))  # 	需要
         return JsonResponse(back_dic)  # 不需要
     return render(request,'index.html')
    # 后续在写ajax请求的时候建议你加上dataType参数
    

    队列

    队列:先进先出

    堆栈:先进后出

    python 内部在内存中帮我们维护了一个队列

    import queue
    
    # 创建一个队列
    q = queue.Queue()
    
    # 往队列中添加数据
    q.put(111)
    q.put(222)
    
    # 从队列中取数据
    v1 = q.get()
    v2 = q.get()
    # v3 = q.get()  # 没有数据原地阻塞直到有数据
    # v4 = q.get_nowait()  # 没有数据直接报错
    try:
     v5 = q.get(timeout=3)  # 没有数据等待10s再没有就报错 queue.Empty
    except queue.Empty as e:
     pass
    print(v1,v2)
    
    # 实际生产中不会使用上述的消息队列 会使用功能更加的强大的
    """
    消息队列
    	redis
    	kafka
    	rebittMQ
    """
    

    基于 ajax 与队列其实就可以实现服务端给客户端推送消息的效果

    服务端给每一个客户端维护一个队列,然后再浏览器上面通过 ajax 请求朝对应队列获取数据,没有数据就原地阻塞(pending状态),有就直接拿走渲染即可

    群聊:获取群聊中某个人发送的消息,将该消息给每一个队列

    递归

    # python中有最大递归限制 997 998 官网给出的是1000
    """
    在python中是没有尾递归优化的!!!
    """
    def func():
    func()
    func()  # 不行
    
    # 在js中 是没有递归的概念的 函数可以自己调用自己 属于正常的事件机制
    function func1(){
    $.ajax({
     url:'',
     type:'',
     data:'',
     dataType:'JSON',
     success:function({
       func1()  # 可以
     })
    })
    }
    func1()
    

    校验性组件

    forms 组件

    modelform 组件(它是forms组件的加强版本,功能和代码差不多,但是更加的方便)

    如何实现服务端主动给客户端推送消息的效果

    伪实现

    可不可以让客户端浏览器每隔一段时间偷偷的去服务器请求数据

    这样能实现效果,但是内部本质还是客户端朝服务端发送消息

    • 轮询
    • 长轮询

    真实现

    • Websocket

    它的诞生真正的实现了服务端主动给客户端推送消息

    轮询(效率极低,基本不用)

    让浏览器定时(例如每隔 5 秒发一次)通过 ajax 朝服务端发送请求获取数据

    缺点:

    消息延迟严重
    请求次数多 消耗资源过大

    长轮询(兼容性好)

    服务端给每个浏览器创建一个队列,让浏览器通过 ajax 向后端偷偷的发送请求,去各自对应的队列中获取数据,如果没有数据则会有阻塞,但是不会一直阻塞,比如最多阻塞 30 秒(pending)后给一个响应,无论响应是否是真正的数据,都会再次通过回调函数调用请求数据的代码

    优点:

    消息基本没有延迟
    请求次数降低 消耗资源减少

    大公司需要考虑兼容性问题 追求兼容 目前网页版本的微信和 qq 用的就是长轮询


    ps:给标签绑定事件的方式大致有两种

    1 标签查找绑定

    $('p').click()
    

    2 直接写函数 注意括号不能少

    <p onclick="sendMsg()"></p>
    

    基于 ajax,队列以及异常处理实现简易版本的群聊功能(长轮询)

    后端

    import queue
    
    q_dict = {}  # {唯一标示:对应的队列,唯一标示:对应的队列}
    
    def home(request):
     # 获取客户端浏览器的唯一标识
     name = request.GET.get('name')
     # 生成一一对应关系
     q_dict[name] = queue.Queue()
     return render(request,'home.html',locals())  # locals 返回给模板
    
    def send_msg(request):
     if request.method == 'POST':
         # 获取用户发送的消息
         message = request.POST.get('content')
         print(message)
         # 将消息给所有的队列发送一份
         for q in q_dict.values():
             q.put(message)
         return HttpResponse('OK')
    
    def get_msg(request):
     # 获取用户唯一标示
     name = request.GET.get('name')
     # 回去对应的队列
     q = q_dict.get(name)
     back_dic = {'status':True,'msg':''}
     try:
         data = q.get(timeout=10)
         back_dic['msg'] = data
     except queue.Empty as e:
         back_dic['status'] = False
     return JsonResponse(back_dic)
    

    前端

    <h1>聊天室:{{ name }}</h1>
    <input type="text" id="txt">
    <button onclick="sendMsg()">提交</button>
    
    <h1>聊天记录</h1>
    <div class="record">
    
    </div>
    
    <script>
    function sendMsg() {
         // 朝后端发送消息
        $.ajax({
            url:'/send_msg/',
            type:'post',
            dataType:'JSON',
            data:{'content':$('#txt').val()},
            success:function (args) {
    
            }
        })
    }
    
    function getMsg() {
         // 偷偷的朝服务端要数据
         $.ajax({
             url:'/get_msg/',
             type:'get',
             data:{'name':'{{ name }}'},
             success:function (args) {
                 if (args.status){
                     // 获取消息 动态渲染到页面上
                     // 1 创建一个p标签
                     var pEle = $('<p>');
                     // 2 给p标签设置文本内容
                     pEle.text(args.msg);
                     // 3 将p标签添加到div内部
                     $('.record').append(pEle)
                 }
                 getMsg()
             }
         })
    }
    // 页面加载完毕立刻执行
    $(function () {
         getMsg()
    })
    </script>
    

    websocker(主流浏览器都支持)

    网络协议

    • HTTP 不加密传输

    • HTTPS 加密传输

      上面两个都是短链接/无链接

    • WebSocket 加密传输
      浏览器和服务端创建链接之后默认不断开(联想网络编程TCP recv和send方法)
      它的诞生能够真正的实现 服务端给客户端推送消息

    内部原理

    websocket 实现原理可以分为两部分

    1 握手环节(handshake):并不是所有的服务端都支持 websocket 所以用握手环节来验证服务端是否支持 websocket
    2 收发数据环节:数据解密

    握手环节

    浏览器访问 服务端之后,浏览器会立刻生成一个随机字符串

    浏览器会将生成好的随机字符串发送给服务端(基于 HTTP 协议 放在请求头中),并且自己也保留一份

    服务端和客户端都会对该随机字符串做以下处理

    • 先拿随机字符串跟 magic string (固定的字符串)做字符串的拼接
    • 将拼接之后的结果做加密处理 (sha1+base64)

    服务端将生成好的处理结果发送给浏览器(基于 HTTP 协议 放在响应头中)

    浏览器接受服务端发送过来的随机字符串,跟本地处理好的随机字符串做比对,如果一致说明服务端支持 websocket,如果不一致说明不支持

    收发数据环节

    前提知识点:
    1.基于网络传输数据都是二进制格式,在 python 中可以用 bytes 类型对应
    2.进制换算

    先读取第二个字节的后七位数据 (payload) 根据 payload 做不同的处理

    =127:继续往后读取 8 个字节数据(数据报10个字节)

    =126:继续往后读取2个字节数据(数据报4个字节)

    <=125:不再往后读取(数据2个字节)

    上述操作完成后,会继续往后读取固定长度4个字节的数据 (masking-key)

    依据 masking-key 解析出真实数据

    关键字:sha1/base64、magic string、payload(127,126,125)、masking-key


    代码验证(了解)

    # 请求头中的随机字符串
    Sec-WebSocket-Key: NlNG/FK/FrQS/RH5Bcy9Gw==
    # 响应头
    tpl = "HTTP/1.1 101 Switching Protocols
    " 
          "Upgrade:websocket
    " 
          "Connection: Upgrade
    " 
          "Sec-WebSocket-Accept: %s
    " 
          "WebSocket-Location: ws://127.0.0.1:8080
    
    "
    response_str = tpl %ac.decode('utf-8')  # 处理到响应头中
    
    import socket
    import hashlib
    import base64
    
    # 正常的socket代码
    sock = socket.socket()  # 默认就是TCP
    # 避免mac本重启服务经常报地址被占用的错误
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', 8080))
    sock.listen(5)
    
    
    conn, address = sock.accept()
    data = conn.recv(1024)  # 获取客户端发送的消息
    # print(data.decode('utf-8'))
    
    def get_headers(data):
        """
        将请求头格式化成字典
        :param data:
        :return:
        """
        header_dict = {}
        data = str(data, encoding='utf-8')
    
        header, body = data.split('
    
    ', 1)
        header_list = header.split('
    ')
        for i in range(0, len(header_list)):
            if i == 0:
                if len(header_list[i].split(' ')) == 3:
                    header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
            else:
                k, v = header_list[i].split(':', 1)
                header_dict[k] = v.strip()
        return header_dict
    
    def get_data(info):
        """
        按照websocket解密规则针对不同的数字进行不同的解密处理
        :param info:
        :return:
        """
        payload_len = info[1] & 127
        if payload_len == 126:
            extend_payload_len = info[2:4]
            mask = info[4:8]
            decoded = info[8:]
        elif payload_len == 127:
            extend_payload_len = info[2:10]
            mask = info[10:14]
            decoded = info[14:]
        else:
            extend_payload_len = None
            mask = info[2:6]
            decoded = info[6:]
    
        bytes_list = bytearray()
        for i in range(len(decoded)):
            chunk = decoded[i] ^ mask[i % 4]
            bytes_list.append(chunk)
        body = str(bytes_list, encoding='utf-8')
    
        return body
    
    
    header_dict = get_headers(data)  # 将一大堆请求头转换成字典数据  类似于wsgiref模块
    client_random_string = header_dict['Sec-WebSocket-Key']  # 获取浏览器发送过来的随机字符串
    magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'  # 全球共用的随机字符串 一个都不能写错
    value = client_random_string + magic_string  # 拼接
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())  # 加密处理
    
    
    tpl = "HTTP/1.1 101 Switching Protocols
    " 
          "Upgrade:websocket
    " 
          "Connection: Upgrade
    " 
          "Sec-WebSocket-Accept: %s
    " 
          "WebSocket-Location: ws://127.0.0.1:8080
    
    "
    response_str = tpl %ac.decode('utf-8')  # 处理到响应头中
    
    
    # 基于websocket收发消息
    conn.send(bytes(response_str,encoding='utf-8'))
    
    while True:
        data = conn.recv(1024)
        # print(data)  # 加密数据 b'x81x89
    x94xac#xee)x0cxc6xaf)Ixb6x80'
        value = get_data(data)
        print(value)
    
    <script>
        var ws = new WebSocket('ws://127.0.0.1:8080/')
        // 这一句话帮你完成了握手环节所有的操作
        // 1 生成随机字符串
        // 2 对字符串做拼接和加密操作
        // 3 接受服务端返回的字符串做比对
    </script>
    

    总结:上述代码知识为了诠释 websocket 内部本质,实际应用直接使用别人封装好的模块即可

    实际应用中,并不是所有的后端框架默认都支持 websocket 协议,如果你想使用的话,可能需要借助于不同的第三方模块

    后端框架
    django
    默认不支持 websocket
    第三方模块: channels

    flask
    默认不支持 websocket
    第三方模块: geventwebsocket

    tornado
    默认支持 websocket

  • 相关阅读:
    1
    vim配置
    pyspark
    添加底部小火箭+目录
    00
    博客园代码高亮设置
    01. 枚举类型
    01. 授权问题
    Android Studio打包签名全过程
    linux 阿里云源地址
  • 原文地址:https://www.cnblogs.com/kai-/p/12692187.html
Copyright © 2011-2022 走看看