Websocket、 channles

一、代码发布项目(全栈项目)

一般互联网公司都会有一套自己的 代码发布系统

并且大部分的代码发布都是用运维工具jenkins(shell脚本)

其实也有公司自己定制自己的代码发布系统(saltstack、Java开发脚本、PHP脚本)

web项目基本都是基于HTTP协议的

 

 

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

 

方法一:伪实现

 

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

 

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

 

  • 轮询          缺点:    消息延迟严重,请求次数多 消耗资源过大

  • 长轮询      优点:消息基本没有延迟,请求次数降低 消耗资源减少

长轮询:服务端给每个浏览器创建一个队列,让浏览器通过ajax向后端偷偷的发送请求,去各自对应的队列中获取数据,

如果没有数据则会有阻塞,但是不会一直阻塞,比如最多阻塞30秒(pending)后给一个响应,无论响应是否是真正的数据,都会再次通过回调函数调用请求数据的代码

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

分析:

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

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

 

方法二:真实现

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

 

  • Websocket

 

 

注意:为什么要做到服务端主动给客户端推送消息,该技术点有哪些应用场景?

  • 大屏幕投票实时展示

  • 任务的执行流程

  • 群聊功能

 

三、以前知识点回顾

1、ajax操作:异步提交,局部刷新。用它就可以偷偷的朝服务端发送请求

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

方法一:标签查找绑定
$('p').click()
方法二:直接写函数 ,注意括号不能少
<p onclick="sendMsg()"></p>

前端

