Django と Channelsでチャット作成のチュートリアルをやってみる(Tutorial Part 2: Implement a Chat Server)
今回はいよいよChannelsを利用してチャットのデモを作るところ
チュートリアルと環境が違うこと、Redis用に新たにdockerコンテナを作るところがあったけど敢えてコンテナを作らず直接インストールしようとしたらすんごいハマってしまった。
でもうまく行ったからよしとしよう
前提
- Django: 2.2.8(本当はDjang 3.0+がいいけどローカルがこれだったので。。)
- Channels: 2.4.0
- Redis-server: 5.0.9
3. tutorial
3-1. Channlesのセットアップ
channles用アプリを作る
$ python manage.py startapp chat
今回はchat/views.py
, chhat/__init__.py
しか使わないので他のファイルは削除する
<project-app>/settings.py
に先程追加したchat
を有効化する処理を追記する
INSTALLED_APPS = [ ... # My applications 'chat', ... ]
chat/templates/chat/index.html
を作成する
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Chat Rooms</title> </head> <body> What chat room would you like to enter?<br> <input id="room-name-input" type="text" size="100"><br> <input id="room-name-submit" type="button" value="Enter"> <script> document.querySelector('#room-name-input').focus(); document.querySelector('#room-name-input').onkeyup = function(e) { if (e.keyCode === 13) { // enter, return document.querySelector('#room-name-submit').click(); } }; document.querySelector('#room-name-submit').onclick = function(e) { var roomName = document.querySelector('#room-name-input').value; window.location.pathname = '/chat/' + roomName + '/'; }; </script> </body> </html>
chat/views.py
に以下を追記する
def index(request): # chat/templates/chat/index.html をビューとして返す return render(request, 'chat/index.html')
chat/urls.py
, <project-app>/urls.py
にルーティングを追加する
# chat/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index') ]
# <project-app>/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ ... path('chat/', include('chat.urls')), ]
3-2. チャットサーバーの実装(Implement a Chat Server)
チャット部屋用のviewを追加する
$ touch <project-app>/chat/templates/chat/room.html
<!-- chat/templates/chat/room.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Chat Room</title> </head> <body> <textarea id="chat-log" cols="100" rows="20"></textarea><br> <input id="chat-message-input" type="text" size="100"><br> <input id="chat-message-submit" type="button" value="Send"> {{ room_name|json_script:"room-name" }} <script> const roomName = JSON.parse(document.getElementById('room-name').textContent); const chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/' + roomName + '/' ); chatSocket.onmessage = function(e) { const data = JSON.parse(e.data); document.querySelector('#chat-log').value += (data.message + '\n'); }; chatSocket.onclose = function(e) { console.error('Chat socket closed unexpectedly'); }; document.querySelector('#chat-message-input').focus(); document.querySelector('#chat-message-input').onkeyup = function(e) { if (e.keyCode === 13) { // enter, return document.querySelector('#chat-message-submit').click(); } }; document.querySelector('#chat-message-submit').onclick = function(e) { const messageInputDom = document.querySelector('#chat-message-input'); const message = messageInputDom.value; chatSocket.send(JSON.stringify({ 'message': message })); messageInputDom.value = ''; }; </script> </body> </html>
追加したテンプレートに対応するview, ルーティングを<project-root>/chat/views.py, urls.py
に追記する
def room(request, room_name): return render(request, 'chat/room.html', { 'room_name': room_name })
urlpatterns = [ path('', views.index, name='index'), path('<str:room_name>/', views.room, name='room'), # 追記 ]
http://localhost:8000/chat/lobby
にアクセスしてみるとチャット用画面が表示される
試しにテキストボックスにHello
と入力しsendを押してみるとWebSocketのエラー
が発生する
これはWebSocket
のルーティングが定義されていないためである
Websocket
の処理を記述するために<project-root>/chat/consumers.py
を作成する
$ touch <project-root>/chat/consumers.py
# chat/consumers.py import json from channels.generic.websocket import WebsocketConsumer class ChatConsumer(WebsocketConsumer): # 接続時処理 def connect(self): self.accept() # 切断時処理 def disconnect(self, close_code): pass # メッセージ受信時処理 def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] self.send(text_data=json.dumps({ 'message': message }))
consumers.py
用に新規でルーティングを<project-root>/chat/routing.py
に記載する
$ touch <project-root>/chat/routing.py
# chat/routing.py from django.urls import re_path from . import consumers websocket_urlpatterns = [ re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()), ] # Django 2.2以下では次のように記載する # `ChatConsumer` has no attribute `as_asgi()`とエラーが出るため websocket_urlpatterns = [ re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer), ]
上記のas_asgi()
はユーザの接続ごとにconsumerのインスタンスを生成するASGIアプリケーションである
Django
のas_view()
のようにユーザの接続ごとにviewを生成する動きにと同じようなもの
次に<project-root>/<project-app>/asgi.py
を以下のように編集する
# mysite/asgi.py import os from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application import chat.routing os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), }) # Django 2.2以下は以下のようなコードになる(get_asgi_applicationあたりが変わっている) import os import django from channels.auth import AuthMiddlewareStack from channels.http import AsgiHandler from channels.routing import ProtocolTypeRouter, URLRouter import chat.routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') django.setup() application = ProtocolTypeRouter({ "http": AsgiHandler, "websocket": AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), })
AuthMiddlewareStack
でWebSocket
の接続情報を定義することでWebSocket
の接続プロトコル(ws://
, wss://
)で接続できるようになる
改めてhttp://localhost:8000/chat/lobby
にアクセスする
テキストボックスにHello
と入力しsend
を押すとエラーなく上のテキストエリアにHelllo
と表示される
しかしタブを複数開きそれぞれでテキストボックスに文字を入力しても、別のタブには反映されない
これはChatConsumer
がそれぞれの接続ごとにインスタンス化されているが、お互いに会話はできていない状態となっている
Channels
にはchannel layer
というレイヤーが提供されており、これを利用することでChatConsumer
同士で会話ができるようになる
チャンネルレイヤーとは
各コンシューマインスタンス同士やDjangoの他のパーツとの会話ができるようになるシステム
チャンネルレイヤーは以下を提供している
channel
: メールボックス的なもの。各チャンネルには名前があり、その名前があれば(所属していれば?)誰でもメッセージを送ることができるgroup
: チャンネルの関連的なもの。各グループには名前があり、その名前があれば(所属していれば?)グループの追加/削除に、グループに所属している全てのチャンネルにメッセージを送ることができる
チャンネルレイヤーを利用するにはRedis
が動いている必要がある
Redis
をインストールする(Tutorialではdockerを起動しているが、PythonのDockerに手動でインストール → 起動してみる)
$ cd /code # redis-serverのインストール $ wget http://download.redis.io/releases/redis-5.0.9.tar.gz $ tar xzf redis-5.0.9.tar.gz $ cd redis-5.0.9 $ make install # redis-serverの起動(続けて操作するために&をつけることでBG起動) $ /usr/loca/bin/redis-server & # ダウンロードしたファイルの削除 $ cd .. $ rm redis-5.0.9.tar.gz $ rm -r redis-5.0.9
Redis
をPythonから利用するためのパッケージをインストールしておく
$ pip install channels_redis
チャンネルレイヤーを使うための設定を<project-root>/<project-app>/settings.py
に追記する
# mysite/settings.py # Channels ASGI_APPLICATION = 'mysite.routing.application' CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, }
チャンネルレイヤーがRedis
の元で動いているか確認をするためにshell
で実行してみる
$ cd <project-root> $ python manage.py shell # チャンネルレイヤーをimport >>> import channels.layers >>> channel_layer = channels.layers.get_channel_layer() >>> from asgiref.sync import async_to_sync # test_channelに`hello`とメッセージを送信 >>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'}) # test_channelに送信されたメッセージを受信 >>> async_to_sync(channel_layer.receive)('test_channel') # helloが取得できれば成功 {'type': 'hello'}
これでチャンネルレイヤーを使う準備ができたので、ChatCoonsumer
を以下の通り修正する
# chat/consumers.py import json from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer class 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 ) # メッセージ受信時処理 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 } ) # メッセージの受信→送信 def chat_message(self, event): message = event['message'] # Send message to WebSocket self.send(text_data=json.dumps({ 'message': message }))
- ユーザがPOSTしたメッセージはJavaScriptにてWebSocketを通じて
ChatConsumer
に届く ChatConsumer
はメッセージに対応するグループに属するChatConsumer
に転送する- グループ名はURLの部屋名称が
ChatConsumer.connect
にて設定されている
- グループ名はURLの部屋名称が
- 同じグループに属する
ChatConsumer
は受け取ったメッセージをWebSocket
を通じてJavaScriptに転送する(chatsocket.onmessage
部分)
ソースコードの修正が完了したら http://localhost:8000/chat/lobby
を2窓開き、それぞれでメッセージを送信してみる
お互いの窓でメッセージが表示されれば成功