Django使用channels实现Websocket连接

  • 2020 年 12 月 18 日
  • 笔记

简述:

  需求:消息实时推送消息以及通知功能,采用django-channels来实现websocket进行实时通讯。并使用docker、daphne启动通道,保持websocket后台运行

介绍Django Channels:

官方文档链接://channels.readthedocs.io/en/latest/introduction.html

  #参考 Django Channels 的docs文档
  通道包装了Django的本机异步视图支持,允许Djangoprojects不仅处理HTTP,还处理需要长时间运行的连接的协议——网络套接字、MQTT、聊天机器人、业余无线电等等。

  它做到了这一点,同时保留了Django的同步和易于使用的特性,允许您选择如何编写代码——以Django视图的方式同步,完全异步,或者两者兼而有之。

  除此之外,它还提供了与Django的授权系统、会话系统等的集成,使得将您的纯HTTP项目扩展到其他协议比以往任何时候都更容易

  #channels由几个包组成:
  Channels,Django集成层
  Daphne,HTTP和Websocket终止服务器
  asgiref,基本ASGI库
  channels_redis,Redis通道层后端(可选) 

依赖性:

    这里假设你的django版本是3.0以上,所有channels项目目前都支持Python 3.6及更高版本。
  channels与Django 2.2、3.0和3.1兼容。如果你的django版本是2.2或以下,相关配置请查看官网。

安装django channels:

  pip install -U channels      #pip 安装需要加上-U

Django项目设置:

  #setting.py添加内容
  将Channels库添加到已安装的应用程序列表中。编辑 settings.py 文件,并将channels添加到INSTALLED_APPS设置中。

  INSTALLED_APPS = [
  'channels',  #在这里添加,请保持channels在第一项
  'appname',            
  ]

  ASGI_APPLICATION = "myproject.asgi.application"      #就这样!一旦启用,通道将自己集成到Django中并控制runserver命令

运行python manage.py runserver 命令:

  System check identified no issues (0 silenced).
  December 18, 2020 - 09:14:59
  Django version 3.1.2, using settings 'myproject.settings'
  Starting ASGI/Channels version 3.0.2 development server at //127.0.0.1:8000/      #<<<请注意这行与之前的区别
  Quit the server with CTRL-BREAK.

描述WSGI与ASGI:

篇幅原因,请参看另一篇博文://www.cnblogs.com/mqlwyz/p/14149945.html

逻辑代码

  #创建默认路由(主WS路由)
  WS(WebSocket )是不安全的 ,容易被窃听,因为任何人只要知道你的ip和端口,任何人都可以去连接通讯。
  WSS(Web Socket Secure)是WebSocket的加密版本。
  所以建议使用WSS,这是测试版使用的是WS
  通道提供了对常见Django特性(如会话和身份验证)的轻松插入支持。只需在WebSocket视图周围添加适当的中间件,就可以将身份验证与WebSocket视图结合起来

在myproject/myproject下创建routing.py:

  from channels.routing import ProtocolTypeRouter,URLRouter
  import os
  from channels.auth import AuthMiddlewareStack
  import appname.routing
  from django.core.asgi import get_asgi_application

  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
  django_asgi_app = get_asgi_application()

  application = ProtocolTypeRouter({
      # (your routes here)
      'http' : django_asgi_app,
      'websocket': AuthMiddlewareStack(
          URLRouter(
              appname.routing.websocket_urlpatterns
          )
      )
  })

django2.2配置:

  注:Django 2.2没有内置的ASGI支持,所以我们需自行在myproject/myproject下创建asgi.py:

  import os
  import django
  from channels.routing import get_default_application

  os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

  django.setup()

  application = get_default_application()

使用Docker安装和运行Redis:

    我们使用Redis作为channel层的后备存储,这是我们在本教程中使用的Channels库的可选组件。从Docker的官方网站上
  安装Docker——有MacOS和Windows的官方运行时,使其易于使用,并且有许多Linux发行版的软件包,可以在本地运行。
  #注:使用的通道功能将需要Redis来运行,建议Docker是实现这一点的最简单方法。

我们将使用一个使用Redis作为后台存储的通道层。要在端口6379上启动Redis服务器,请运行以下命令:

  docker run -p 6379:6379 -d redis:5

参照图片:

使用运行协议服务器>Daphne:

注:Daphne是一个纯Python ASGI服务器,由Django项目的成员维护。它充当ASGI的参考服务器。
不过django官网上就寥寥几句话:>>>//docs.djangoproject.com/zh-hans/3.0/howto/deployment/asgi/daphne/

  #安装 Daphne
  
  python -m pip install daphne

为了与外界对话,需要将Channels/ASGI应用程序加载到协议服务器中。它们可以像WSGI服务器一样,以HTTP模式运行应用程序,但它们也可以连接到任何数量的其他协议(聊天协议、物联网协议、甚至无线网络)。
所有这些服务器都有自己的配置选项,但它们都有一个共同点——它们希望您向它们传递一个ASGI应用程序来运行。您只需在项目的asgi.py公司将文件作为它应该运行的应用程序发送到协议服务器:

  daphne -b 127.0.0.1 -p 8001 myproject.asgi:application      #使用daphne需单独安装redis,并配置添加windos服务,C:\Windows\System32\drivers\etc\hosts添加redis的ip地址,默认是127.0.0.1