$.ajax({
  url:'',           # 控制后端提交路径
  type:'',          # 控制请求方式 
  data:{},          # 控制提交的数据
  dataType:"JSON",  # django后端用HttpResponse返回json格式字符串,args不会自动反序列化,拿到的还是json格式字符串string字符类型
# 而如果是用JsonResponse返回的那么args会自动返序列化成前端js的对象类型 success:
function(args){ # 异步回调机制 })

后端

urls中

url(r'^index/',views.index)

views中

import json
import django.http import JsonResponse
import django.shortcuts import render,HttpResponse
def
index(request): if request.method == 'POST': back_dic = {'msg':'hahaha'} return HttpResponse(json.dumps(back_dic)) # 需要 ajax中的dataType:“JSON” return JsonResponse(back_dic) # 不需要 ajax中的dataType:“JSON”
 return render(request,'index.html')                # 后续在写ajax请求的时候建议你加上dataType参数

注意:需要先注释掉csrf中间件

2、队列

队列:先进先出

堆栈:先进后出

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=10)  # 没有数据等待10s再没有就报错 queue.Empty
except queue.Empty as e:
    pass
print(v1,v2)

# 实际生产中不会使用上述的消息队列 会使用功能更加的强大的消息队列 如下:
    redis
    kafka
    rebittMQ

 

3、递归: python中有最大递归限制 997或 998 官网给出的是1000

注意:在python中是没有尾递归优化的。例子如下

def func():
  func()
func()  # 不行

 

注意: 在js中 是没有递归的概念的 函数可以自己调用自己 属于正常的事件机制

function func1(){
  $.ajax({
    url:'',
    type:'',
    data:'',
    dataType:'JSON',
    success:function({
        func1()  # 可以
    })
  })
}
func1()

 

4、forms组件(校验性组件

  注意:modelform组件:是forms组件的加强版本,功能和代码差不多,但是更加的方便

 

 

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

前端

<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>

 

后端

import queue


q_dict = {}  # {唯一标示:对应的队列,唯一标示:对应的队列}


def home(request):
    # 获取客户端浏览器的唯一标识
    name = request.GET.get('name')
    # 生成一一对应关系
    q_dict[name] = queue.Queue()
    return render(request,'home.html',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)

 

 

 

五、websocket(主流浏览器都支持)实现服务器主动推送信息

1、网络协议
HTTP 不加密传输
HTTPS 加密传输
这两个都是短链接/无链接
2、WebSocket 加密传输,是基于HTTP协议上的协议。

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

3、websocket实现原理:可以分为两部分

 

 

 

第一部分.握手环节(并不是所有的服务端都支持websocket 所以用握手环节来验证服务端是否支持websocket)
浏览器访问服务端之后浏览器会立刻生成一个随机字符串
浏览器会将生成好的随机字符串发送给服务端(基于HTTP协议 放在请求头中),并且自己也保留一份

服务端和客户端都会对该随机字符串做以下处理
1.1 先拿随机字符串跟magic string(固定的字符串)做字符串的拼接
1.2 将拼接之后的结果做加密处理(sha1+base64)

服务端将生成好的处理结果发送给浏览器(基于HTTP协议 放在响应头中)
浏览器接受服务端发送过来的随机字符串跟本地处理好的随机字符串做比对,如果一致说明服务端支持websocket,如果不一致说明不支持

第二部分:收发数据环节
分析:
1.基于网络传输数据都是二进制格式 在python中可以用bytes类型对应
2.进制换算

先读取第二个字节的后七位数据(payload) 根据payload做不同的处理
=127:继续往后读取8个字节数据(数据报10个字节)
=126:继续往后读取2个字节数据(数据报4个字节)
<=125:不再往后读取(数据2个字节)

上述操作完成后,会继续往后读取固定长度4个字节的数据(masking-key)
依据masking-key解析出真实数据
后端:server.py文件

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('\r\n\r\n', 1)
    header_list = header.split('\r\n')
    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\r\n" \
      "Upgrade:websocket\r\n" \
      "Connection: Upgrade\r\n" \
      "Sec-WebSocket-Accept: %s\r\n" \
      "WebSocket-Location: ws://127.0.0.1:8080\r\n\r\n"
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'\x81\x89\n\x94\xac#\xee)\x0c\xc6\xaf)I\xb6\x80'
    value = get_data(data)
    print(value)

 

前端:client.html文件

 

 注意:

<script>
var ws = new WebSocket('ws://127.0.0.1:8080/')
</script>

// 这一句话帮你完成了握手环节所有的操作
// 1 生成随机字符串
// 2 对字符串做拼接和加码操作
// 3 接受服务端返回的字符串做比对

结果

 

 

ws.send('你好啊')

六、支持websocket协议的 第三方模块(geventwebsocket、channels

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

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

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

tornado
默认支持websocket

七、django如何支持websocket

1、安装:命令

pip3 install channles==2.3

channels模块内部帮你封装了握手/加密/解密等所有操作

2、使用

settings文件中 注册  channles 

注册
INSTALLED_APPS = [ 'channels' ]

配置变量

ASGI_APPLICATION = '项目名同名的文件名.文件夹下py文件名默认就叫routing.该py文件内部的变量名默认就叫application'

例子:

ASGI_APPLICATION = 's13_day01.routing.application'

去项目名同名的文件夹(s13_day01)下面新建一个routing.py文件,定义application变量

from channels.routing import ProtocolTypeRouter,URLRouter


application = ProtocolTypeRouter({
    'websocket':URLRouter([
                             # 书写websocket路由与视图函数对应关系
    ])
})

注意:

1、上述操作配置完成后,启动django会由原来的wsgiref启动变成asgi启动 (内部:达芙妮)

2、启动之后django即支持websocket也支持http协议

原因:看源码

class ProtocolTypeRouter:
    """
    Takes a mapping of protocol type names to other Application instances,
    and dispatches to the right one based on protocol name (or raises an error)
    """
    def __init__(self, application_mapping):
        self.application_mapping = application_mapping
        if "http" not in self.application_mapping:
            self.application_mapping["http"] = AsgiHandler

 

3、基于http的操作还是在urls.py和views.py中完成

4、基于websocket的操作则在routing.py和consumer.py (对应的应用中创建) 中完成

 

 

 

posted @ 2020-04-17 20:42  薛定谔的猫66  阅读(424)  评论(0)    收藏  举报