Websocket 一、基础知识 1 通讯方式
轮询:让客户端每隔一定时间向服务端发送一次请求。缺点:延迟、请求太多服务端压力大。
长轮询:客户端向服务端发送请求,服务器保持这个请求一定时间,一旦有数据到来就立即返回数据。否则保持一段间后返回没有新数据,此时客户端重新发送请求,保持循环。优点:数据的响应没有延迟。例如:WebQQ,Web微信等。
websocket:客户端和服务端创建连接以后,这个连接不会断开。那么就可以实现双向通信。
二、 轮询方式 1 简单案例
前端定时向后端发送请求,后端发现数据更新后将新数据发送回去。
前端设定了一个index,来确保每次发送的数据都是新数据,不用重复发送数据。
<!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <style > .message { height : 500px ; border : 1px solid #dddddd ; width : 100% ; } </style > </head > <body > <script src ="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js" > </script > <script > function sendMessage ( ){ var text = $("#txt" ).val (); $.ajax ({ url :'/send/msg/' , type : 'GET' , data : { text : text }, success : function (res ){ console .log ("请求发送成功" , res) } }) } max_index = 0 ; setInterval (function ( ){ $.ajax ({ url :'/get/msg/' , type : 'GET' , data : { index : max_index }, dataType : "JSON" , success : function (dataDict ){ max_index = dataDict.max_index ; var dataArray = dataDict.data ; $.each (dataArray, function (index, item ) { console .log (index, item); var tag = $("<div>" ); tag.text (item); $("#message" ).append (tag); }) } }) }, 2000 ) </script > <div class ="message" id ="message" > </div > <div > <input type ="text" id ="txt" placeholder ="请输入" > <input type ="button" value ="发送" onclick ="sendMessage();" > </div > </body > </html >
from django.http import HttpResponse, JsonResponsefrom django.shortcuts import renderDB = [] def home (request ): return render(request, 'home.html' ) def send_msg (request ): text = request.GET.get('text' ) DB.append(text) return HttpResponse("OK" ) def get_msg (request ): try : index = int (request.GET.get("index" )) except TypeError: index = 0 context = { "data" : DB[index:], "max_index" : len (DB), } return JsonResponse(context)
三、 长轮询方式 1 简单案例
访问/home/
显示的聊天室界面。同时每个用户创建一个队列。
点击发送内容,数据也可以发送到后台。同时扔到每个人的队列中。
递归获取消息,去自己的队列中获取数据,然后再界面上展示。
<!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <style > .message { height : 500px ; border : 1px solid #dddddd ; width : 100% ; } </style > </head > <body > <script src ="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js" > </script > <script > USER_UID = "{{ uid }}" ; function sendMessage ( ){ var text = $("#txt" ).val (); $.ajax ({ url :'/send/msg/' , type : 'GET' , data : { text : text }, success : function (res ){ console .log ("请求发送成功" , res) } }) } function getMessage ( ){ $.ajax ({ url :'/get/msg/' , type : 'GET' , data : { uid : USER_UID }, dataType : "JSON" , success : function (res ){ console .log (res); if (res.status ){ var tag = $("<div>" ); tag.text (res.data ); $("#message" ).append (tag); } getMessage (); } }) } $(function ( ){ getMessage (); }) </script > <div class ="message" id ="message" > </div > <div > <input type ="text" id ="txt" placeholder ="请输入" > <input type ="button" value ="发送" onclick ="sendMessage();" > </div > </body > </html >
import queuefrom django.http import HttpResponse, JsonResponsefrom django.shortcuts import renderUSER_QUEUE = {} 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) return HttpResponse("OK" ) def get_msg (request ): uid = request.GET.get('uid' ) q = USER_QUEUE[uid] res = {'status' : True , 'data' : None } try : data = q.get(timeout=10 ) res['data' ] = data except queue.Empty as e: res['status' ] = False return JsonResponse(res)
2 优化方案
Q:服务端持有这个连接,压力是否会很大?
A:使用IO多复用+异步可以解决。
四、websocket方式 1 定义
简单理解:web版的socket。
使用场景:想要服务端向客户端主动推送消息:
2 原理
author::你真的了解WebSocket吗? - 武沛齐
http协议:
websocket协议,是建立在http协议之上的。
连接,客户端发起
握手(验证),客户端发送一个消息,后端接收到消息再做一些特殊处理并返回
收发数据(加密)
断开连接
请求和响应的【握手】信息需要遵循规则:
从请求【握手】信息中提取 Sec-WebSocket-Key
利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
将加密结果响应给客户端
验证时服务器的具体操作:
获得Sec-WebSocket-Key = mnwFxiOlctXFN/DeMt1Amg==
v1 = "mnwFxiOlctXFN/DeMt1Amg==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
res = base64(hmac1(v1))
返回res
解密时的数据包:
取第2个字节的后7位(payload length):
值=127时,数据头2字节、8字节,后面字节(4字节 masking key + 数据)。
值=128,数据头2字节、2字节,后面字节(4字节 masking key + 数据)。
值<=125,数据头2字节,后面字节(4字节 masking key + 数据)。
获得masking key,然后对数据解密
var DECODED = "" ;for (var i = 0 ;i < ENCODE .length ;i++){ DECODED [i] = ENCODED [i] ^ MASK [i % 4 ]; }
验证时客服端发送的数据
GET /chatsocket HTTP/1.1 Host : 127.0.0.1:8002Connection : UpgradePragma : no-cacheCache-Control : no-cacheUpgrade : websocketOrigin : http://localhost:63342Sec-WebSocket-Version : 13Sec-WebSocket-Key : mnwFxiOlctXFN/DeMt1Amg==Sec-WebSocket-Extensions : permessage-deflate; client_max_window_bits... ...
验证时服务端返回的数据
HTTP/1.1 101 Switching ProtocolsUpgrade:websocket Connection:Upgrade Sec-WebSocket-Accept : res
五、websocket使用 1 Django中配置
安装:pip install channels
。注意版本问题,使用不了就降低版本(实测Django=4.2.6
可以用Channels=3.0.5
)。
案例:新建一个项目是ws_demo
,新建一个app
是app001
。
需要新增两个文件:一个routing.py
(相当于websocket
的urls.py
),一个consumers.py
(相当于websocket
的views.py
)
在django中需要了解:
wsgi:是一套Python Web的接口标准协议/规范。django一般情况下都是wsgi。启动时是Starting development server at http://127.0.0.1:8080/
asgi:wsgi + 异步 + websocket。启动时是Starting ASGI/Channels version 3.0.3 development server at http://127.0.0.1:8080/
制作流程:
访问地址看到聊天室的页面,服务端发送http请求。
让客户端主动向服务端发起websocket连接,服务端接收到连接后通过(握手)。
注意:
无论是客户端还是服务端主动断开连接,还是用户关闭了页面,都会触发websocket_disconnect()
函数。
客户端代码是返回的首页模板,服务端代码是修改customers.py
。
INSTALLED_APPS = [ 'channels' ] ASGI_APPLICATION = "<project_name>.asgi.application"
import osfrom django.core.asgi import get_asgi_applicationfrom channels.routing import ProtocolTypeRouter, URLRouterfrom ws_demo import routingos.environ.setdefault('DJANGO_SETTINGS_MODULE' , 'ws_demo.settings' ) application = ProtocolTypeRouter({ "http" : get_asgi_application(), "websocket" : URLRouter(routing.websocket_urlpatterns), })
from django.urls import re_pathfrom app001 import consumerswebsocket_urlpatterns = [ re_path(r'room/(?P<group>\w+)/$' , consumers.ChatConsumer.as_asgi()), ]
from channels.generic.websocket import WebsocketConsumerfrom channels.exceptions import StopConsumerclass ChatConsumer (WebsocketConsumer ): def websocket_connect (self, message ): """ 有客户端来向后端发送websocket连接请求时,自动触发 """ print ("哼哼啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊" ) self.accept() def websocket_receive (self, message ): """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """ print (message) self.send("不要回复不要回复" ) self.close() def websocket_disconnect (self, message ): """ 客户端与服务端断开连接时,自动触发 """ raise StopConsumer()
2 客户端发消息 <div class ="message" id ="message" > </div > <div > <input type ="text" id ="txt" placeholder ="请输入" > <input type ="button" value ="发送" onclick ="sendMessage();" > </div > <script > socket = new WebSocket ("ws://127.0.0.1:8000/room/123/" ); function sendMessage ( ){ let message = document .getElementById ("txt" ).value ; socket.send (message); } </script >
from channels.generic.websocket import WebsocketConsumerfrom channels.exceptions import StopConsumerclass ChatConsumer (WebsocketConsumer ): def websocket_connect (self, message ): self.accept() def websocket_receive (self, message ): text = message.get("text" , " " ) print ("get: " + text) def websocket_disconnect (self, message ): raise StopConsumer()
3 服务端发消息 <div class ="message" id ="message" > </div > <div > <input type ="text" id ="txt" placeholder ="请输入" > <input type ="button" value ="发送" onclick ="sendMessage();" > </div > <script > socket = new WebSocket ("ws://127.0.0.1:8000/room/123/" ); socket.onopen = function (event ) { let tag = document .createElement ("div" ); tag.innerHTML = "[i]连接成功" ; document .getElementById ("message" ).appendChild (tag); }; socket.onmessage = function (event ) { console .log (event.data ); }; </script >
class ChatConsumer (WebsocketConsumer ): def websocket_connect (self, message ): """ 有客户端来向后端发送websocket连接请求时,自动触发 """ self.accept() self.send("[!]World Hello!" ) def websocket_receive (self, message ): """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """ text = message.get("text" , " " ) print ("get: " + text) def websocket_disconnect (self, message ): """ 客户端与服务端断开连接时,自动触发 """ raise StopConsumer()
4 综合案例
注:该案例只是客户端和服务端一对一通信,还不能群聊。
<div class ="message" id ="message" > </div > <div > <input type ="text" id ="txt" placeholder ="请输入" > <input type ="button" value ="发送" onclick ="sendMessage();" > <input type ="button" value ="关闭" onclick ="closeConn();" > </div > <script > socket = new WebSocket ("ws://127.0.0.1:8000/room/123/" ); socket.onopen = function (event ) { let tag = document .createElement ("div" ); tag.innerHTML = "[i]连接成功" ; document .getElementById ("message" ).appendChild (tag); }; socket.onmessage = function (event ) { let tag = document .createElement ("div" ); tag.innerHTML = event.data ; document .getElementById ("message" ).appendChild (tag); }; socket.onclose = function (event ){ let tag = document .createElement ("div" ); tag.innerHTML = "[x]连接关闭" ; document .getElementById ("message" ).appendChild (tag); } function sendMessage ( ){ let message = document .getElementById ("txt" ).value ; socket.send (message); } function closeConn ( ){ socket.close (); } </script >
from channels.generic.websocket import WebsocketConsumerfrom channels.exceptions import StopConsumerclass ChatConsumer (WebsocketConsumer ): def websocket_connect (self, message ): """ 有客户端来向后端发送websocket连接请求时,自动触发 """ self.accept() self.send("[!]World Hello!" ) def websocket_receive (self, message ): """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """ text = message.get("text" , " " ) print ("get: " + text) if text == "关闭" : self.close() return self.send("[!]收到了" + text + "喵" ) def websocket_disconnect (self, message ): """ 客户端与服务端断开连接时,自动触发 """ print ("客户端主动断开了喵" ) raise StopConsumer()
5 连接列表实现群聊
在后端维护有个连接列表,有连接来就加入列表中,然后发送消息时遍历用户发送消息,用户离开时就移除列表。
优点:实现快。
缺点:效率低,功能不够强大。
<div class ="message" id ="message" > </div > <div > <input type ="text" id ="txt" placeholder ="请输入" > <input type ="button" value ="发送" onclick ="sendMessage();" > <input type ="button" value ="关闭" onclick ="closeConn();" > </div > <script > socket = new WebSocket ("ws://127.0.0.1:8000/room/123/" ); socket.onopen = function (event ) { let tag = document .createElement ("div" ); tag.innerHTML = "[i]连接成功" ; document .getElementById ("message" ).appendChild (tag); }; socket.onmessage = function (event ) { let tag = document .createElement ("div" ); tag.innerHTML = event.data ; document .getElementById ("message" ).appendChild (tag); }; socket.onclose = function (event ){ let tag = document .createElement ("div" ); tag.innerHTML = "[x]连接关闭" ; document .getElementById ("message" ).appendChild (tag); } function sendMessage ( ){ let message = document .getElementById ("txt" ).value ; socket.send (message); } function closeConn ( ){ socket.close (); } </script >
from channels.generic.websocket import WebsocketConsumerfrom channels.exceptions import StopConsumerCONN_LIST = [] class ChatConsumer (WebsocketConsumer ): def websocket_connect (self, message ): """ 有客户端来向后端发送websocket连接请求时,自动触发 """ self.accept() CONN_LIST.append(self) self.send("[!]World Hello!" ) def websocket_receive (self, message ): """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """ text = message.get("text" , " " ) print ("get: " + text) for conn in CONN_LIST: conn.send("[!]收到了" + text + "喵" ) def websocket_disconnect (self, message ): """ 客户端与服务端断开连接时,自动触发 """ print ("客户端主动断开了喵" ) CONN_LIST.remove(self) raise StopConsumer()
6 Channel layers实现 6.1 配置
author::django channels - 武沛齐 - 博客园 (cnblogs.com)
如果使用redis作为内存需要安装:pip3 install channels-redis
CHANNEL_LAYERS = { "default" : { "BACKEND" : "channels.layer.InMemeoryCahnnelLayer" , } }
CHANNEL_LAYERS = { 'default' : { 'BACKEND' : 'channels_redis.core.RedisChannelLayer' , 'CONFIG' : {"hosts" : ["redis://10.211.55.25:6379/1" ],}, }, }
6.2使用
客户端发送时是发送http://127.0.0.1:8000/index/?qq=123
,来进入不同的群。
服务器依照群号来发送消息。
<div class ="message" id ="message" > </div > <div > <input type ="text" id ="txt" placeholder ="请输入" > <input type ="button" value ="发送" onclick ="sendMessage();" > <input type ="button" value ="关闭" onclick ="closeConn();" > </div > <script > socket = new WebSocket ("ws://127.0.0.1:8000/room/{{ qq_group_num }}/" ); socket.onopen = function (event ) { let tag = document .createElement ("div" ); tag.innerHTML = "[i]连接成功" ; document .getElementById ("message" ).appendChild (tag); }; socket.onmessage = function (event ) { let tag = document .createElement ("div" ); tag.innerHTML = event.data ; document .getElementById ("message" ).appendChild (tag); }; socket.onclose = function (event ) { let tag = document .createElement ("div" ); tag.innerHTML = "[x]连接关闭" ; document .getElementById ("message" ).appendChild (tag); } function sendMessage ( ) { let message = document .getElementById ("txt" ).value ; socket.send (message); } function closeConn ( ){ socket.close (); } </script >
from django.urls import pathfrom app001 import viewsurlpatterns = [ path('index/' , views.index) ]
from django.shortcuts import renderdef index (request ): qq_group_num = request.GET.get("qq" , "" ) return render(request, 'index.html' ,{"qq_group_num" : qq_group_num})
from channels.generic.websocket import WebsocketConsumerfrom channels.exceptions import StopConsumerfrom asgiref.sync import async_to_syncclass ChatConsumer (WebsocketConsumer ): def websocket_connect (self, message ): """ 有客户端来向后端发送websocket连接请求时,自动触发 """ self.accept() group = self.scope['url_route' ]['kwargs' ].get("group" ) async_to_sync(self.channel_layer.group_add)(group, self.channel_name) def websocket_receive (self, message ): """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """ group = self.scope['url_route' ]['kwargs' ].get("group" ) dic = {"type" : "xx.oo" , "message" : message} async_to_sync(self.channel_layer.group_send)(group, dic) 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()