轮询、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),
]))
})