Django使用channels實現Websocket連線

柒路abc發表於2020-12-18

簡述:

  需求:訊息實時推送訊息以及通知功能,採用django-channels來實現websocket進行實時通訊。並使用docker、daphne啟動通道,保持websocket後臺執行

介紹Django Channels:

官方文件連結:https://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 http://127.0.0.1:8000/      #<<<請注意這行與之前的區別
  Quit the server with CTRL-BREAK.

描述WSGI與ASGI:

篇幅原因,請參看另一篇博文:https://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官網上就寥寥幾句話:>>>https://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="http://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選項:
或提示訊息:

相關文章