少し空きましたが前回に引き続きChannelsを触っていきます。前回はChannelsでWebSocketでのエコーサーバ的なところまで実装しましたので、Room等を作っていきます。
Channel Layerの有効化
Roomを実装するためには複数のコネクションがメッセージを共有できる必要があります。ChannelsではChannel Layerというもが利用できます。これは複数のConsumer Instance間でメッセージを共有する仕組みを提供します。
channel layerにはGroupという概念があります。Groupには名前をつける事ができ、同じ名前を指定することで相互にメッセージをやり取りすることができます。
今回利用しているリポジトリではdocker-compose.yml
にすでにredisサーバの定義がかいてありますので、実はすでにredisが起動しています。これを利用します。
mysite/settings.py
に以下を追記して、Redisをバックエンドに使用できるようにします。
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('redis', 6379)], }, }, }
これで再起動すればChannel Layerが有効化されます。default
という名前からわかるようにバックエンドは複数定義できますが殆どのケースでdefault
だけ定義しておけばいいようです。
Groupを利用したChatRoomの実装
有効化したChannel Layerを利用していきます。mysite/chat/consumer.py
に定義していたChatConsumer
を以下の様に書き換えます。
from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer import json class ChatConsumer(WebsocketConsumer): """ WebSocketでの通信をハンドルする """ 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=None, bytes_data=None): 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 }))
connect
時にself.scope['url_route']['kwargs']['room_name']
の部分で部屋名を取得している。これはmysite/chat/routing.py
に定義していた以下の部分に関係します。
# chat/routing.py from django.urls import path from . import consumers websocket_urlpatterns = [ path('ws/chat/<str:room_name>/', consumers.ChatConsumer), ]
self.scope
にはそのコネクションの情報が入っており、self.scope['url_route']['kwargs']
にはURLのパターンから取得出来る情報が入っています。今回のケースではws/chat/<str:room_name>/
というURLパターンですのでws/chat/hoge
というアクセスではroom_name=hoge
が代入されています。
ここで取得したRoom名を元に以下の部分でself.channel_layer.group_add
を利用してGroupに参加しています。
async_to_sync(self.channel_layer.group_add)( self.room_group_name, self.channel_name )
関数自体はself.channel_layer.group_add
ですがasync_to_sync
(from asgiref.sync import async_to_sync
)でラップされています。これはasync_to_sync
は名前の通りですが、非同期関数を同期関数に変換するものです。今回継承しているWebsocketConsumer
は同期処理の実装なので、その中で呼び出す処理も同期処理である必要がありますが、channel_layer
関連の処理は非同期なのでそのままでは呼び出せないので、ラップしているわけです。
disconnect
は切断時の処理ですが、呼び出す関数がself.channel_layer.group_discard
になる程度でほとんど変わりません。切断時に参加していたGroupから離脱するだけです。
def receive(self, text_data=None, bytes_data=None): 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 } )
この部分では前回まで単にself.send
で返送していた部分がself.channel_layer.group_send
に変わっています。自身が参加しているroom_group_name
を指定してメッセージを送信しています。こうすることで同じグループに参加しているコネクション全てにメッセージが送信されます。
Group宛に送信されたメッセージの受信時はここで処理が行われています。
# 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 }))
関数名は送信時に指定している'type': 'chat_message',
と一致します。受け取ったevent
にはgroup_send
で指定したデータが入っているので、それを取得してからself.send
でクライアント側に戻しています。
これで複数のルーム間でメッセージが共有されチャットルームとしての必要な機能が実現されました。
今回のCommitは以下です。
まとめ
これで最低限チャットとしての機能が実装されました。次回はConsumerを非同期実装に差し替えていく予定です。