14-2 聊天室实现思路:轮训、长轮训、websocket_哔哩哔哩_bilibili 参考大佬的B站学习笔记
https://www.cnblogs.com/wupeiqi/p/6558766.html 参考博客
https://www.cnblogs.com/wupeiqi/articles/9593858.html 参考博客
http协议: 是短连接,无状态的,一次性的,无法保证实时信息交互
- 客户端主动连接服务器
- 客户端向服务端发送消息,服务端接收到返回数据
- 客户端接收到数据
- 端口连接
websock协议:创建持久的连接不断开,基于这个连接进行收发数据
- 实时响应:接收发送消息
- 实时图表,柱状图,饼图
websocket 原理:
- 连接,客户端发起
- 握手,客户端发送一个消息,后端接收到消息再做一些特殊处理返回(服务端要支持websocket协议)
- 收发数据(加密)
- 断开连接
握手流程:
1.客户端向服务端发送
GET /chatsocket HTTP/1.1
Host: 127.0.0.1:8002
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
2.服务端接收:
请求和响应的【握手】信息需要遵循规则:
从请求【握手】信息中提取 Sec-WebSocket-Key
利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
将加密结果响应给客户端
注:magic string固定为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
返回数据给客户端浏览器,验证通过则完成握手
HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection: Upgrade
Sec-WebSocket-Accept: 密文
收发数据流程:
数据 b'asdfa;efawe;sdfas;awdfawea;sdfasdfaf;sdfasdfa;'
先获取第二个字节,8位 10001010
再获取第二个字节的后七位 0001010 -> payload len
- =127 2个字节,8个字节 其他字节(4字节 masking key + 数据)
- =126 2个字节,8个字节 其他字节(4字节 masking key + 数据)
- <=125 2个字节 其他字节(4字节 masking key + 数据)
- 获取masking key,然后对数据进行解密
var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}
实时交互的解决方案:
- 轮训,浏览器每隔一段时间向后台发送一次请求。缺点:有延迟、请求太多网站压力大
- 长轮询,客户端向服务端发送请求,保持一定的时间,一旦有数据就立即返回。特点:数据无延迟,常应用于大平台、WebQQ、Web微信
- websocket,客户端和服务端创建连接不断开,可以实现双向通信。特点:旧版浏览器不支持
长轮询实现群聊功能
- 访问url进入聊天室页面,为每个用户创建一个队列
- 点击发送内容,数据发送到后台,给到每个人的队列中
- 递归获取消息,去队列中获取数据,展示在页面
前端
<body>
<div class="message" id="message"></div>
<div>
<input type="text" placeholder="请输入" id="txt">
<input type="button" value="发送" onclick="sendMessage()">
<!-- <input type="button" value="关闭连接" onclick="closeConn()">-->
</div>
<script>
USER_ID = "{{uid}}";
function sendMessage(){
var text=$('#txt').val();
// 基于ajax将用户文本信息发送到后台
$.ajax({
url:'/send/msg/',
data:{text:text},
type: 'GET',
dataType:'JSON',
success: function (res){
console.log('请求发送成功',res)
//超时,没有新数据
// 有数据,立即展示
// if (res.status){
// var tag =$("<div>");
// tag.text(res.data)
// $("#message").appendImage(tag);
// }
}
})
}
function getMessage(){
$.ajax({
url:'/get/msg/',
data:{uid:USER_ID},
type: 'GET',
dataType:'JSON',
success: function (res){
//超时,没有新数据
// 有数据,立即展示
if (res.status){
var tag =$("<div>");
tag.text(res.data)
$("#message").appendImage(tag);
}
getMessage(); // 递归调用该函数
}
})
}
$(function (){
getMessage();
})
</script>
</body>
后端
# view 视图
import queue
from django.shortcuts import render,HttpResponse
from django.http import request,JsonResponse
USER_QUEUE = {}
def index(request):
qq_number = request.GET.get('num')
return render(request,'index.html',{"qq_number":qq_number})
def home(request):
uid = request.GET.get('uid')
USER_QUEUE[uid]=queue.Queue()
return render(request,'home.html',{'uid':uid})
def send_msg(request):
text =request.GET.get('text')
for uid,q in USER_QUEUE.items():
q.put(text)
# print("接收到客户端的请求:"+request.GET)
return HttpResponse("ok")
def get_msg(request):
# 去自己的队列中获取数据
uid = request.GET.get('uid')
q = USER_QUEUE[uid]
result = {'status':True,'data':None}
try:
data = q.get(timeout=10)
result['data']=data
except queue.Empty as e:
result['status']=False
return JsonResponse(result)
# url
path('home/', home),
path('send/msg/', send_msg),
path('get/msg/', get_msg),
django 中配置websocket
pip install channels # 安装组件
# 注册app
'channels'
# 配置asgi.application
ASGI_APPLICATION = 'web.asgi.application'
更新asgi文件(在支持http的基础上支持websocket)
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter,URLRouter
from . import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
application = ProtocolTypeRouter({
'http':get_asgi_application(),
'websocket':URLRouter(routing.websocket_urlpatterns),
})
创建routing文件在setting同级目录
from django.urls import re_path
from app import consumers
websocket_urlpatterns = [
re_path(r'ws/(?P<group>\w+)/$',consumers.ChatConsumer.as_asgi()),
]
创建app目录下consumer文件
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
"""
wsgi: 同步
asgi: 异步+asgi+websocket
"""
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
# 客户端向后端发送websocket连接请求时自动触发
# 容许和客户端创建连接
self.accept()
def websocket_receive(self, message):
# 浏览器基于websocket向后端发送数据,自动触发接收消息
print(message)
self.send('不要回复!!!')
def websocket_disconnect(self, message):
# 客户端与服务端断开时自动触发
raise StopConsumer
运行结果:
注意: 当启动服务器是 Starting development server 而非ASGI服务时要检查channel版本,较高的版本可能不适配django,经调试发现3.0.1版本适配,重新安装channels即可
pip install channels==3.0.1
Pycharm 的html 注释小技巧!
setting --> Template Languages --> None
websocket 收发消息流程
- 访问地址看到聊天室的页面
- 客户端主动向服务端发送websocket连接,服务端接收到通过(完成握手)
- 客户端,执行websocket连接动作
// http://www.baidu.com // ws://www.baidu.com 注释下面 ws://xxx的使用 socket = new WebSocket("ws://127.0.0.1:8000/ws/123/");- 服务端
def websocket_connect(self, message): # 客户端向后端发送websocket连接请求时自动触发 # 容许和客户端创建连接(握手) print("有人来连接了") self.accept()- 收发消息(客户端向服务端发送消息)
- 客户端
<input type="button" value="发送" οnclick="sendMessage()">// 获取输入框的信息进行发送 function sendMessage(){ let tag =document.getElementById('txt'); socket.send(tag.value); }- 服务端
def websocket_receive(self, message): # 浏览器基于websocket向后端发送数据,自动触发接收消息 txt = message['text'] print('收到消息-->', txt) self.send(txt+'哈哈') # 在客户端发送消息的基础上加上字段 "哈哈"- 收发消息(服务端主动发给客户端)
服务端def websocket_connect(self, message): # 客户端向后端发送websocket连接请求时自动触发 # 容许和客户端创建连接(握手) print("有人来连接了") self.accept() # 服务端向客户端发送消息 self.send('来了呀年轻人!') 客户端// 当websocket接收到服务端发来的消息时,自动触发这个函数 socket.onmessage = function (event){ var tag= document.createElement('div'); tag.innerText = event.data; console.log(tag.textContent); }
运行结果:
前端页面的实现及DOM函数的触发
<div class="message" id="message"></div>
<div>
<input type="text" placeholder="请输入" id="txt">
<input type="button" value="发送" onclick="sendMessage()">
<input type="button" value="关闭连接" onclick="closeConn()">
</div>
<script>
// http://www.baidu.com
// ws://www.baidu.com 注释下面 ws://xxx的使用
socket = new WebSocket("ws://127.0.0.1:8000/ws/123/");
// 创建好连接之后自动触发,即当执行self.accept()
socket.onopen = function (event){
let tag= document.createElement('div');
tag.innerText = '[连接成功]';
console.log(tag.textContent)
document.getElementById('message').appendChild(tag);
}
// 当websocket接收到服务端发来的消息时,自动触发这个函数
socket.onmessage = function (event){
let tag= document.createElement('div');
tag.innerText = event.data;
// console.log(tag.textContent);
document.getElementById('message').appendChild(tag);
}
// 断开连接时触发
socket.onclose =function (event){
let tag= document.createElement('div');
tag.innerText = '[断开连接]';
console.log(tag.textContent)
document.getElementById('message').appendChild(tag);
}
// 获取输入框的信息进行发送
function sendMessage(){
let tag =document.getElementById('txt');
socket.send(tag.value);
}
// 关闭连接
function closeConn(){
socket.close(); // 向服务端发送断开连接的请求
}
</script>
群聊功能的实践:
方法1: 方法笨重,可行性差,不便于接口
CONN_LIST = []def websocket_connect(self, message): # 客户端向后端发送websocket连接请求时自动触发 self.accept() CONN_LIST.append(self)def websocket_receive(self, message): txt = message['text'] for conn in CONN_LIST: conn.send(txt)def websocket_disconnect(self, message): CONN_LIST.remove(self) raise StopConsumer # 容许断开连接
Channel Layers实现群聊
方法2:基于channel中提供channel layers来实现-主要流程
配置setting channel layers
CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer", } }- 更新view
qq_number = request.GET.get('num')- 更新收发后端consumer.py
def websocket_connect(self, message): # 客户端向后端发送websocket连接请求时自动触发 self.accept() # 获取群号即路由匹配的数值 group = self.scope['url_route']['kwargs'].get('group') # 固定用法 # 将客户端的连接对象加入到某个地方 redis or 内存 async_to_sync(self.channel_layer.group_add)(group,self.channel_name) # 服务端向客户端发送消息 self.send('来了呀年轻人!')def websocket_receive(self, message): # 获取群号即路由匹配的数值 group = self.scope['url_route']['kwargs'].get('group') # 固定用法 # 通知组内的所有客户端,执行 xx_oo 方法,在此方法中自定义功能 async_to_sync(self.channel_layer.group_send)(group, {'type':'xx.oo','message':message})def xx_oo(self,event): text = event['message']['text'] self.send(text)def websocket_disconnect(self, message): group = self.scope['url_route']['kwargs'].get('group') # 固定用法 async_to_sync(self.channel_layer.group_discard)(group,self.channel_name) # 客户端与服务端断开时自动触发 raise StopConsumer # 容许断开连接
运行结果: