14-2 聊天室实现思路:轮训、长轮训、websocket_哔哩哔哩_bilibili 参考大佬的B站学习笔记
https://www.cnblogs.com/wupeiqi/p/6558766.html 参考博客
https://www.cnblogs.com/wupeiqi/articles/9593858.html 参考博客
http协议: 是短连接,无状态的,一次性的,无法保证实时信息交互
websock协议:创建持久的连接不断开,基于这个连接进行收发数据
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,客户端和服务端创建连接不断开,可以实现双向通信。特点:旧版浏览器不支持
长轮询实现群聊功能
前端
- <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 =$("");
- // 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 =$("");
- 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
\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()
- 收发消息(客户端向服务端发送消息)
- 客户端
// 获取输入框的信息进行发送
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 # 容许断开连接
运行结果:
-
相关阅读:
Linux
火眼金睛破局ES伪慢查询
python:talib.BBANDS 画股价-布林线图
【Verilog 教程】5.2Verilog 模块例化
react知识点
嵌入式基础准备 | Linux命令(包括 文件、目录和压缩、系统操作、vi、vim、ctags、cscope)
红黑树(Red Black Tree)
笔试强训day35(抄送列表,年会抽奖)
精品springboot的二手车管理系统vue
东南亚三国(新/马/菲)2022女装市场趋势解读及爆品指南
-
原文地址:https://blog.csdn.net/qq_44238024/article/details/136287374