运行之后成功参考如下代码:

  (venv) D:\myproject>daphne -b 127.0.0.1 -p 8001 myproject.asgi:application      #在你myproject项目下的文件下执行命令
  2020-12-18 09:15:55,042 INFO     Starting server at tcp:port=8001:interface=127.0.0.1
  2020-12-18 09:15:55,042 INFO     HTTP/2 support not enabled (install the http2 and tls Twisted extras)
  2020-12-18 09:15:55,043 INFO     Configuring endpoint tcp:port=8001:interface=127.0.0.1
  2020-12-18 09:15:55,050 INFO     Listening on TCP address 127.0.0.1:8001

我们需要知道如何安装redis频道。运行以下命令:

  pip install -U channels_redis

验证通道是否开启的方法:

  使用win + r 打开cmd,输入:
  
  telnet 127.0.0.1 6379      #6379为端口号,请使用相应的端口测试

telnet通的情况如下:

在使用通道层之前,我们必须对其进行配置。编辑myproject/setting.py文件并在底部添加一个CHANNEL_LAYERS设置。它应该看起来像:

  # myproject/settings.py
  # Channels
  ASGI_APPLICATION = "myproject.asgi.application"      #这是之前在setting设置过的,不要重复
  CHANNEL_LAYERS = {
      'default': {
          'BACKEND': 'channels_redis.core.RedisChannelLayer',
          'CONFIG': {
              'hosts': [
            ('localhost', 6379),     #可以配置多个通道层。然而,大多数项目将只使用一个“默认”通道层。
              #如果你是使用Docker安装运行redis的请注释下面的配置
            ('redis_server_name', 6379),
              #如果你是使用Daphne运行的请注释掉localhost那一行配置
        ],
          },
      },
  }

现在我们有了一个通道层,让我们在chatcustomer中使用它。在chat中输入以下代码appname/consumers.py,代码如下:

  # appname/consumers.py
  import json
  from channels.generic.websocket import AsyncWebsocketConsumer

  class ChatConsumer(AsyncWebsocketConsumer):
      async def connect(self):
          self.room_name = self.scope['url_route']['kwargs']['room_name']
          self.room_group_name = 'chat_%s' % self.room_name

          # Join room group
          await self.channel_layer.group_add(
              self.room_group_name,
              self.channel_name
          )

          await self.accept()

      async def disconnect(self, close_code):
          # Leave room group
          await self.channel_layer.group_discard(
              self.room_group_name,
              self.channel_name
          )

      # Receive message from WebSocket
      async def receive(self, text_data):
          text_data_json = json.loads(text_data)
          message = text_data_json['message']

          # Send message to room group
          await self.channel_layer.group_send(
              self.room_group_name,
              {
                  'type': 'chat_message',
                  'message': message
              }
          )

      # Receive message from room group
      async def chat_message(self, event):
          message = event['message']

          # Send message to WebSocket
          await self.send(text_data=json.dumps({
              'message': message
          }))

注:ChatConsumer现在继承了AsyncWebsocketConsumer而不是WebsocketConsumer。所有方法都是异步def,而不仅仅是def。

await用于调用执行I/O的异步函数。在通道层上调用方法时,不再需要异步同步。

应用下创建 routing.py (类似Django路由)

  from django.urls import path
  from appadmin import consumers

  websocket_urlpatterns = [
      # url(r'^ws/msg/(?P<room_name>[^/]+)/$', consumers.SyncConsumer),
      path("ws/test_async" , consumers.ChatConsumer.as_asgi()),
  ]

前端页面连接Websockct:

templates下新建test/test.html

注:需正常在urls.py添加路由

  <<!DOCTYPE html>
  <html lang="en">
  <head>
        <meta charset="UTF-8">
        <title>Websocket测试</title>
        <link rel="stylesheet" href="/static/layui/css/layui.css" media="all">
        <script src="//cdn.bootcss.com/jquery/1.12.3/jquery.min.js"></script>
        <script src="/static/layui/layui.js"></script>
  </head>
  <body style="margin: auto">
  <script>
      if ("WebSocket" in window) {
          // 打开一个 web socket
          var ws = new WebSocket(+ window.location.host + "/ws/test_async");
          console.log("ws:" + window.location.host + "/ws/test_async")
          console.log('ws',ws)
          ws.onopen = function () {
              // Web Socket 已连接上,使用 send() 方法发送数据
              alert("链接成功")
              ws.send("发送消息");
              console.log('onopen')
              {#alert("数据发送中...");#}
          };
          ws.onmessage = function (evt) {
              var received_msg = evt.data;
              {#alert("数据已接收...");#}
              console.log("数据:" + received_msg)
          };
          ws.onclose = function () {
              // 关闭 websocket
              alert("链接关闭,请重试")
              console.log("连接已关闭...");
          };
      }
      else {
          // 浏览器不支持 WebSocket
          alert("您的浏览器不支持 WebSocket!");
      }
  </script>

网页上访问

输入127.0.0.1:8000/test.html
按F12,查看console选项:
或提示消息: