{"id":20619765,"url":"https://github.com/twtrubiks/django-channels2-tutorial","last_synced_at":"2025-04-15T12:12:27.055Z","repository":{"id":84518914,"uuid":"129090902","full_name":"twtrubiks/django-channels2-tutorial","owner":"twtrubiks","description":"django-channels2 tutorial 💬","archived":false,"fork":false,"pushed_at":"2018-04-12T13:46:17.000Z","size":14,"stargazers_count":33,"open_issues_count":1,"forks_count":6,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-15T12:12:10.165Z","etag":null,"topics":["channels","chat-room","django","docker","tutorial-code"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/twtrubiks.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-04-11T12:34:26.000Z","updated_at":"2022-09-15T16:46:56.000Z","dependencies_parsed_at":null,"dependency_job_id":"f0046193-dcf6-42ca-82af-d47c3ff62d0c","html_url":"https://github.com/twtrubiks/django-channels2-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twtrubiks%2Fdjango-channels2-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twtrubiks%2Fdjango-channels2-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twtrubiks%2Fdjango-channels2-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twtrubiks%2Fdjango-channels2-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/twtrubiks","download_url":"https://codeload.github.com/twtrubiks/django-channels2-tutorial/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249067779,"owners_count":21207396,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["channels","chat-room","django","docker","tutorial-code"],"created_at":"2024-11-16T12:12:27.912Z","updated_at":"2025-04-15T12:12:27.048Z","avatar_url":"https://github.com/twtrubiks.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# django-channels2-tutorial\n\n django-channels2-tutorial 💬\n\n* [Youtube Tutorial Part1 - django channels2 demo 以及簡介](https://youtu.be/jIMtZkfs8yY)\n\n* [Youtube Tutorial Part2 - django channels2 tutorial](https://youtu.be/_4Q801WL8sA)\n\n## 前言\n\n最近剛好想玩一下聊天室，於是就找到 [Channels](https://github.com/django/channels)，也從 [releases](https://github.com/django/channels/releases) 這裡發現在今年 2 月的時候 **Channels 2**\n\n被 releases 出來，所以決定簡單整理一篇介紹給大家。\n\n透過 Django [Channels](https://github.com/django/channels) 建立簡單的聊天室範例，此範例為官方的 [Tutorial](https://channels.readthedocs.io/en/latest/tutorial/index.html)，希望能透過這個簡單的範例，讓大\n\n家更了解 Channels，我有稍微修改一些部分，官方範例是使用  Channels 2.0 ,  Python 3.5+ , Django 1.11，\n\n我這邊最主要的是將他修改為 Django 2.0。\n\n注意，Channels 1 和 Channels 2 有蠻大的差異，這邊都是講 Channels 2，詳細可參考 [What’s new in Channels 2?](https://channels.readthedocs.io/en/latest/one-to-two.html)\n\n之前，我也有使用過 flask 寫過聊天室，可參考 [chat-room](https://github.com/twtrubiks/chat-room)。\n\n讓我們先來看看執行的畫面吧:laughing:\n\n## 執行畫面\n\n輸入一個名稱建立聊天室群組，直接瀏覽 [http://localhost:8000/chat/](http://localhost:8000/chat/)\n\n![alt tag](https://i.imgur.com/WSZEJNU.png)\n\n接著可以在聊天室裡面打字\n\n![alt tag](https://i.imgur.com/sraj4Ls.png)\n\n同一個聊天室群組會互相收到訊息，不同的聊天室群組訊息 **不會互通**，\n\n![alt tag](https://i.imgur.com/JDD0JYb.png)\n\n我知道這個聊天室真的非常的醜:joy:，而且也沒搭配 database，但這篇只是一個要讓大家了解 Channels\n\n如何建立一個聊天室，下一篇文章，我會依照這篇為雛形，建立一個有簡單的登入註冊系統以及美化過的\n\n聊天室給各位，如果大家等不及想先搶先看，可瀏覽 [django-chat-room](https://github.com/twtrubiks/django-chat-room)。\n\n但建議這篇文章還是要看，因為我將介紹一些基本的概念以及互動的流程。\n\n## 如何執行\n\n確認電腦有安裝 docker 後，直接執行以下指令即可，\n\n```cmd\ndocker-compose up\n```\n\n![alt tag](https://i.imgur.com/jTFNXoH.png)\n\n如何移除 ( 包含移除 volume )，\n\n```cmd\ndocker-compose down -v\n```\n\n## 簡介\n\n這邊先介紹幾個名詞，我不會講的非常詳細，因為大家可以用關鍵字去 google ，很多文章都解釋非常清楚了:grin:\n\n### WebSocket\n\nWebSocket 是一種單一 TCP 連線上進行全雙工（full-duplex）通訊管道，可以讓網頁與伺服器之間做即時性、\n\n雙向的資料傳遞。\n\nWebsocket 需要先建立連線，需要通過瀏覽器發出請求，之後伺服器進行回應，這段過程稱為 **交握**（ handshaking ）。\n\n延伸閱讀，如果大家有興趣，可以再去看看 polling ( 輪詢 ) 的概念。\n\n### Channels\n\n本次的主角，你可以把 Django 想成是 synchronous ( 同步 )，而透過 Channels，可以改變\n\nDjango synchronous（ 同步）的核心轉變為 asynchronous（非同步）的程式碼。\n\n以下擷取官方文件\n\nchannels allowing Django projects to handle not only HTTP, but protocols that require long-running connections too WebSockets, MQTT, chatbots, amateur radio, and more.\n\nit provides integrations with Django’s auth system, session system, and more, making it easier than ever to extend your HTTP-only project to other protocols.\n\nchannels 支持很多協定，而且也整合了 Django 的 auth 以及 session 系統等等。\n\n### ASGI\n\nASGI 全名為 Asynchronous Server Gateway Interface，\n\n他是 WSGI 的精神繼承者，不只是使用 `asyncio` 異步的方法運行，而且也支援多種協定。\n\n更多說明可參考 [ASGI](https://channels.readthedocs.io/en/stable/asgi.html)。\n\n## 教學\n\n我將簡單說明這個範例的流程，但詳細的介紹，我還是非常建議大家觀看官方的 [Tutorial](https://channels.readthedocs.io/en/latest/tutorial/index.html) 範例。\n\n### 建立環境\n\n這部份只是和大家說明基本的環境設定，其實直接 `docker-compose up` 即可，因為我都幫大家包成 docker 了，\n\n解決了環境的問題（ 像我在 windows 上 `channels` 一直裝不起來 :expressionless:）。\n\n首先， 我使用的 Python 版本為 3.6.4，\n\n安裝套件\n\n```cmd\npip install -r requirements.txt\n```\n\n[requirements.txt](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/requirements.txt)\n\n```txt\nDjango==2.0.4\nchannels==2.0.2\nchannels_redis==2.1.1\n```\n\n使用 Django 2.0.4 以及 channels 2.0.2，channels_redis 2.1.1 為 `CHANNEL_LAYERS` 中的 `BACKEND` 需要使用到的。\n\n### 用 docker 建立 redis\n\n這部份只是和大家說明基本的環境設定，其實直接 `docker-compose up` 即可，因為我都幫大家包成 docker 了，\n\n解決了環境的問題（ 像我在 windows 上 `channels` 一直裝不起來 :expressionless:）。\n\n因為這邊會使用到 redis，所以使用 docker 建立 redis，如果不了解 docker 以及 redis ，\n\n可參考下面這兩篇文章，分別介紹了 docker 以及 redis\n\n* [Docker 基本教學 - 從無到有 Docker-Beginners-Guide](https://github.com/twtrubiks/docker-tutorial)\n\n* [django-docker-redis-tutorial 基本教學](https://github.com/twtrubiks/django-docker-redis-tutorial)\n\n建立 redis 指令，\n\n```cmd\ndocker run --name some-redis  -p 6379:6379  -d redis redis-server --appendonly yes\n```\n\n### channels installation\n\n接下來將介紹 channels 的設定，官方文件可參考 [installation](https://channels.readthedocs.io/en/latest/installation.html)，\n\n將 channels 加入 INSTALLED_APPS，\n\ndjango_channels2_tutorial/[settings.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/settings.py)\n\n```python\nINSTALLED_APPS = [\n    ....\n    'django.contrib.messages',\n    'django.contrib.staticfiles',\n    'channels',\n    'chat',\n]\n```\n\n溫馨小提醒:heart:\n\n`channels` 官方範例會將他放在最前面的原因是，有些套件會衝突，所以將他放到第一順位這樣。\n\nchat 是我們建立的（ 後面會介紹 ），\n\n接著建立 default routing，\n\ndjango_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py)\n\n```python\nfrom channels.auth import AuthMiddlewareStack\nfrom channels.routing import ProtocolTypeRouter, URLRouter\n\nimport chat.routing\n\napplication = ProtocolTypeRouter({\n    # Empty for now (http-\u003edjango views is added by default)\n    'websocket': AuthMiddlewareStack(\n        URLRouter(\n            chat.routing.websocket_urlpatterns\n        )\n    ),\n})\n```\n\n`chat.routing` 以及 `chat.routing.websocket_urlpatterns` 是我們自己建立的（ 後面會介紹 ），\n\n設定 channel settings，\n\ndjango_channels2_tutorial/[settings.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/settings.py)\n\n```python\nASGI_APPLICATION = \"django_channels2_tutorial.routing.application\"\nCHANNEL_LAYERS = {\n    'default': {\n        'BACKEND': 'channels_redis.core.RedisChannelLayer',\n        'CONFIG': {\n            'hosts': [('redis', 6379)],\n        },\n    }\n}\n```\n\n`ASGI_APPLICATION` 設定為自己的 project 名稱 ( 這裡我們命名為 `django_channels2_tutorial` )，\n\n指向底下的 routing（ 我們剛剛建立的 ）裡的 application（ 剛剛建立的 ），所以完整名稱為\n\n`django_channels2_tutorial.routing.application`。\n\n`CHANNEL_LAYERS` 中的 `BACKEND` 設定為 redis ，也就是為什麼我們前面要安裝 `channels_redis` 的原因，\n\n`CONFIG` 就是設定連線 redis 字串，是不是很好奇為什麼 `host` 的部份我直接寫 `redis`？\n\n( 其實就是 [docker-compose.yml](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/docker-compose.yml) 中的 redis 名稱 )。\n\n如果大家還是不了解，建議可以閱讀 [這篇](https://github.com/twtrubiks/docker-tutorial#user-defined-networks) 的說明。\n\n### 說明\n\n剛剛上面提到了 [chat](https://github.com/twtrubiks/django-channels2-tutorial/tree/master/chat) 資料夾，接下來讓我們來看看 [chat](https://github.com/twtrubiks/django-channels2-tutorial/tree/master/chat) 做了什麼事情，\n\nchat/[views.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/views.py)\n\n```python\nimport json\n\nfrom django.shortcuts import render\nfrom django.utils.safestring import mark_safe\n\ndef index(request):\n    return render(request, 'chat/index.html', {})\n\n\ndef room(request, room_name):\n    return render(request, 'chat/room.html', {\n        'room_name_json': mark_safe(json.dumps(room_name))\n    })\n```\n\nchat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py)\n\n```python\n# chat/urls.py\nfrom django.urls import path\n\nfrom . import views\n\nurlpatterns = [\n    path('', views.index, name='index'),\n    path('\u003cstr:room_name\u003e/', views.room, name='room'),\n]\n```\n\n這邊很簡單，就是定義好 views 以及 url 而已，比較需要注意的是 url 的部份，\n\n因為我們使用的是 `django 2.0`，所以已經改用 `path` 了，其實總體來說，我\n\n覺得`django 2.0` 在處理 url 上更方便了，以前要寫正則表達式:scream:。\n\n我們來看一下比較重要的 consumers，\n\n詳細的介紹，可參考官網說明 [consumers](https://channels.readthedocs.io/en/stable/topics/consumers.html)，\n\n這裡先給大家簡單的觀念，consumers 是在 Channels 中的一個基本單位，當一個 request 或 socket 進來時，\n\nChannels 會去找他的 routing table，找到對的 consumers，基本上，consumers 就像是 Django 中的 views。\n\nconsumers 有兩個點要和大家提一下（ 擷取官方說明 ），\n\n* Structures your code as a series of functions to be called whenever an event happens, rather than making you write an event loop.\n\n* Allow you to write synchronous or async code and deals with handoffs and threading for you.\n\n先來看 chat/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/routing.py)，\n\n```python\nfrom django.urls import path\n\nfrom . import consumers\n\nwebsocket_urlpatterns = [\n    path('ws/chat/\u003cstr:room_name\u003e/', consumers.ChatConsumer),\n]\n```\n\n定義了 websocket_urlpatterns，並且設定 `ChatConsumer` class，\n\n那我們在哪邊定義這個 routing 呢 ？\n\ndjango_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py)\n\n```python\nfrom channels.auth import AuthMiddlewareStack\nfrom channels.routing import ProtocolTypeRouter, URLRouter\n\nimport chat.routing\n\napplication = ProtocolTypeRouter({\n    # Empty for now (http-\u003edjango views is added by default)\n    'websocket': AuthMiddlewareStack(\n        URLRouter(\n            chat.routing.websocket_urlpatterns\n        )\n    ),\n})\n```\n\nroot routing 設定的地方就是在前面介紹的 django_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py)，\n\n也就是上面的 `chat.routing.websocket_urlpatterns`。\n\n接下來看 chat/[consumers.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/consumers.py)，\n\n```python\nimport json\n\nfrom asgiref.sync import async_to_sync\nfrom channels.generic.websocket import WebsocketConsumer\n\n\nclass ChatConsumer(WebsocketConsumer):\n    def connect(self):\n        self.room_name = self.scope['url_route']['kwargs']['room_name']\n        self.room_group_name = 'chat_%s' % self.room_name\n\n        # Join room group\n        async_to_sync(self.channel_layer.group_add)(\n            self.room_group_name,\n            self.channel_name\n        )\n\n        self.accept()\n\n    def disconnect(self, close_code):\n        # Leave room group\n        async_to_sync(self.channel_layer.group_discard)(\n            self.room_group_name,\n            self.channel_name\n        )\n\n    # Receive message from WebSocket\n    def receive(self, text_data):\n        text_data_json = json.loads(text_data)\n        message = text_data_json['message']\n\n        # Send message to room group\n        async_to_sync(self.channel_layer.group_send)(\n            self.room_group_name,\n            {\n                'type': 'chat_message',\n                'message': message\n            }\n        )\n\n    # Receive message from room group\n    def chat_message(self, event):\n        message = event['message']\n\n        # Send message to WebSocket\n        self.send(text_data=json.dumps({\n            'message': message\n        }))\n```\n\n以上是使用 synchronous（ 同步 ）的方法。\n\n接著將介紹他們互動的流程（ 事件如何觸發 ），\n\n`connect`\n\n當前端發 Websocket 過來的時候會觸發此事件，\n\n那前端哪時候會送訊息過來呢 ？\n\n我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html)，\n\n```javascript\n...\n\u003cscript\u003e\n    var roomName = {{ room_name_json }};\n\n    var chatSocket = new WebSocket(\n        'ws://' + window.location.host + '/ws/chat/' + roomName + '/');\n    ...\n\u003c/script\u003e\n```\n\n當前端 WebSocket 初始話連線的時候，會觸發 `connect`。\n\n接下來說明 `connect` 中的一些方法，\n\n首先是 `self.scope` 這個，你可以把它想成像是 Django 裡的 `self.request`，\n\n而 `url_route` 則是抓取 url，我們取出 `room_name` ，為什麼是 `room_name` ，\n\n原因是我們在 chat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py) 中設定 `urlpatterns` 變數為  `room_name`，\n\nchat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py)\n\n```python\nfrom django.urls import path\n\nfrom . import views\n\nurlpatterns = [\n    path('', views.index, name='index'),\n    path('\u003cstr:room_name\u003e/', views.room, name='room'),\n]\n```\n\n接下來我們透過 `async_to_sync` 把 channel 加入 group 中，channel 和 group 的關係也不用想的太複雜，\n\n其實他們的關係就是一個 group 中，可以有很多個 channel 這樣。\n\n最後是 `self.accept()` 這個，就是接受這個連線，如果要拒絕這次的連線，使用 `self.close()` 即可。\n\n`disconnect`\n\n將 channel 從 group 中移除，\n\n我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html)\n\n```javascript\n...\n\u003cscript\u003e\n    ....\n    chatSocket.onclose = function(e) {\n        console.error('Chat socket closed unexpectedly');\n    };\n    ....\n\u003c/script\u003e\n```\n\n當 server 端的 WebSocket 關閉時，前端的 `chatSocket.onclose` 會被觸發。\n\n`receive`\n\n當我們收到來至前端的 WebSocket 訊息時，\n\n那前端哪時候會送訊息過來呢 ？\n\n我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html)\n\n```javascript\n...\n\u003cscript\u003e\n    ....\n    document.querySelector('#chat-message-submit').onclick = function(e) {\n        var messageInputDom = document.querySelector('#chat-message-input');\n        var message = messageInputDom.value;\n        chatSocket.send(JSON.stringify({\n            'message': message\n        }));\n\n        messageInputDom.value = '';\n    };\n\u003c/script\u003e\n```\n\n`chatSocket.send` 就會觸發這個事件，`receive` 將收到的 message 送到對應的\ngroup 中，\n\n`type` 就是指 `chat_message`。\n\n`chat_message`\n\n當從 group 中收到 message 時，會觸發這個事件，我們將收到的 message 送回前端的 WebSocket，\n\n那前端誰接收的？\n\n我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html)\n\n```javascript\n...\n\u003cscript\u003e\n    ....\n    chatSocket.onmessage = function(e) {\n        var data = JSON.parse(e.data);\n        var message = data['message'];\n        document.querySelector('#chat-log').value += (message + '\\n');\n    };\n    ....\n\u003c/script\u003e\n```\n\n`chatSocket.onmessage` 會收到訊息，前端再將訊息增加到畫面上。\n\n以上，就是整個前後端 WebSocket 事件互動的流程。\n\n### Rewrite Chat Server as Asynchronous\n\n官網可參考 [Tutorial Part 3](https://channels.readthedocs.io/en/latest/tutorial/part_3.html)，\n\n剛剛是使用 synchronous（ 同步 ）的方法，現在我們要改寫他為 asynchronous（ 非同步）的方法，\n\n```python\nimport json\nfrom channels.generic.websocket import AsyncWebsocketConsumer\n\n\nclass ChatConsumer(AsyncWebsocketConsumer):\n    async def connect(self):\n        self.room_name = self.scope['url_route']['kwargs']['room_name']\n        self.room_group_name = 'chat_%s' % self.room_name\n\n        # Join room group\n        await self.channel_layer.group_add(\n            self.room_group_name,\n            self.channel_name\n        )\n\n        await self.accept()\n\n    async def disconnect(self, close_code):\n        # Leave room group\n        await self.channel_layer.group_discard(\n            self.room_group_name,\n            self.channel_name\n        )\n\n    # Receive message from WebSocket\n    async def receive(self, text_data):\n        text_data_json = json.loads(text_data)\n        message = text_data_json['message']\n\n        # Send message to room group\n        await self.channel_layer.group_send(\n            self.room_group_name,\n            {\n                'type': 'chat_message',\n                'message': message\n            }\n        )\n\n    # Receive message from room group\n    async def chat_message(self, event):\n        message = event['message']\n\n        # Send message to WebSocket\n        await self.send(text_data=json.dumps({\n            'message': message\n        }))\n\n```\n\n官網的最後一部分是 [Automated Testing](https://channels.readthedocs.io/en/latest/tutorial/part_4.html)，這部份我就沒有寫了，如果各位有興趣，就請再自行前往閱讀。\n\n## 後記\n\n這次和大家解釋了利用 channels 建立出的簡易版 chat room，也說明了他們互動的方式以及過程，\n\n希望可以對 channels 有基礎的認識，如果意猶未盡，可以參考下一篇結合 database 以及美化的聊天\n\n室，基本上是用這篇的教學延伸出去的，可參考 [django-chat-room](https://github.com/twtrubiks/django-chat-room) 。\n\n## 執行環境\n\n* Python 3.6.4\n\n## Reference\n\n* [Django](https://www.djangoproject.com/)\n* [Channels](https://github.com/django/channels)\n\n## Donation\n\n文章都是我自己研究內化後原創，如果有幫助到您，也想鼓勵我的話，歡迎請我喝一杯咖啡:laughing:\n\n![alt tag](https://i.imgur.com/LRct9xa.png)\n\n[贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8)\n\n## License\n\nMIT licens\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwtrubiks%2Fdjango-channels2-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftwtrubiks%2Fdjango-channels2-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwtrubiks%2Fdjango-channels2-tutorial/lists"}