相关推荐recommended
Django使用WebSocket
作者:mmseoamin日期:2024-03-20

1、websocket 相关

实现一个系统,20 个用户同时打开网站,呈现出来一个群聊界面

解决方案

  • 轮询:让浏览器每隔2s向后台发送一次请求,缺点:延迟,请求太多网站压力大

  • 长轮询:客户端服务端发送请求,服务器最多宕20s,一旦有数据接入,就立即返回。数据的响应没有延迟时间。

  • websocket:客户端和服务端创建连接后,不断开,实现双向通信

    Django使用WebSocket,在这里插入图片描述,第1张

    轮询

    • 访问 /home/ 显示的聊天室界面

    • 点击发送内容,数据可以发送到后台

    • 定时获取消息,发送到前端

      长轮询

      Django使用WebSocket,在这里插入图片描述,第2张

      • 访问/home/ 显示聊天界面, → 每个用户创建一个队列

      • 点击发送内容,数据也可以发送到后台 → 扔到每个用户的队列中

      • 递归获取消息, 去自己的队列中获取数据,然后展示在界面中。

        问题:

        • 服务端持有连接,压力是否会很大?

          如果基于IO多复用 + 异步,还会有这种情况吗? 可以

        • 如果后台有100线程,同时对应100个用户的请求,则在等待期间(假设10s),这100个用户则一直占用县城,如果有更多的用户请求,则需等待释放。

          webSocket

          原来的web中:

          • http协议: 无状态 & 短连接

            • 客户端主动连接服务端。

            • 客户端向服务端发送消息,服务端接收后,返回数据

            • 客户端接收到数据

            • 断开连接

            • https协议 = http协议 + 对数据进行加密

              我们在开发过程中,想要保留一些状态信息,基于Cookie来做

              现在支持:

              • http协议:一次请求一次响应

              • websocket协议: 创建持有的连接不断开,基于这个连接可以进行收发数据。【服务端向客户端主动推送消息】

                • web聊天室

                • 实时图标,柱状图、饼图(echarts)

                  WebSocket原理

                  • http协议

                    • 连接

                    • 数据传输

                    • 断开连接

                    • websocket 协议 → 建立在 http 协议之上

                      • 连接, 客户端发出请求

                      • 握手(验证), 客户端发送一段消息,后端接收到消息后,做一些特殊处理并返回。服务端支持 websocket 协议

                        https://www.cnblogs.com/wupeiqi/p/6558766.html

                      • 客户端向服务端发送握手信息

                        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
                        ...
                        ...
                        
                        • 服务端接收
                          • 接收后的加密过程
                            // Sec-WebSocket-Key 与 magic String 进行拼接
                            Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==  
                            magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
                            v1 = "mnwFxiOlctXFN/DeMt1Amg==" +  '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
                            v2 = hmac1(v1)  // 通过 hmac1 进行加密
                            v3 = base64(v2) // 通过 base64 加密
                            
                            • 返回数据
                              HTTP/1.1 101 Switching Protocols
                              Upgrade:websocket
                              Connection: Upgrade
                              Sec-WebSocket-Accept: 密文
                              
                              • 收发数据(加密)

                                • 先获取第 2 个字节,对应 8 位

                                • 在获取第二个字节的后 7 位

                                • 断开连接

                                  Django 框架实现 WebSocket

                                  Django 默认不支持WebSocket,安装第三方组件

                                  pip install channels

                                  版本不能超过4.0,最好是3.0.5,不然不能成功启动asgi

                                  配置:

                                  django channels - 武沛齐 - 博客园

                                  • 注册 channels
                                    	INSTALLED_APPS = [
                                    	    'django.contrib.admin',
                                    	    'django.contrib.auth',
                                    	    'django.contrib.contenttypes',
                                    	    'django.contrib.sessions',
                                    	    'django.contrib.messages',
                                    	    'django.contrib.staticfiles',
                                    	    'channels',
                                    	    'app01.apps.App01Config'
                                    	]
                                    
                                    • 在 settings.py 中添加 asgi_application
                                      	ASGI_APPLICATION = 'ws_demo.asgi.application'
                                      
                                      • 修改 asgi.py文件
                                        	import os
                                        	
                                        	from django.core.asgi import get_asgi_application
                                        	from channels.routing import ProtocolTypeRouter, URLRouter
                                        	from ws_demo import routings
                                        	
                                        	os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ws_demo.settings')
                                        	
                                        	# application = get_asgi_application()
                                        	
                                        	# 支持 http 和 WebSocket 请求
                                        	application = ProtocolTypeRouter({
                                        	    "http": get_asgi_application(),  # 自动找 urls.py , 找视图函数  --》 http
                                        	    "websocket": URLRouter(routings.websocket_urlpatterns),  # routing(urls)、 consumers(views)
                                        	})
                                        
                                        • 在 settings.py同级目录下,创建routings.py
                                          from django.urls import re_path
                                          from app01 import consumers
                                          websocket_urlpatterns = [
                                              # 示例 url : xxxxx/room/x1/
                                              re_path(r"room/(?P\w+)/$", consumers.ChatConsumer.as_asgi())
                                          ]
                                          
                                          • 在 app01 目录下,创建consumers.py ,用于设置 WebSocket 请求
                                            from channels.generic.websocket import WebsocketConsumer
                                            from channels.exceptions import StopConsumer
                                            class ChatConsumer(WebsocketConsumer):  # 继承WebsocketConsumer
                                               def websocket_connect(self, message):
                                                   print("有人进行连接了。。。。")
                                                   # 有客户端向后端发送 WebSocket 连接的请求时,自动触发(握手)
                                                   self.accept()
                                               def websocket_receive(self, message):
                                                   # 浏览器基于 WebSocket 向后端发送数据,自动触发接收消息
                                                   print(message)
                                                   self.send("不要回复不要回复!!!")
                                               def websocket_disconnect(self, message):
                                                   # 客户端向服务端断开连接时,自动触发
                                                   print("连接断开!!")
                                                   raise StopConsumer()
                                            

                                            django 中,需要了解:

                                            • wsg:

                                              Django使用WebSocket,在这里插入图片描述,第3张

                                              • asgi: wsgi + 异步 + WebSocket

                                                Django使用WebSocket,在这里插入图片描述,第4张

                                                聊天室

                                                • 访问地址看到聊天室界面,使用 http 请求

                                                • 让客户端主动向服务端发起 websocket连接,服务端接收到连接后,进行握手

                                                  • 客户端向后台发布WebSocket请求

                                                    var socket = new WebSocket("ws://localhost:8000/room/123/")
                                                    
                                                  • 服务端接收消息

                                                       from channels.generic.websocket import WebsocketConsumer
                                                       from channels.exceptions import StopConsumer
                                                       
                                                       
                                                       class ChatConsumer(WebsocketConsumer):  # 继承WebsocketConsumer
                                                           def websocket_connect(self, message):
                                                               print("有人进行连接了。。。。")
                                                               # 有客户端向后端发送 WebSocket 连接的请求时,自动触发(握手)
                                                               self.accept()
                                                    
                                                  • 收发消息(客户端向服务端发消息)

                                                    • 客户端发送消息
                                                      
                                                      
                                                      
                                                         
                                                         Title
                                                         
                                                      
                                                      
                                                         
                                                         
                                                      
                                                      
                                                      
                                                      • 服务端接收消息
                                                        def websocket_receive(self, message):
                                                         # 浏览器基于 WebSocket 向后端发送数据,自动触发接收消息
                                                         text = message["text"]
                                                         print("接收到的消息为:", text)
                                                        
                                                        • 收发消息(服务端主动发给客户端)
                                                          # 连接之后,服务端给客户端发送消息
                                                          self.send("来了啊 !!!")
                                                          
                                                          // 回调函数,客户端接收服务端消息
                                                          socket.onmessage = function (event){
                                                            console.log(event.data)
                                                          }
                                                          

                                                          其他方法

                                                          // 创建websocket对象,向后台发送请求
                                                          let socket = new WebSocket("ws://localhost:8000/room/123/");
                                                          // 当客户端和服务端刚创建好连接(self.accept)之后,自动触发.
                                                          socket.onopen = function (event){
                                                            let tag = document.createElement("div");
                                                            tag.innerText = "[连接成功]";
                                                            document.getElementById("message").appendChild(tag);
                                                          }
                                                          // 回调函数,客户端接收服务端消息
                                                          socket.onmessage = function (event){
                                                            let tag = document.createElement("div");
                                                            tag.innerText = event.data;
                                                            document.getElementById("message").appendChild(tag);
                                                          }
                                                          // 当断开连接时,触发该函数
                                                          socket.onclose =function (event){
                                                            let tag = document.createElement("div");
                                                            tag.innerText = "[连接断开]";
                                                            document.getElementById("message").appendChild(tag);
                                                          }
                                                          function sendMessage(){
                                                            let tag = document.getElementById("txt");
                                                            socket.send(tag.value);
                                                          }
                                                          function closeMessage(){
                                                            socket.close();
                                                          }
                                                          function handleKeyPress(event){
                                                            if (event.keyCode === 13){
                                                              document.getElementById("send").click();
                                                              document.getElementById("txt").value = "";
                                                            }
                                                          }
                                                          document.onkeydown = handleKeyPress;
                                                          
                                                              def websocket_connect(self, message):
                                                                  print("有人进行连接了。。。。")
                                                                  # 有客户端向后端发送 WebSocket 连接的请求时,自动触发(握手)
                                                                  self.accept()
                                                                  # 连接之后,服务端给客户端发送消息
                                                                  self.send("来了啊 !!!")
                                                              def websocket_receive(self, message):
                                                                  # 浏览器基于 WebSocket 向后端发送数据,自动触发接收消息
                                                                  text = message["text"]
                                                                  print("接收到的消息为:", text)
                                                                  # 当接收的值为【关闭】,则服务端关闭连接
                                                                  if text == "close":
                                                                      self.close()
                                                                      return
                                                                  else:
                                                                      self.send(text + "NB")
                                                              def websocket_disconnect(self, message):
                                                                  # 客户端向服务端断开连接时,自动触发
                                                                  print("断开连接!!!")
                                                                  # 当客户端断开连接时,服务端也需关闭与客户端的连接,连接是双向的
                                                                  raise StopConsumer()
                                                          

                                                          小结

                                                          现在的交互还是停留在对某个人的操作

                                                          群聊

                                                          基于 channels 中提供的channel layers 来实现

                                                          • settings 中配置
                                                            # 声明基于内存的 channel layers
                                                            CHANNEL_LAYERS = {
                                                                "default": {
                                                                    "BACKEND": "channels.layers.InMemoryChannelLayer"
                                                                }
                                                            }
                                                            

                                                            也可声明基于 redis 的 channel layer → pip install channels-redis

                                                            # 基于redis 内存的 channel layers
                                                            CHANNEL_LAYERS = {
                                                                "default": {
                                                                    "BACKEND": "channels_redis.core.RedisChannelLayer",
                                                                    "CONFIG": {
                                                                        "hosts": ["redis://10.211.55.25:6379/1"]
                                                                    }
                                                                }
                                                            }
                                                            
                                                            • consumers 中特殊的代码
                                                              from channels.generic.websocket import WebsocketConsumer
                                                              from channels.exceptions import StopConsumer
                                                              from asgiref.sync import async_to_sync
                                                              class ChatConsumer(WebsocketConsumer):  # 继承WebsocketConsumer
                                                                  def websocket_connect(self, message):
                                                                      # 接收客户端的连接
                                                                      self.accept()
                                                                      print("连接成功!!!")
                                                                      # 获取群号
                                                                      group_num = self.scope["url_route"]["kwargs"].get("group")
                                                                      # 将这个客户端的链接对象添加到某个地方(内存或者 redis)
                                                                      # self.channel_layer.group_add(group_num, self.channel_name)
                                                                      async_to_sync(self.channel_layer.group_add)(group_num, self.channel_name)
                                                                  def websocket_receive(self, message):
                                                                      # 浏览器基于 WebSocket 向后端发送数据,自动触发接收消息
                                                                      text = message["text"]
                                                                      print("接收到的消息为:", text)
                                                                      group_num = self.scope["url_route"]["kwargs"].get("group")
                                                                      print("group_num", group_num)
                                                                      # 通知组内的所有的客户端,执行 xx_oo方法,在方法中可以自定义任意的功能
                                                                      # self.channel_layer.group_send(group_num, {"type": "xx.oo", "message": message})
                                                                      async_to_sync(self.channel_layer.group_send)(group_num, {"type": "xx.oo", "message": message})
                                                                  def xx_oo(self, event):
                                                                      text = event["message"]["text"]
                                                                      print("发送的 text:", text)
                                                                      self.send(text)  # 给组中的每一个人去发送消息
                                                                  def websocket_disconnect(self, message):
                                                                      # 客户端向服务端断开连接时,自动触发
                                                                      print("断开连接!!!")
                                                                      group_num = self.scope["url_route"]["kwargs"].get("group_num")
                                                                      # self.channel_layer.group_discard(group_num, self.channel_name)
                                                                      async_to_sync(self.channel_layer.group_discard)(group_num, self.channel_name)
                                                                      raise StopConsumer()
                                                                      
                                                              
                                                              
                                                              
                                                              
                                                                  
                                                                  Title
                                                                  
                                                              
                                                              
                                                                  
                                                                  
                                                                  
                                                              
                                                              
                                                              

                                                              总结