├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── chat ├── __init__.py ├── admin.py ├── apps.py ├── consumers.py ├── models.py ├── routing.py ├── templates │ └── chat │ │ ├── index.html │ │ └── room.html ├── tests.py ├── urls.py └── views.py ├── django_channels2_tutorial ├── __init__.py ├── routing.py ├── settings.py ├── urls.py └── wsgi.py ├── docker-compose.yml ├── manage.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.idea/ 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.4 2 | LABEL maintainer twtrubiks 3 | ENV PYTHONUNBUFFERED 1 4 | RUN mkdir /django_channels2_tutorial 5 | WORKDIR /django_channels2_tutorial 6 | COPY . /django_channels2_tutorial/ 7 | RUN pip install -r requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-channels2-tutorial 2 | 3 | django-channels2-tutorial 💬 4 | 5 | * [Youtube Tutorial Part1 - django channels2 demo 以及簡介](https://youtu.be/jIMtZkfs8yY) 6 | 7 | * [Youtube Tutorial Part2 - django channels2 tutorial](https://youtu.be/_4Q801WL8sA) 8 | 9 | ## 前言 10 | 11 | 最近剛好想玩一下聊天室,於是就找到 [Channels](https://github.com/django/channels),也從 [releases](https://github.com/django/channels/releases) 這裡發現在今年 2 月的時候 **Channels 2** 12 | 13 | 被 releases 出來,所以決定簡單整理一篇介紹給大家。 14 | 15 | 透過 Django [Channels](https://github.com/django/channels) 建立簡單的聊天室範例,此範例為官方的 [Tutorial](https://channels.readthedocs.io/en/latest/tutorial/index.html),希望能透過這個簡單的範例,讓大 16 | 17 | 家更了解 Channels,我有稍微修改一些部分,官方範例是使用 Channels 2.0 , Python 3.5+ , Django 1.11, 18 | 19 | 我這邊最主要的是將他修改為 Django 2.0。 20 | 21 | 注意,Channels 1 和 Channels 2 有蠻大的差異,這邊都是講 Channels 2,詳細可參考 [What’s new in Channels 2?](https://channels.readthedocs.io/en/latest/one-to-two.html) 22 | 23 | 之前,我也有使用過 flask 寫過聊天室,可參考 [chat-room](https://github.com/twtrubiks/chat-room)。 24 | 25 | 讓我們先來看看執行的畫面吧:laughing: 26 | 27 | ## 執行畫面 28 | 29 | 輸入一個名稱建立聊天室群組,直接瀏覽 [http://localhost:8000/chat/](http://localhost:8000/chat/) 30 | 31 | ![alt tag](https://i.imgur.com/WSZEJNU.png) 32 | 33 | 接著可以在聊天室裡面打字 34 | 35 | ![alt tag](https://i.imgur.com/sraj4Ls.png) 36 | 37 | 同一個聊天室群組會互相收到訊息,不同的聊天室群組訊息 **不會互通**, 38 | 39 | ![alt tag](https://i.imgur.com/JDD0JYb.png) 40 | 41 | 我知道這個聊天室真的非常的醜:joy:,而且也沒搭配 database,但這篇只是一個要讓大家了解 Channels 42 | 43 | 如何建立一個聊天室,下一篇文章,我會依照這篇為雛形,建立一個有簡單的登入註冊系統以及美化過的 44 | 45 | 聊天室給各位,如果大家等不及想先搶先看,可瀏覽 [django-chat-room](https://github.com/twtrubiks/django-chat-room)。 46 | 47 | 但建議這篇文章還是要看,因為我將介紹一些基本的概念以及互動的流程。 48 | 49 | ## 如何執行 50 | 51 | 確認電腦有安裝 docker 後,直接執行以下指令即可, 52 | 53 | ```cmd 54 | docker-compose up 55 | ``` 56 | 57 | ![alt tag](https://i.imgur.com/jTFNXoH.png) 58 | 59 | 如何移除 ( 包含移除 volume ), 60 | 61 | ```cmd 62 | docker-compose down -v 63 | ``` 64 | 65 | ## 簡介 66 | 67 | 這邊先介紹幾個名詞,我不會講的非常詳細,因為大家可以用關鍵字去 google ,很多文章都解釋非常清楚了:grin: 68 | 69 | ### WebSocket 70 | 71 | WebSocket 是一種單一 TCP 連線上進行全雙工(full-duplex)通訊管道,可以讓網頁與伺服器之間做即時性、 72 | 73 | 雙向的資料傳遞。 74 | 75 | Websocket 需要先建立連線,需要通過瀏覽器發出請求,之後伺服器進行回應,這段過程稱為 **交握**( handshaking )。 76 | 77 | 延伸閱讀,如果大家有興趣,可以再去看看 polling ( 輪詢 ) 的概念。 78 | 79 | ### Channels 80 | 81 | 本次的主角,你可以把 Django 想成是 synchronous ( 同步 ),而透過 Channels,可以改變 82 | 83 | Django synchronous( 同步)的核心轉變為 asynchronous(非同步)的程式碼。 84 | 85 | 以下擷取官方文件 86 | 87 | channels allowing Django projects to handle not only HTTP, but protocols that require long-running connections too WebSockets, MQTT, chatbots, amateur radio, and more. 88 | 89 | it 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. 90 | 91 | channels 支持很多協定,而且也整合了 Django 的 auth 以及 session 系統等等。 92 | 93 | ### ASGI 94 | 95 | ASGI 全名為 Asynchronous Server Gateway Interface, 96 | 97 | 他是 WSGI 的精神繼承者,不只是使用 `asyncio` 異步的方法運行,而且也支援多種協定。 98 | 99 | 更多說明可參考 [ASGI](https://channels.readthedocs.io/en/stable/asgi.html)。 100 | 101 | ## 教學 102 | 103 | 我將簡單說明這個範例的流程,但詳細的介紹,我還是非常建議大家觀看官方的 [Tutorial](https://channels.readthedocs.io/en/latest/tutorial/index.html) 範例。 104 | 105 | ### 建立環境 106 | 107 | 這部份只是和大家說明基本的環境設定,其實直接 `docker-compose up` 即可,因為我都幫大家包成 docker 了, 108 | 109 | 解決了環境的問題( 像我在 windows 上 `channels` 一直裝不起來 :expressionless:)。 110 | 111 | 首先, 我使用的 Python 版本為 3.6.4, 112 | 113 | 安裝套件 114 | 115 | ```cmd 116 | pip install -r requirements.txt 117 | ``` 118 | 119 | [requirements.txt](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/requirements.txt) 120 | 121 | ```txt 122 | Django==2.0.4 123 | channels==2.0.2 124 | channels_redis==2.1.1 125 | ``` 126 | 127 | 使用 Django 2.0.4 以及 channels 2.0.2,channels_redis 2.1.1 為 `CHANNEL_LAYERS` 中的 `BACKEND` 需要使用到的。 128 | 129 | ### 用 docker 建立 redis 130 | 131 | 這部份只是和大家說明基本的環境設定,其實直接 `docker-compose up` 即可,因為我都幫大家包成 docker 了, 132 | 133 | 解決了環境的問題( 像我在 windows 上 `channels` 一直裝不起來 :expressionless:)。 134 | 135 | 因為這邊會使用到 redis,所以使用 docker 建立 redis,如果不了解 docker 以及 redis , 136 | 137 | 可參考下面這兩篇文章,分別介紹了 docker 以及 redis 138 | 139 | * [Docker 基本教學 - 從無到有 Docker-Beginners-Guide](https://github.com/twtrubiks/docker-tutorial) 140 | 141 | * [django-docker-redis-tutorial 基本教學](https://github.com/twtrubiks/django-docker-redis-tutorial) 142 | 143 | 建立 redis 指令, 144 | 145 | ```cmd 146 | docker run --name some-redis -p 6379:6379 -d redis redis-server --appendonly yes 147 | ``` 148 | 149 | ### channels installation 150 | 151 | 接下來將介紹 channels 的設定,官方文件可參考 [installation](https://channels.readthedocs.io/en/latest/installation.html), 152 | 153 | 將 channels 加入 INSTALLED_APPS, 154 | 155 | django_channels2_tutorial/[settings.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/settings.py) 156 | 157 | ```python 158 | INSTALLED_APPS = [ 159 | .... 160 | 'django.contrib.messages', 161 | 'django.contrib.staticfiles', 162 | 'channels', 163 | 'chat', 164 | ] 165 | ``` 166 | 167 | 溫馨小提醒:heart: 168 | 169 | `channels` 官方範例會將他放在最前面的原因是,有些套件會衝突,所以將他放到第一順位這樣。 170 | 171 | chat 是我們建立的( 後面會介紹 ), 172 | 173 | 接著建立 default routing, 174 | 175 | django_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py) 176 | 177 | ```python 178 | from channels.auth import AuthMiddlewareStack 179 | from channels.routing import ProtocolTypeRouter, URLRouter 180 | 181 | import chat.routing 182 | 183 | application = ProtocolTypeRouter({ 184 | # Empty for now (http->django views is added by default) 185 | 'websocket': AuthMiddlewareStack( 186 | URLRouter( 187 | chat.routing.websocket_urlpatterns 188 | ) 189 | ), 190 | }) 191 | ``` 192 | 193 | `chat.routing` 以及 `chat.routing.websocket_urlpatterns` 是我們自己建立的( 後面會介紹 ), 194 | 195 | 設定 channel settings, 196 | 197 | django_channels2_tutorial/[settings.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/settings.py) 198 | 199 | ```python 200 | ASGI_APPLICATION = "django_channels2_tutorial.routing.application" 201 | CHANNEL_LAYERS = { 202 | 'default': { 203 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 204 | 'CONFIG': { 205 | 'hosts': [('redis', 6379)], 206 | }, 207 | } 208 | } 209 | ``` 210 | 211 | `ASGI_APPLICATION` 設定為自己的 project 名稱 ( 這裡我們命名為 `django_channels2_tutorial` ), 212 | 213 | 指向底下的 routing( 我們剛剛建立的 )裡的 application( 剛剛建立的 ),所以完整名稱為 214 | 215 | `django_channels2_tutorial.routing.application`。 216 | 217 | `CHANNEL_LAYERS` 中的 `BACKEND` 設定為 redis ,也就是為什麼我們前面要安裝 `channels_redis` 的原因, 218 | 219 | `CONFIG` 就是設定連線 redis 字串,是不是很好奇為什麼 `host` 的部份我直接寫 `redis`? 220 | 221 | ( 其實就是 [docker-compose.yml](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/docker-compose.yml) 中的 redis 名稱 )。 222 | 223 | 如果大家還是不了解,建議可以閱讀 [這篇](https://github.com/twtrubiks/docker-tutorial#user-defined-networks) 的說明。 224 | 225 | ### 說明 226 | 227 | 剛剛上面提到了 [chat](https://github.com/twtrubiks/django-channels2-tutorial/tree/master/chat) 資料夾,接下來讓我們來看看 [chat](https://github.com/twtrubiks/django-channels2-tutorial/tree/master/chat) 做了什麼事情, 228 | 229 | chat/[views.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/views.py) 230 | 231 | ```python 232 | import json 233 | 234 | from django.shortcuts import render 235 | from django.utils.safestring import mark_safe 236 | 237 | def index(request): 238 | return render(request, 'chat/index.html', {}) 239 | 240 | 241 | def room(request, room_name): 242 | return render(request, 'chat/room.html', { 243 | 'room_name_json': mark_safe(json.dumps(room_name)) 244 | }) 245 | ``` 246 | 247 | chat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py) 248 | 249 | ```python 250 | # chat/urls.py 251 | from django.urls import path 252 | 253 | from . import views 254 | 255 | urlpatterns = [ 256 | path('', views.index, name='index'), 257 | path('/', views.room, name='room'), 258 | ] 259 | ``` 260 | 261 | 這邊很簡單,就是定義好 views 以及 url 而已,比較需要注意的是 url 的部份, 262 | 263 | 因為我們使用的是 `django 2.0`,所以已經改用 `path` 了,其實總體來說,我 264 | 265 | 覺得`django 2.0` 在處理 url 上更方便了,以前要寫正則表達式:scream:。 266 | 267 | 我們來看一下比較重要的 consumers, 268 | 269 | 詳細的介紹,可參考官網說明 [consumers](https://channels.readthedocs.io/en/stable/topics/consumers.html), 270 | 271 | 這裡先給大家簡單的觀念,consumers 是在 Channels 中的一個基本單位,當一個 request 或 socket 進來時, 272 | 273 | Channels 會去找他的 routing table,找到對的 consumers,基本上,consumers 就像是 Django 中的 views。 274 | 275 | consumers 有兩個點要和大家提一下( 擷取官方說明 ), 276 | 277 | * Structures your code as a series of functions to be called whenever an event happens, rather than making you write an event loop. 278 | 279 | * Allow you to write synchronous or async code and deals with handoffs and threading for you. 280 | 281 | 先來看 chat/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/routing.py), 282 | 283 | ```python 284 | from django.urls import path 285 | 286 | from . import consumers 287 | 288 | websocket_urlpatterns = [ 289 | path('ws/chat//', consumers.ChatConsumer), 290 | ] 291 | ``` 292 | 293 | 定義了 websocket_urlpatterns,並且設定 `ChatConsumer` class, 294 | 295 | 那我們在哪邊定義這個 routing 呢 ? 296 | 297 | django_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py) 298 | 299 | ```python 300 | from channels.auth import AuthMiddlewareStack 301 | from channels.routing import ProtocolTypeRouter, URLRouter 302 | 303 | import chat.routing 304 | 305 | application = ProtocolTypeRouter({ 306 | # Empty for now (http->django views is added by default) 307 | 'websocket': AuthMiddlewareStack( 308 | URLRouter( 309 | chat.routing.websocket_urlpatterns 310 | ) 311 | ), 312 | }) 313 | ``` 314 | 315 | root routing 設定的地方就是在前面介紹的 django_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py), 316 | 317 | 也就是上面的 `chat.routing.websocket_urlpatterns`。 318 | 319 | 接下來看 chat/[consumers.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/consumers.py), 320 | 321 | ```python 322 | import json 323 | 324 | from asgiref.sync import async_to_sync 325 | from channels.generic.websocket import WebsocketConsumer 326 | 327 | 328 | class ChatConsumer(WebsocketConsumer): 329 | def connect(self): 330 | self.room_name = self.scope['url_route']['kwargs']['room_name'] 331 | self.room_group_name = 'chat_%s' % self.room_name 332 | 333 | # Join room group 334 | async_to_sync(self.channel_layer.group_add)( 335 | self.room_group_name, 336 | self.channel_name 337 | ) 338 | 339 | self.accept() 340 | 341 | def disconnect(self, close_code): 342 | # Leave room group 343 | async_to_sync(self.channel_layer.group_discard)( 344 | self.room_group_name, 345 | self.channel_name 346 | ) 347 | 348 | # Receive message from WebSocket 349 | def receive(self, text_data): 350 | text_data_json = json.loads(text_data) 351 | message = text_data_json['message'] 352 | 353 | # Send message to room group 354 | async_to_sync(self.channel_layer.group_send)( 355 | self.room_group_name, 356 | { 357 | 'type': 'chat_message', 358 | 'message': message 359 | } 360 | ) 361 | 362 | # Receive message from room group 363 | def chat_message(self, event): 364 | message = event['message'] 365 | 366 | # Send message to WebSocket 367 | self.send(text_data=json.dumps({ 368 | 'message': message 369 | })) 370 | ``` 371 | 372 | 以上是使用 synchronous( 同步 )的方法。 373 | 374 | 接著將介紹他們互動的流程( 事件如何觸發 ), 375 | 376 | `connect` 377 | 378 | 當前端發 Websocket 過來的時候會觸發此事件, 379 | 380 | 那前端哪時候會送訊息過來呢 ? 381 | 382 | 我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html), 383 | 384 | ```javascript 385 | ... 386 | 393 | ``` 394 | 395 | 當前端 WebSocket 初始話連線的時候,會觸發 `connect`。 396 | 397 | 接下來說明 `connect` 中的一些方法, 398 | 399 | 首先是 `self.scope` 這個,你可以把它想成像是 Django 裡的 `self.request`, 400 | 401 | 而 `url_route` 則是抓取 url,我們取出 `room_name` ,為什麼是 `room_name` , 402 | 403 | 原因是我們在 chat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py) 中設定 `urlpatterns` 變數為 `room_name`, 404 | 405 | chat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py) 406 | 407 | ```python 408 | from django.urls import path 409 | 410 | from . import views 411 | 412 | urlpatterns = [ 413 | path('', views.index, name='index'), 414 | path('/', views.room, name='room'), 415 | ] 416 | ``` 417 | 418 | 接下來我們透過 `async_to_sync` 把 channel 加入 group 中,channel 和 group 的關係也不用想的太複雜, 419 | 420 | 其實他們的關係就是一個 group 中,可以有很多個 channel 這樣。 421 | 422 | 最後是 `self.accept()` 這個,就是接受這個連線,如果要拒絕這次的連線,使用 `self.close()` 即可。 423 | 424 | `disconnect` 425 | 426 | 將 channel 從 group 中移除, 427 | 428 | 我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html) 429 | 430 | ```javascript 431 | ... 432 | 439 | ``` 440 | 441 | 當 server 端的 WebSocket 關閉時,前端的 `chatSocket.onclose` 會被觸發。 442 | 443 | `receive` 444 | 445 | 當我們收到來至前端的 WebSocket 訊息時, 446 | 447 | 那前端哪時候會送訊息過來呢 ? 448 | 449 | 我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html) 450 | 451 | ```javascript 452 | ... 453 | 465 | ``` 466 | 467 | `chatSocket.send` 就會觸發這個事件,`receive` 將收到的 message 送到對應的 468 | group 中, 469 | 470 | `type` 就是指 `chat_message`。 471 | 472 | `chat_message` 473 | 474 | 當從 group 中收到 message 時,會觸發這個事件,我們將收到的 message 送回前端的 WebSocket, 475 | 476 | 那前端誰接收的? 477 | 478 | 我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html) 479 | 480 | ```javascript 481 | ... 482 | 491 | ``` 492 | 493 | `chatSocket.onmessage` 會收到訊息,前端再將訊息增加到畫面上。 494 | 495 | 以上,就是整個前後端 WebSocket 事件互動的流程。 496 | 497 | ### Rewrite Chat Server as Asynchronous 498 | 499 | 官網可參考 [Tutorial Part 3](https://channels.readthedocs.io/en/latest/tutorial/part_3.html), 500 | 501 | 剛剛是使用 synchronous( 同步 )的方法,現在我們要改寫他為 asynchronous( 非同步)的方法, 502 | 503 | ```python 504 | import json 505 | from channels.generic.websocket import AsyncWebsocketConsumer 506 | 507 | 508 | class ChatConsumer(AsyncWebsocketConsumer): 509 | async def connect(self): 510 | self.room_name = self.scope['url_route']['kwargs']['room_name'] 511 | self.room_group_name = 'chat_%s' % self.room_name 512 | 513 | # Join room group 514 | await self.channel_layer.group_add( 515 | self.room_group_name, 516 | self.channel_name 517 | ) 518 | 519 | await self.accept() 520 | 521 | async def disconnect(self, close_code): 522 | # Leave room group 523 | await self.channel_layer.group_discard( 524 | self.room_group_name, 525 | self.channel_name 526 | ) 527 | 528 | # Receive message from WebSocket 529 | async def receive(self, text_data): 530 | text_data_json = json.loads(text_data) 531 | message = text_data_json['message'] 532 | 533 | # Send message to room group 534 | await self.channel_layer.group_send( 535 | self.room_group_name, 536 | { 537 | 'type': 'chat_message', 538 | 'message': message 539 | } 540 | ) 541 | 542 | # Receive message from room group 543 | async def chat_message(self, event): 544 | message = event['message'] 545 | 546 | # Send message to WebSocket 547 | await self.send(text_data=json.dumps({ 548 | 'message': message 549 | })) 550 | 551 | ``` 552 | 553 | 官網的最後一部分是 [Automated Testing](https://channels.readthedocs.io/en/latest/tutorial/part_4.html),這部份我就沒有寫了,如果各位有興趣,就請再自行前往閱讀。 554 | 555 | ## 後記 556 | 557 | 這次和大家解釋了利用 channels 建立出的簡易版 chat room,也說明了他們互動的方式以及過程, 558 | 559 | 希望可以對 channels 有基礎的認識,如果意猶未盡,可以參考下一篇結合 database 以及美化的聊天 560 | 561 | 室,基本上是用這篇的教學延伸出去的,可參考 [django-chat-room](https://github.com/twtrubiks/django-chat-room) 。 562 | 563 | ## 執行環境 564 | 565 | * Python 3.6.4 566 | 567 | ## Reference 568 | 569 | * [Django](https://www.djangoproject.com/) 570 | * [Channels](https://github.com/django/channels) 571 | 572 | ## Donation 573 | 574 | 文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡:laughing: 575 | 576 | ![alt tag](https://i.imgur.com/LRct9xa.png) 577 | 578 | [贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8) 579 | 580 | ## License 581 | 582 | MIT licens 583 | -------------------------------------------------------------------------------- /chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django-channels2-tutorial/f46d77e8ddc4107cb5bcdff1ebc8da08822814d0/chat/__init__.py -------------------------------------------------------------------------------- /chat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | name = 'chat' 6 | -------------------------------------------------------------------------------- /chat/consumers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | synchronous version 4 | ''' 5 | 6 | import json 7 | 8 | from asgiref.sync import async_to_sync 9 | from channels.generic.websocket import WebsocketConsumer 10 | 11 | 12 | class ChatConsumer(WebsocketConsumer): 13 | def connect(self): 14 | self.room_name = self.scope['url_route']['kwargs']['room_name'] 15 | self.room_group_name = 'chat_%s' % self.room_name 16 | # Join room group 17 | async_to_sync(self.channel_layer.group_add)( 18 | self.room_group_name, 19 | self.channel_name 20 | ) 21 | 22 | self.accept() 23 | 24 | def disconnect(self, close_code): 25 | # Leave room group 26 | async_to_sync(self.channel_layer.group_discard)( 27 | self.room_group_name, 28 | self.channel_name 29 | ) 30 | 31 | # Receive message from WebSocket 32 | def receive(self, text_data): 33 | text_data_json = json.loads(text_data) 34 | message = text_data_json['message'] 35 | # Send message to room group 36 | async_to_sync(self.channel_layer.group_send)( 37 | self.room_group_name, 38 | { 39 | 'type': 'chat_message', 40 | 'message': message 41 | } 42 | ) 43 | 44 | # Receive message from room group 45 | def chat_message(self, event): 46 | message = event['message'] 47 | # Send message to WebSocket 48 | self.send(text_data=json.dumps({ 49 | 'message': message 50 | })) 51 | 52 | 53 | ''' 54 | 55 | asynchronous version 56 | ''' 57 | 58 | # import json 59 | # from channels.generic.websocket import AsyncWebsocketConsumer 60 | # 61 | # 62 | # class ChatConsumer(AsyncWebsocketConsumer): 63 | # async def connect(self): 64 | # self.room_name = self.scope['url_route']['kwargs']['room_name'] 65 | # self.room_group_name = 'chat_%s' % self.room_name 66 | # 67 | # # Join room group 68 | # await self.channel_layer.group_add( 69 | # self.room_group_name, 70 | # self.channel_name 71 | # ) 72 | # 73 | # await self.accept() 74 | # 75 | # async def disconnect(self, close_code): 76 | # # Leave room group 77 | # await self.channel_layer.group_discard( 78 | # self.room_group_name, 79 | # self.channel_name 80 | # ) 81 | # 82 | # # Receive message from WebSocket 83 | # async def receive(self, text_data): 84 | # text_data_json = json.loads(text_data) 85 | # message = text_data_json['message'] 86 | # 87 | # # Send message to room group 88 | # await self.channel_layer.group_send( 89 | # self.room_group_name, 90 | # { 91 | # 'type': 'chat_message', 92 | # 'message': message 93 | # } 94 | # ) 95 | # 96 | # # Receive message from room group 97 | # async def chat_message(self, event): 98 | # message = event['message'] 99 | # 100 | # # Send message to WebSocket 101 | # await self.send(text_data=json.dumps({ 102 | # 'message': message 103 | # })) 104 | # 105 | -------------------------------------------------------------------------------- /chat/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /chat/routing.py: -------------------------------------------------------------------------------- 1 | # chat/routing.py 2 | from django.urls import path 3 | 4 | from . import consumers 5 | 6 | websocket_urlpatterns = [ 7 | path('ws/chat//', consumers.ChatConsumer), 8 | ] 9 | -------------------------------------------------------------------------------- /chat/templates/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Rooms 7 | 8 | 9 | What chat room would you like to enter?
10 |
11 | 12 | 13 | 26 | -------------------------------------------------------------------------------- /chat/templates/chat/room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Room 7 | 8 | 9 |
10 |
11 | 12 | 13 | 49 | -------------------------------------------------------------------------------- /chat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /chat/urls.py: -------------------------------------------------------------------------------- 1 | # chat/urls.py 2 | from django.urls import path 3 | 4 | from . import views 5 | 6 | urlpatterns = [ 7 | path('', views.index, name='index'), 8 | path('/', views.room, name='room'), 9 | ] 10 | -------------------------------------------------------------------------------- /chat/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.shortcuts import render 4 | from django.utils.safestring import mark_safe 5 | 6 | 7 | # Create your views here. 8 | 9 | def index(request): 10 | return render(request, 'chat/index.html', {}) 11 | 12 | 13 | def room(request, room_name): 14 | return render(request, 'chat/room.html', { 15 | 'room_name_json': mark_safe(json.dumps(room_name)) 16 | }) 17 | -------------------------------------------------------------------------------- /django_channels2_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django-channels2-tutorial/f46d77e8ddc4107cb5bcdff1ebc8da08822814d0/django_channels2_tutorial/__init__.py -------------------------------------------------------------------------------- /django_channels2_tutorial/routing.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | 4 | import chat.routing 5 | 6 | application = ProtocolTypeRouter({ 7 | # Empty for now (http->django views is added by default) 8 | 'websocket': AuthMiddlewareStack( 9 | URLRouter( 10 | chat.routing.websocket_urlpatterns 11 | ) 12 | ), 13 | }) 14 | -------------------------------------------------------------------------------- /django_channels2_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_channels2_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '-4j-_62in#s7%d_1^-4pm3tx63oda^hkg4-zw_a(zsngb4j&tc' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'channels', 41 | 'chat', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'django_channels2_tutorial.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'django_channels2_tutorial.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | # channel settings 86 | ASGI_APPLICATION = "django_channels2_tutorial.routing.application" 87 | CHANNEL_LAYERS = { 88 | 'default': { 89 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 90 | 'CONFIG': { 91 | 'hosts': [('redis', 6379)], 92 | }, 93 | } 94 | } 95 | 96 | 97 | # Password validation 98 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 99 | 100 | AUTH_PASSWORD_VALIDATORS = [ 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 112 | }, 113 | ] 114 | 115 | 116 | # Internationalization 117 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 118 | 119 | LANGUAGE_CODE = 'en-us' 120 | 121 | TIME_ZONE = 'UTC' 122 | 123 | USE_I18N = True 124 | 125 | USE_L10N = True 126 | 127 | USE_TZ = True 128 | 129 | 130 | # Static files (CSS, JavaScript, Images) 131 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 132 | 133 | STATIC_URL = '/static/' 134 | 135 | TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'templates'),) 136 | -------------------------------------------------------------------------------- /django_channels2_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | """django_channels2_tutorial URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('chat/', include('chat.urls')), 22 | ] 23 | -------------------------------------------------------------------------------- /django_channels2_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_channels2_tutorial project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_channels2_tutorial.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | # docker run --name some-redis -p 6379:6379 -d redis redis-server --appendonly yes 5 | 6 | redis: 7 | container_name: redis 8 | image: redis 9 | restart: always 10 | command: ["redis-server", "--appendonly", "yes"] 11 | ports: 12 | - "6379:6379" 13 | volumes: 14 | - redis-data:/data 15 | app: 16 | build: . 17 | command: bash -c "python manage.py runserver 0.0.0.0:8000" 18 | restart: always 19 | ports: 20 | - "8000:8000" 21 | volumes: 22 | - .:/django_channels2_tutorial 23 | depends_on: 24 | - redis 25 | volumes: 26 | redis-data: 27 | pgdata: 28 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_channels2_tutorial.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.0.4 2 | channels==2.0.2 3 | channels_redis==2.1.1 4 | --------------------------------------------------------------------------------