https://github.com/twtrubiks/django-channels2-tutorial
django-channels2 tutorial 💬
https://github.com/twtrubiks/django-channels2-tutorial
channels chat-room django docker tutorial-code
Last synced: 6 months ago
JSON representation
django-channels2 tutorial 💬
- Host: GitHub
- URL: https://github.com/twtrubiks/django-channels2-tutorial
- Owner: twtrubiks
- License: mit
- Created: 2018-04-11T12:34:26.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2018-04-12T13:46:17.000Z (over 7 years ago)
- Last Synced: 2025-04-15T12:12:10.165Z (6 months ago)
- Topics: channels, chat-room, django, docker, tutorial-code
- Language: Python
- Homepage:
- Size: 13.7 KB
- Stars: 33
- Watchers: 1
- Forks: 6
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# django-channels2-tutorial
django-channels2-tutorial 💬
* [Youtube Tutorial Part1 - django channels2 demo 以及簡介](https://youtu.be/jIMtZkfs8yY)
* [Youtube Tutorial Part2 - django channels2 tutorial](https://youtu.be/_4Q801WL8sA)
## 前言
最近剛好想玩一下聊天室,於是就找到 [Channels](https://github.com/django/channels),也從 [releases](https://github.com/django/channels/releases) 這裡發現在今年 2 月的時候 **Channels 2**
被 releases 出來,所以決定簡單整理一篇介紹給大家。
透過 Django [Channels](https://github.com/django/channels) 建立簡單的聊天室範例,此範例為官方的 [Tutorial](https://channels.readthedocs.io/en/latest/tutorial/index.html),希望能透過這個簡單的範例,讓大
家更了解 Channels,我有稍微修改一些部分,官方範例是使用 Channels 2.0 , Python 3.5+ , Django 1.11,
我這邊最主要的是將他修改為 Django 2.0。
注意,Channels 1 和 Channels 2 有蠻大的差異,這邊都是講 Channels 2,詳細可參考 [What’s new in Channels 2?](https://channels.readthedocs.io/en/latest/one-to-two.html)
之前,我也有使用過 flask 寫過聊天室,可參考 [chat-room](https://github.com/twtrubiks/chat-room)。
讓我們先來看看執行的畫面吧:laughing:
## 執行畫面
輸入一個名稱建立聊天室群組,直接瀏覽 [http://localhost:8000/chat/](http://localhost:8000/chat/)

接著可以在聊天室裡面打字

同一個聊天室群組會互相收到訊息,不同的聊天室群組訊息 **不會互通**,

我知道這個聊天室真的非常的醜:joy:,而且也沒搭配 database,但這篇只是一個要讓大家了解 Channels
如何建立一個聊天室,下一篇文章,我會依照這篇為雛形,建立一個有簡單的登入註冊系統以及美化過的
聊天室給各位,如果大家等不及想先搶先看,可瀏覽 [django-chat-room](https://github.com/twtrubiks/django-chat-room)。
但建議這篇文章還是要看,因為我將介紹一些基本的概念以及互動的流程。
## 如何執行
確認電腦有安裝 docker 後,直接執行以下指令即可,
```cmd
docker-compose up
```
如何移除 ( 包含移除 volume ),
```cmd
docker-compose down -v
```## 簡介
這邊先介紹幾個名詞,我不會講的非常詳細,因為大家可以用關鍵字去 google ,很多文章都解釋非常清楚了:grin:
### WebSocket
WebSocket 是一種單一 TCP 連線上進行全雙工(full-duplex)通訊管道,可以讓網頁與伺服器之間做即時性、
雙向的資料傳遞。
Websocket 需要先建立連線,需要通過瀏覽器發出請求,之後伺服器進行回應,這段過程稱為 **交握**( handshaking )。
延伸閱讀,如果大家有興趣,可以再去看看 polling ( 輪詢 ) 的概念。
### Channels
本次的主角,你可以把 Django 想成是 synchronous ( 同步 ),而透過 Channels,可以改變
Django synchronous( 同步)的核心轉變為 asynchronous(非同步)的程式碼。
以下擷取官方文件
channels allowing Django projects to handle not only HTTP, but protocols that require long-running connections too WebSockets, MQTT, chatbots, amateur radio, and more.
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.
channels 支持很多協定,而且也整合了 Django 的 auth 以及 session 系統等等。
### ASGI
ASGI 全名為 Asynchronous Server Gateway Interface,
他是 WSGI 的精神繼承者,不只是使用 `asyncio` 異步的方法運行,而且也支援多種協定。
更多說明可參考 [ASGI](https://channels.readthedocs.io/en/stable/asgi.html)。
## 教學
我將簡單說明這個範例的流程,但詳細的介紹,我還是非常建議大家觀看官方的 [Tutorial](https://channels.readthedocs.io/en/latest/tutorial/index.html) 範例。
### 建立環境
這部份只是和大家說明基本的環境設定,其實直接 `docker-compose up` 即可,因為我都幫大家包成 docker 了,
解決了環境的問題( 像我在 windows 上 `channels` 一直裝不起來 :expressionless:)。
首先, 我使用的 Python 版本為 3.6.4,
安裝套件
```cmd
pip install -r requirements.txt
```[requirements.txt](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/requirements.txt)
```txt
Django==2.0.4
channels==2.0.2
channels_redis==2.1.1
```使用 Django 2.0.4 以及 channels 2.0.2,channels_redis 2.1.1 為 `CHANNEL_LAYERS` 中的 `BACKEND` 需要使用到的。
### 用 docker 建立 redis
這部份只是和大家說明基本的環境設定,其實直接 `docker-compose up` 即可,因為我都幫大家包成 docker 了,
解決了環境的問題( 像我在 windows 上 `channels` 一直裝不起來 :expressionless:)。
因為這邊會使用到 redis,所以使用 docker 建立 redis,如果不了解 docker 以及 redis ,
可參考下面這兩篇文章,分別介紹了 docker 以及 redis
* [Docker 基本教學 - 從無到有 Docker-Beginners-Guide](https://github.com/twtrubiks/docker-tutorial)
* [django-docker-redis-tutorial 基本教學](https://github.com/twtrubiks/django-docker-redis-tutorial)
建立 redis 指令,
```cmd
docker run --name some-redis -p 6379:6379 -d redis redis-server --appendonly yes
```### channels installation
接下來將介紹 channels 的設定,官方文件可參考 [installation](https://channels.readthedocs.io/en/latest/installation.html),
將 channels 加入 INSTALLED_APPS,
django_channels2_tutorial/[settings.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/settings.py)
```python
INSTALLED_APPS = [
....
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'chat',
]
```溫馨小提醒:heart:
`channels` 官方範例會將他放在最前面的原因是,有些套件會衝突,所以將他放到第一順位這樣。
chat 是我們建立的( 後面會介紹 ),
接著建立 default routing,
django_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py)
```python
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouterimport chat.routing
application = ProtocolTypeRouter({
# Empty for now (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
````chat.routing` 以及 `chat.routing.websocket_urlpatterns` 是我們自己建立的( 後面會介紹 ),
設定 channel settings,
django_channels2_tutorial/[settings.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/settings.py)
```python
ASGI_APPLICATION = "django_channels2_tutorial.routing.application"
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('redis', 6379)],
},
}
}
````ASGI_APPLICATION` 設定為自己的 project 名稱 ( 這裡我們命名為 `django_channels2_tutorial` ),
指向底下的 routing( 我們剛剛建立的 )裡的 application( 剛剛建立的 ),所以完整名稱為
`django_channels2_tutorial.routing.application`。
`CHANNEL_LAYERS` 中的 `BACKEND` 設定為 redis ,也就是為什麼我們前面要安裝 `channels_redis` 的原因,
`CONFIG` 就是設定連線 redis 字串,是不是很好奇為什麼 `host` 的部份我直接寫 `redis`?
( 其實就是 [docker-compose.yml](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/docker-compose.yml) 中的 redis 名稱 )。
如果大家還是不了解,建議可以閱讀 [這篇](https://github.com/twtrubiks/docker-tutorial#user-defined-networks) 的說明。
### 說明
剛剛上面提到了 [chat](https://github.com/twtrubiks/django-channels2-tutorial/tree/master/chat) 資料夾,接下來讓我們來看看 [chat](https://github.com/twtrubiks/django-channels2-tutorial/tree/master/chat) 做了什麼事情,
chat/[views.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/views.py)
```python
import jsonfrom django.shortcuts import render
from django.utils.safestring import mark_safedef index(request):
return render(request, 'chat/index.html', {})def room(request, room_name):
return render(request, 'chat/room.html', {
'room_name_json': mark_safe(json.dumps(room_name))
})
```chat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py)
```python
# chat/urls.py
from django.urls import pathfrom . import views
urlpatterns = [
path('', views.index, name='index'),
path('/', views.room, name='room'),
]
```這邊很簡單,就是定義好 views 以及 url 而已,比較需要注意的是 url 的部份,
因為我們使用的是 `django 2.0`,所以已經改用 `path` 了,其實總體來說,我
覺得`django 2.0` 在處理 url 上更方便了,以前要寫正則表達式:scream:。
我們來看一下比較重要的 consumers,
詳細的介紹,可參考官網說明 [consumers](https://channels.readthedocs.io/en/stable/topics/consumers.html),
這裡先給大家簡單的觀念,consumers 是在 Channels 中的一個基本單位,當一個 request 或 socket 進來時,
Channels 會去找他的 routing table,找到對的 consumers,基本上,consumers 就像是 Django 中的 views。
consumers 有兩個點要和大家提一下( 擷取官方說明 ),
* Structures your code as a series of functions to be called whenever an event happens, rather than making you write an event loop.
* Allow you to write synchronous or async code and deals with handoffs and threading for you.
先來看 chat/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/routing.py),
```python
from django.urls import pathfrom . import consumers
websocket_urlpatterns = [
path('ws/chat//', consumers.ChatConsumer),
]
```定義了 websocket_urlpatterns,並且設定 `ChatConsumer` class,
那我們在哪邊定義這個 routing 呢 ?
django_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py)
```python
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouterimport chat.routing
application = ProtocolTypeRouter({
# Empty for now (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
```root routing 設定的地方就是在前面介紹的 django_channels2_tutorial/[routing.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/django_channels2_tutorial/routing.py),
也就是上面的 `chat.routing.websocket_urlpatterns`。
接下來看 chat/[consumers.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/consumers.py),
```python
import jsonfrom asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumerclass ChatConsumer(WebsocketConsumer):
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
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)self.accept()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)# Receive message from WebSocket
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)# Receive message from room group
def chat_message(self, event):
message = event['message']# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message
}))
```以上是使用 synchronous( 同步 )的方法。
接著將介紹他們互動的流程( 事件如何觸發 ),
`connect`
當前端發 Websocket 過來的時候會觸發此事件,
那前端哪時候會送訊息過來呢 ?
我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html),
```javascript
...var roomName = {{ room_name_json }};
var chatSocket = new WebSocket(
'ws://' + window.location.host + '/ws/chat/' + roomName + '/');
...```
當前端 WebSocket 初始話連線的時候,會觸發 `connect`。
接下來說明 `connect` 中的一些方法,
首先是 `self.scope` 這個,你可以把它想成像是 Django 裡的 `self.request`,
而 `url_route` 則是抓取 url,我們取出 `room_name` ,為什麼是 `room_name` ,
原因是我們在 chat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py) 中設定 `urlpatterns` 變數為 `room_name`,
chat/[urls.py](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/urls.py)
```python
from django.urls import pathfrom . import views
urlpatterns = [
path('', views.index, name='index'),
path('/', views.room, name='room'),
]
```接下來我們透過 `async_to_sync` 把 channel 加入 group 中,channel 和 group 的關係也不用想的太複雜,
其實他們的關係就是一個 group 中,可以有很多個 channel 這樣。
最後是 `self.accept()` 這個,就是接受這個連線,如果要拒絕這次的連線,使用 `self.close()` 即可。
`disconnect`
將 channel 從 group 中移除,
我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html)
```javascript
.......
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
....```
當 server 端的 WebSocket 關閉時,前端的 `chatSocket.onclose` 會被觸發。
`receive`
當我們收到來至前端的 WebSocket 訊息時,
那前端哪時候會送訊息過來呢 ?
我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html)
```javascript
.......
document.querySelector('#chat-message-submit').onclick = function(e) {
var messageInputDom = document.querySelector('#chat-message-input');
var message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));messageInputDom.value = '';
};```
`chatSocket.send` 就會觸發這個事件,`receive` 將收到的 message 送到對應的
group 中,`type` 就是指 `chat_message`。
`chat_message`
當從 group 中收到 message 時,會觸發這個事件,我們將收到的 message 送回前端的 WebSocket,
那前端誰接收的?
我們來看 chat/templates/chat/[room.html](https://github.com/twtrubiks/django-channels2-tutorial/blob/master/chat/templates/chat/room.html)
```javascript
.......
chatSocket.onmessage = function(e) {
var data = JSON.parse(e.data);
var message = data['message'];
document.querySelector('#chat-log').value += (message + '\n');
};
....```
`chatSocket.onmessage` 會收到訊息,前端再將訊息增加到畫面上。
以上,就是整個前後端 WebSocket 事件互動的流程。
### Rewrite Chat Server as Asynchronous
官網可參考 [Tutorial Part 3](https://channels.readthedocs.io/en/latest/tutorial/part_3.html),
剛剛是使用 synchronous( 同步 )的方法,現在我們要改寫他為 asynchronous( 非同步)的方法,
```python
import json
from channels.generic.websocket import AsyncWebsocketConsumerclass 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
}))```
官網的最後一部分是 [Automated Testing](https://channels.readthedocs.io/en/latest/tutorial/part_4.html),這部份我就沒有寫了,如果各位有興趣,就請再自行前往閱讀。
## 後記
這次和大家解釋了利用 channels 建立出的簡易版 chat room,也說明了他們互動的方式以及過程,
希望可以對 channels 有基礎的認識,如果意猶未盡,可以參考下一篇結合 database 以及美化的聊天
室,基本上是用這篇的教學延伸出去的,可參考 [django-chat-room](https://github.com/twtrubiks/django-chat-room) 。
## 執行環境
* Python 3.6.4
## Reference
* [Django](https://www.djangoproject.com/)
* [Channels](https://github.com/django/channels)## Donation
文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡:laughing:

[贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8)
## License
MIT licens