1. 程式人生 > 實用技巧 >輪詢與websocket(重點)對比

輪詢與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),
    ]))
})