轮询、websocket(重点)

长轮询

轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。 缺点:有延迟,浪费服务器资源。

长轮询:客户端向服务器发送Ajax请求,服务器接到请求后夯住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。

首先需要为每个用户维护一个队列,用户浏览器会通过js递归向后端自己的队列获取数据,自己队列没有数据,会将请求夯住(去队列中获取数据),夯一段时间之后再返回。
注意:一旦有数据立即获取,获取到数据之后会再发送请求。

优点: 在无消息的情况下不会频繁的请求,耗费资源小。 WebQQ

缺点:服务器夯住连接会消耗资源,返回数据顺序无保证,难于管理维护。

基于长轮询简单实现聊天:

views.py

from django.shortcuts import render,HttpResponse
from django.http import JsonResponse
import queue

QUEUE_DICT = {}

def index(request):
    username = request.GET.get('username')
    if not username:
        return HttpResponse('请输入名字')
    QUEUE_DICT[username] = queue.Queue()	# 为每个请求用户开一个队列
    return render(request,'index.html',{'username':username})

def send_msg(request):
    """
    接受用户发来的消息
    :param request:
    :return:
    """
    text = request.POST.get('text')
    for k,v in QUEUE_DICT.items():
        v.put(text)
    return HttpResponse('ok')

def get_msg(request):
    """
    想要来获取消息
    :param request:
    :return:
    """
    ret = {'status':True,'data':None}

    username = request.GET.get('user')
    user_queue = QUEUE_DICT.get(username)

    try:
        message = user_queue.get(timeout=10)
        ret['data'] = message
    except queue.Empty:
        ret['status'] = False
    return JsonResponse(ret)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>聊天室({{ username }})</h1>
    <div class="form">
        <input id="txt" type="text" placeholder="请输入文字">
        <input id="btn" type="button" value="发送">
    </div>
    <div id="content">

    </div>

    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <script>

        $(function () {
            $('#btn').click(function () {
                var text = $("#txt").val();
                $.ajax({
                    url:'/send/msg/',
                    type:'POST',
                    data: {text:text},
                    success:function (arg) {
                        console.log(arg);
                    }
                })
             });

            getMessage();
        });

        function getMessage() {
            $.ajax({
                url:'/get/msg/',
                type:'GET',
                data:{user:"{{ username }}" },
                dataType:"JSON",
                success:function (info) {
                    console.log(info);
                    if(info.status){
                        var tag = document.createElement('div');
                        tag.innerHTML = info.data;
                        $('#content').append(tag);
                    }
                    getMessage();
                }
            })
        }
    </script>
</body>
</html>

websocket

基于http的一个协议。是用http协议规定传递。协议规定了浏览器和服务端创建连接之后,不断开,保持连接。相互之间可以基于连接进行主动的收发消息。

原理:

​ 关键字:协议,ws,魔法字符串magic_string,payload, mask

​ magic_string = '258EAFAA5-E914-47DA-95CA-C5AB0DC85B11’ 全球唯一的魔法字符串。

  1. websocket握手环节:

    - 客户端向服务端发送随机字符串,在http的请求头 Sec-WebSocket-Key 中;
    - 服务端接受到到随机字符串,将这个字符串与魔法字符串拼接,然后进行sha1、base64加密;放在响应头Sec-WebSocket-Accept中,返回给浏览器;
    - 浏览器进行校验,校验不通过,说明服务端不支持websocket协议;
    - 校验成功,会建立连接,服务端与浏览器能够进行收发消息,传输的数据都是加密的。
    
  2. 数据解密:

    - 获取第二个字节的后7位,称为payload_len
    - 判断payload_len的值:
    	=127 : 2字节 + 8字节 + 4字节masking key + 数据
    	=126 : 2字节 + 2字节 + 4字节masking key + 数据
    	<=125: 2字节 + 4字节masking key +数据
    描述:	
    	127:在8个字节后时数据部分
    	126:在2个字节后时数据部分
    	<=125:后面就是数据部分
    	数据部分的前4个字节是 masking key 掩码,后面的数据会与其进行按位与运算进行数据的解密。
    

