輪詢與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’ 全球唯一的魔法字串。
-
websocket握手環節:
- 客戶端向服務端傳送隨機字串,在http的請求頭 Sec-WebSocket-Key 中; - 服務端接受到到隨機字串,將這個字串與魔法字串拼接,然後進行sha1、base64加密;放在響應頭Sec-WebSocket-Accept中,返回給瀏覽器; - 瀏覽器進行校驗,校驗不通過,說明服務端不支援websocket協議; - 校驗成功,會建立連線,服務端與瀏覽器能夠進行收發訊息,傳輸的資料都是加密的。
-
資料解密:
- 獲取第二個位元組的後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),
]))
})