手动创建支持websocket的服务端

  • 服务端

    import socket
    import hashlib
    import base64
    
    
    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):
        """
        进行数据的解密
        :param data:
        :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
    
    
    # 创建socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', 8002))
    sock.listen(5)
    
    
    # 等待用户连接
    conn, address = sock.accept()
    # 握手环节
    header_dict = get_headers(conn.recv(1024))
    magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'	# 魔法字符串
    random_string = header_dict['Sec-WebSocket-Key']	# 获取随机字符串
    value = random_string + magic_string
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())	# bytes类型
    
    response = "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:8002\r\n\r\n"		# ws开头
    
    response = response %ac.decode('utf-8')
    # print(response)
    conn.send(response.encode('utf-8'))
    
    # 接受数据
    while True:
        data = conn.recv(1024)
        msg = get_data(data)	# 进行数据解密
        print(msg)
    
    
  • html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <input type="button" value="开始" onclick="startConnect();">
    
    <script>
        var ws = null;
        function startConnect() {
            // 1. 内部会先发送随机字符串
            // 2. 内部会校验加密字符串
            ws = new WebSocket('ws://127.0.0.1:8002')
        }
    </script>
    </body>
    </html>
    

Django实现websocket

django和flask框架,内部基于wsgi做的socket,默认都不支持websocket协议,只支持http协议。

  • flask中应用:

    pip3 install gevent-websocket 
    
  • django中应用:

    pip3 install channels
    

    在django中使用,是将 wsgi(wsgiref) 替换成 asgi(daphne) ,asgi支持 http和 websocket 协议。channel layer 可以实现多个人发送消息。

单对单实现通信

setting.py配置

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
]

ASGI_APPLICATION = "django_channels_demo.routing.application"	# 添加ASGI_APPLICATION支持websocket

urls.py路由:

from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.index),
]

routing.py路由:

from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from app01 import consumers

application = ProtocolTypeRouter({
    'websocket': URLRouter([
        url(r'^chat/$', consumers.ChatConsumer),
    ])
})

consumers.py 应用

from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        """ websocket连接到来时,自动执行 """
        print('有人来了')	# 可以在连接之前,做一些操作
        self.accept()	# 连接成功

    def websocket_receive(self, message):
        """ websocket浏览器给发消息时,自动触发此方法 """
        print('接收到消息', message)

        self.send(text_data='收到了')	# 发送数据。内部会进行数据的加密

        # self.close()	# 可自动关闭

    def websocket_disconnect(self, message):
        print('客户端主动断开连接了')
        raise StopConsumer()

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Web聊天室:<span id="tips"></span></h1>
    <div class="form">
        <input id="txt" type="text" placeholder="请输入文字">
        <input id="btn" type="button" value="发送" onclick="sendMessage();">
    </div>
    <div id="content">

    </div>

    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <script>
        var ws;

        $(function () {
            initWebSocket();
        });

        function initWebSocket() {
            ws = new WebSocket("ws://127.0.0.1:8000/chat/");
            ws.onopen = function(){
                $('#tips').text('连接成功');
            };
            ws.onmessage = function (arg) {
                var tag = document.createElement('div');
                tag.innerHTML = arg.data; //接收返回的数据
                $('#content').append(tag);
            };
            ws.onclose = function () {
                ws.close();
            }
        }
		// 发送数据
        function sendMessage() {
            ws.send($('#txt').val());
        }
    </script>
</body>
</html>

多人实现通信 -- channel_layer

基于内存的channel_layer。

配置channel_layer

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer",
    }
}

consumers.py 逻辑

方式一:

from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        """ websocket连接到来时,自动执行 """
        print('有人来了')
        
        # 将过来连接的self.channel_layer加入group_add到'222'的组中,channel_name是一个随机字符串,相当于用户
        async_to_sync(self.channel_layer.group_add)('222', self.channel_name)

        self.accept()

    def websocket_receive(self, message):
        """ websocket浏览器给发消息时,自动触发此方法 """
        print('接收到消息', message)

        async_to_sync(self.channel_layer.group_send)('222', {
            'type': 'xxx.ooo',
            'message': message['text']
        })
		# type定义回调函数,发送消息
        
    def xxx_ooo(self, event):
        message = event['message']
        self.send(message)	

    def websocket_disconnect(self, message):
        """ 断开连接 """
        print('客户端主动断开连接了')
        async_to_sync(self.channel_layer.group_discard)('222', self.channel_name)
        raise StopConsumer()

方式二:

from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        print('有人来了')
        async_to_sync(self.channel_layer.group_add)('22922192', self.channel_name)
        self.accept()

    def receive(self, text_data=None, bytes_data=None):
        print('接收到消息', text_data)

        async_to_sync(self.channel_layer.group_send)('22922192', {
            'type': 'xxx.ooo',
            'message': text_data
        })
        # type定义回调函数,发送消息
    def xxx_ooo(self, event):
        # 发消息
        message = event['message']
        self.send(message)

    def disconnect(self, code):
        print('客户端主动断开连接了')
        async_to_sync(self.channel_layer.group_discard)('22922192', self.channel_name)

上面两个方式本质上是一样的,第二种较简单。

基于redis的 channel layer

# 下载组件
pip3 install channels-redis

配置:

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [('10.211.55.25', 6379)]
        },
    },
}

consumers.py 逻辑

from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):

    def connect(self):
        async_to_sync(self.channel_layer.group_add)('x1', self.channel_name)
        self.accept()

    def receive(self, text_data=None, bytes_data=None):
        async_to_sync(self.channel_layer.group_send)('x1', {
            'type': 'xxx.ooo',
            'message': text_data
        })

    def xxx_ooo(self, event):
        # 回调函数,真正的发送
        message = event['message']
        self.send(message)

    def disconnect(self, code):
        async_to_sync(self.channel_layer.group_discard)('x1', self.channel_name)

session

websocket 后端可以通过 self.scope获取请求的数据,全都放在scope中。如果需要保存session,必须save()

self.scope['session']['键']		# 获取
self.scope['session']['user'] = 'xxx'		# 设置session,默认不保存
self.scope['session'].save()	# 保存

routing.py路由:

from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from channels.sessions import CookieMiddleware,SessionMiddlewareStack
from apps.web import consumers


application = ProtocolTypeRouter({
    'websocket': SessionMiddlewareStack(URLRouter([
        # 支持session
        url(r'^deploy/(?P<task_id>\d+)/$', consumers.DeployConsumer),
    ]))
})
posted @ 2020-08-27 13:08  SensorError  阅读(499)  评论(0编辑  收藏  举报